Made backup manager more generic
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static fr.pandacube.lib.chat.ChatStatic.text;
|
||||
|
||||
public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> {
|
||||
|
||||
private static final boolean testOnly = true; // if true, no files are deleted
|
||||
|
||||
public static BackupCleaner KEEPING_N_LAST(int n) {
|
||||
return new BackupCleaner() {
|
||||
@Override
|
||||
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
|
||||
return archives.descendingSet().stream()
|
||||
.limit(n)
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) {
|
||||
return new BackupCleaner() {
|
||||
@Override
|
||||
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> localDateTimes) {
|
||||
return localDateTimes.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
ldt -> {
|
||||
return ldt.getYear() * 4 + ldt.getMonthValue() / n;
|
||||
},
|
||||
TreeMap::new,
|
||||
Collectors.minBy(LocalDateTime::compareTo))
|
||||
)
|
||||
.values()
|
||||
.stream()
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public BackupCleaner merge(BackupCleaner other) {
|
||||
BackupCleaner self = this;
|
||||
return new BackupCleaner() {
|
||||
@Override
|
||||
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
|
||||
TreeSet<LocalDateTime> merged = new TreeSet<>();
|
||||
merged.addAll(self.apply(archives));
|
||||
merged.addAll(other.apply(archives));
|
||||
return merged;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public void cleanupArchives(File archiveDir, String compressDisplayName) {
|
||||
String[] files = archiveDir.list();
|
||||
|
||||
Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "...");
|
||||
|
||||
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
|
||||
|
||||
for (String filename : files) {
|
||||
File file = new File(archiveDir, filename);
|
||||
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
|
||||
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
String dateTimeStr = filename.substring(0, filename.length() - 4);
|
||||
LocalDateTime ldt;
|
||||
try {
|
||||
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
|
||||
} catch (DateTimeParseException e) {
|
||||
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
datedFiles.put(ldt, file);
|
||||
}
|
||||
|
||||
TreeSet<LocalDateTime> keptFiles = apply(new TreeSet<>(datedFiles.keySet()));
|
||||
|
||||
Chat c = text("[Backup] ")
|
||||
.then(text(compressDisplayName).gray())
|
||||
.thenText(testOnly ? " Archive cleanup debug (no files are actually deleted):\n" : " Deleted archive files:\n");
|
||||
boolean oneDeleted = false;
|
||||
for (Entry<LocalDateTime, File> datedFile : datedFiles.entrySet()) {
|
||||
if (keptFiles.contains(datedFile.getKey())) {
|
||||
if (testOnly)
|
||||
c.thenText("- " + datedFile.getValue().getName() + " ")
|
||||
.thenSuccess("kept")
|
||||
.thenText(".\n");
|
||||
continue;
|
||||
}
|
||||
oneDeleted = true;
|
||||
c.thenText("- " + datedFile.getValue().getName() + " ");
|
||||
if (testOnly)
|
||||
c.thenFailure("deleted")
|
||||
.thenText(".\n");
|
||||
else
|
||||
datedFile.getValue().delete();
|
||||
}
|
||||
|
||||
if (testOnly || oneDeleted)
|
||||
Log.warning(c.getLegacyText());
|
||||
|
||||
Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned.");
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fc.cron.CronExpression;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
public class BackupManager extends TimerTask {
|
||||
|
||||
private final File backupDirectory;
|
||||
|
||||
protected final Persist persist;
|
||||
|
||||
protected final List<BackupProcess> backupQueue = new ArrayList<>();
|
||||
|
||||
/* package */ final AtomicReference<BackupProcess> runningBackup = new AtomicReference<>();
|
||||
|
||||
private final Timer schedulerTimer = new Timer();
|
||||
|
||||
public BackupManager(File backupDirectory) {
|
||||
this.backupDirectory = backupDirectory;
|
||||
persist = new Persist(this);
|
||||
|
||||
|
||||
long nextMinute = ZonedDateTime.now().plusMinutes(1).withSecond(0).withNano(0)
|
||||
.toInstant().toEpochMilli();
|
||||
schedulerTimer.scheduleAtFixedRate(this, new Date(nextMinute), 60_000);
|
||||
}
|
||||
|
||||
|
||||
protected void addProcess(BackupProcess process) {
|
||||
process.displayNextSchedule();
|
||||
backupQueue.add(process);
|
||||
}
|
||||
|
||||
|
||||
public File getBackupDirectory() {
|
||||
return backupDirectory;
|
||||
}
|
||||
|
||||
public synchronized void run() {
|
||||
BackupProcess tmp;
|
||||
if ((tmp = runningBackup.get()) != null) {
|
||||
tmp.logProgress();
|
||||
}
|
||||
else {
|
||||
backupQueue.sort(null);
|
||||
for (BackupProcess process : backupQueue) {
|
||||
if (System.currentTimeMillis() >= process.getNext() && process.couldRunNow()) {
|
||||
process.run();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public synchronized void onDisable() {
|
||||
|
||||
schedulerTimer.cancel();
|
||||
|
||||
if (runningBackup.get() != null) {
|
||||
Log.warning("[Backup] Waiting after the end of a backup...");
|
||||
BackupProcess tmp;
|
||||
while ((tmp = runningBackup.get()) != null) {
|
||||
try {
|
||||
tmp.logProgress();
|
||||
// wait 5 seconds between each progress log
|
||||
// but check if the process has ended each .5 seconds
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (runningBackup.get() == null)
|
||||
break;
|
||||
Thread.sleep(500);
|
||||
}
|
||||
} catch (Throwable e) { // could occur because of synchronization errors/interruption/...
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persist.save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,270 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import fc.cron.CronExpression;
|
||||
import fr.pandacube.lib.util.FileUtils;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
public abstract class BackupProcess implements Comparable<BackupProcess>, Runnable {
|
||||
private final BackupManager backupManager;
|
||||
|
||||
public final String identifier;
|
||||
|
||||
|
||||
protected ZipCompressor compressor = null;
|
||||
|
||||
|
||||
private boolean enabled = true;
|
||||
private String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
|
||||
private BackupCleaner backupCleaner = null;
|
||||
private List<String> ignoreList = new ArrayList<>();
|
||||
|
||||
|
||||
protected BackupProcess(BackupManager bm, final String n) {
|
||||
backupManager = bm;
|
||||
identifier = n;
|
||||
}
|
||||
|
||||
public BackupManager getBackupManager() {
|
||||
return backupManager;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
protected String getDisplayName() {
|
||||
return getIdentifier();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(final BackupProcess process) {
|
||||
return Long.compare(getNext(), process.getNext());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public abstract BiPredicate<File, String> getFilenameFilter();
|
||||
|
||||
public abstract File getSourceDir();
|
||||
|
||||
protected abstract File getTargetDir();
|
||||
|
||||
protected abstract void onBackupStart();
|
||||
|
||||
protected abstract void onBackupEnd(boolean success);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getScheduling() {
|
||||
return scheduling;
|
||||
}
|
||||
|
||||
public void setScheduling(String scheduling) {
|
||||
this.scheduling = scheduling;
|
||||
}
|
||||
|
||||
public BackupCleaner getBackupCleaner() {
|
||||
return backupCleaner;
|
||||
}
|
||||
|
||||
public void setBackupCleaner(BackupCleaner backupCleaner) {
|
||||
this.backupCleaner = backupCleaner;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
getBackupManager().runningBackup.set(this);
|
||||
|
||||
try {
|
||||
BiPredicate<File, String> filter = getFilenameFilter();
|
||||
File sourceDir = getSourceDir();
|
||||
|
||||
if (!sourceDir.exists()) {
|
||||
Log.warning("[Backup] Unable to compress " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": source directory " + sourceDir + " doesn’t exist");
|
||||
return;
|
||||
}
|
||||
|
||||
File targetDir = getTargetDir();
|
||||
File target = new File(targetDir, getDateFileName() + ".zip");
|
||||
|
||||
onBackupStart();
|
||||
|
||||
new Thread(() -> {
|
||||
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
|
||||
|
||||
compressor = new ZipCompressor(sourceDir, target, 9, filter);
|
||||
|
||||
boolean success = false;
|
||||
try {
|
||||
compressor.compress();
|
||||
|
||||
success = true;
|
||||
|
||||
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
|
||||
|
||||
try {
|
||||
BackupCleaner cleaner = getBackupCleaner();
|
||||
if (cleaner != null)
|
||||
cleaner.cleanupArchives(targetDir, getDisplayName());
|
||||
} catch (Exception e) {
|
||||
Log.severe(e);
|
||||
}
|
||||
}
|
||||
catch (final Exception e) {
|
||||
Log.severe("[Backup] Failed: " + sourceDir + " -> " + target, e);
|
||||
|
||||
FileUtils.delete(target);
|
||||
if (target.exists())
|
||||
Log.warning("unable to delete: " + target);
|
||||
} finally {
|
||||
|
||||
getBackupManager().runningBackup.set(null);
|
||||
|
||||
onBackupEnd(success);
|
||||
|
||||
displayNextSchedule();
|
||||
|
||||
}
|
||||
}, "Backup Thread " + identifier).start();
|
||||
} catch (Throwable t) {
|
||||
getBackupManager().runningBackup.set(null);
|
||||
throw t;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public abstract void displayNextSchedule();
|
||||
|
||||
|
||||
public static final DateTimeFormatter dateFileNameFormatter = new DateTimeFormatterBuilder()
|
||||
.appendValue(ChronoField.YEAR, 4)
|
||||
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
|
||||
.appendValue(ChronoField.DAY_OF_MONTH, 2)
|
||||
.appendLiteral('-')
|
||||
.appendValue(ChronoField.HOUR_OF_DAY, 2)
|
||||
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
|
||||
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
|
||||
.toFormatter();
|
||||
|
||||
|
||||
private String getDateFileName() {
|
||||
return dateFileNameFormatter.format(ZonedDateTime.now());
|
||||
}
|
||||
|
||||
|
||||
public void logProgress() {
|
||||
if (compressor == null)
|
||||
return;
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public boolean couldRunNow() {
|
||||
if (!isEnabled())
|
||||
return false;
|
||||
if (!isDirty())
|
||||
return false;
|
||||
if (getNext() > System.currentTimeMillis())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public long getNext() {
|
||||
if (!hasNextScheduled())
|
||||
return Long.MAX_VALUE;
|
||||
return getNextCompress(backupManager.persist.isDirtySince(identifier));
|
||||
}
|
||||
|
||||
public boolean hasNextScheduled() {
|
||||
return isEnabled() && isDirty();
|
||||
}
|
||||
|
||||
public boolean isDirty() {
|
||||
return backupManager.persist.isDirty(identifier);
|
||||
}
|
||||
|
||||
public void setDirtySinceNow() {
|
||||
backupManager.persist.setDirtySinceNow(identifier);
|
||||
}
|
||||
|
||||
public void setNotDirty() {
|
||||
backupManager.persist.setNotDirty(identifier);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* get the timestamp (in ms) of when the next compress will run, depending on since when the files to compress are dirty.
|
||||
* @param dirtySince the timestamp in ms since the files are dirty
|
||||
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled
|
||||
*/
|
||||
public long getNextCompress(long dirtySince) {
|
||||
if (dirtySince == -1)
|
||||
return 0;
|
||||
|
||||
CronExpression parsedScheduling;
|
||||
try {
|
||||
parsedScheduling = new CronExpression(getScheduling(), false);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.severe("Invalid backup scheduling configuration '" + getScheduling() + "'.", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parsedScheduling.nextTimeAfter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(dirtySince), ZoneId.systemDefault()))
|
||||
.toInstant()
|
||||
.toEpochMilli();
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import fr.pandacube.lib.core.json.Json;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Persist {
|
||||
protected final BackupManager backupManager;
|
||||
|
||||
private Map<String, Long> dirtySince = new HashMap<>();
|
||||
|
||||
private final File file;
|
||||
|
||||
// private final Set<String> dirtyWorldsSave = new HashSet<>();
|
||||
|
||||
public Persist(BackupManager bm) {
|
||||
backupManager = bm;
|
||||
file = new File(bm.getBackupDirectory(), "source-dirty-since.yml");
|
||||
load();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
load();
|
||||
}
|
||||
|
||||
protected void load() {
|
||||
boolean loaded = false;
|
||||
try {
|
||||
dirtySince = Json.gson.fromJson(new FileReader(file), new TypeToken<Map<String, Long>>(){}.getType());
|
||||
loaded = true;
|
||||
}
|
||||
catch (final FileNotFoundException ignored) { }
|
||||
catch (final JsonParseException e) {
|
||||
Log.severe("cannot load " + file, e);
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
Json.gsonPrettyPrinting.toJson(dirtySince, new FileWriter(file, false));
|
||||
}
|
||||
catch (final FileNotFoundException ignored) { }
|
||||
catch (final JsonParseException | IOException e) {
|
||||
Log.severe("could not save " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void setDirtySinceNow(String id) {
|
||||
dirtySince.put(id, System.currentTimeMillis());
|
||||
save();
|
||||
}
|
||||
|
||||
public void setNotDirty(String id) {
|
||||
dirtySince.put(id, -1L);
|
||||
save();
|
||||
}
|
||||
|
||||
|
||||
public boolean isDirty(String id) {
|
||||
return isDirtySince(id) != -1;
|
||||
}
|
||||
|
||||
public long isDirtySince(String id) {
|
||||
if (!dirtySince.containsKey(id))
|
||||
setDirtySinceNow(id);
|
||||
return dirtySince.get(id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
public class RotatedLogsBackupProcess extends BackupProcess {
|
||||
final String logFileRegexPattern;
|
||||
final File sourceLogDirectory;
|
||||
final boolean inNewThread;
|
||||
|
||||
public RotatedLogsBackupProcess(BackupManager bm, boolean inNewThread, File sourceLogDir, String logFileRegexPattern) {
|
||||
super(bm, "logs");
|
||||
this.logFileRegexPattern = logFileRegexPattern;
|
||||
sourceLogDirectory = sourceLogDir;
|
||||
this.inNewThread = inNewThread;
|
||||
super.setScheduling("0 1 * * *"); // do this every day at 1 am, by default
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// do not call super. We override the zip archive process, we just want to copy log files, here
|
||||
if (inNewThread) {
|
||||
new Thread(this::actuallyRun, "Backup Thread " + identifier).start();
|
||||
}
|
||||
else {
|
||||
actuallyRun();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void actuallyRun() {
|
||||
|
||||
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
|
||||
|
||||
try {
|
||||
// wait a little after the log message above, in case the log file rotation has to be performed.
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
onBackupStart();
|
||||
|
||||
boolean success = false;
|
||||
|
||||
File targetDir = getTargetDir();
|
||||
|
||||
try {
|
||||
List<File> filesToMove = getFilesToMove();
|
||||
|
||||
for (File source : filesToMove) {
|
||||
try {
|
||||
Files.move(source, new File(targetDir, source.getName()));
|
||||
} catch (IOException e) {
|
||||
Log.severe("Unable to move file " + source + " into " + targetDir);
|
||||
}
|
||||
}
|
||||
|
||||
success = true;
|
||||
|
||||
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
|
||||
} catch (final Exception e) {
|
||||
Log.severe("[Backup] Failed for : " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET, e);
|
||||
} finally {
|
||||
onBackupEnd(success);
|
||||
|
||||
displayNextSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public List<File> getFilesToMove() {
|
||||
List<File> ret = new ArrayList<>();
|
||||
for (File f : getSourceDir().listFiles()) {
|
||||
if (f.getName().matches(logFileRegexPattern))
|
||||
ret.add(f);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public BiPredicate<File, String> getFilenameFilter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getSourceDir() {
|
||||
return sourceLogDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected File getTargetDir() {
|
||||
return new File(getBackupManager().getBackupDirectory(), "logs");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBackupStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBackupEnd(boolean success) {
|
||||
setDirtySinceNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayNextSchedule() {
|
||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
|
||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||
}
|
||||
}
|
@@ -0,0 +1,194 @@
|
||||
package fr.pandacube.lib.core.backup;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import fr.pandacube.lib.chat.Chat;
|
||||
import fr.pandacube.lib.util.MemoryUtil;
|
||||
import fr.pandacube.lib.util.TimeUtil;
|
||||
|
||||
/**
|
||||
* Handles the creation of a zip file that will have the content of a provided folder.
|
||||
*/
|
||||
public class ZipCompressor {
|
||||
private static final int BUFFER_SIZE = 16 * 1024;
|
||||
|
||||
|
||||
|
||||
private final File srcDir, destFile;
|
||||
private final int compressionLevel;
|
||||
private final BiPredicate<File, String> filter;
|
||||
|
||||
private final List<Entry> entriesToCompress;
|
||||
private ZipOutputStream zipOutStream;
|
||||
|
||||
private final Object stateLock = new Object();
|
||||
private final long inputByteSize;
|
||||
private long startTime;
|
||||
private long elapsedByte = 0;
|
||||
private Exception exception = null;
|
||||
private boolean started = false;
|
||||
private boolean finished = false;
|
||||
|
||||
/**
|
||||
* Creates a new zip compressor.
|
||||
* @param s the source directory.
|
||||
* @param d the destination file.
|
||||
* @param c the compression level, used in {@link ZipOutputStream#setLevel(int)} .
|
||||
* @param f a filter that returns true for the files to include in the zip file, false to exclude.
|
||||
*/
|
||||
public ZipCompressor(File s, File d, int c, BiPredicate<File, String> f) {
|
||||
srcDir = s;
|
||||
destFile = d;
|
||||
compressionLevel = c;
|
||||
filter = f;
|
||||
|
||||
entriesToCompress = new ArrayList<>();
|
||||
inputByteSize = addEntry("");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a displayable representation of the running compression.
|
||||
* @return a displayable representation of the running compression.
|
||||
*/
|
||||
public Chat getState() {
|
||||
synchronized (stateLock) {
|
||||
if (!started) {
|
||||
return Chat.text("Démarrage...");
|
||||
}
|
||||
else if (!finished && exception == null) {
|
||||
float progress = getProgress();
|
||||
long elapsedTime = System.nanoTime() - startTime;
|
||||
long remainingTime = (long)(elapsedTime / progress) - elapsedTime;
|
||||
return Chat.chat()
|
||||
.infoColor()
|
||||
.thenData(Math.round(progress*100*10)/10 + "% ")
|
||||
.thenText("(")
|
||||
.thenData(MemoryUtil.humanReadableSize(elapsedByte) + "/" + MemoryUtil.humanReadableSize(inputByteSize))
|
||||
.thenText(") - Temps restant estimé : ")
|
||||
.thenData(TimeUtil.durationToString(remainingTime / 1000000));
|
||||
}
|
||||
else if (exception != null) {
|
||||
return Chat.failureText("Erreur lors de l'archivage (voir console pour les détails)");
|
||||
}
|
||||
else { // finished
|
||||
return Chat.successText("Terminé !");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the progress of the running compression.
|
||||
* @return the progress of the running compression 0 when it starts and 1 when it finishes.
|
||||
*/
|
||||
public float getProgress() {
|
||||
if (!started)
|
||||
return 0;
|
||||
if (finished)
|
||||
return 1;
|
||||
return elapsedByte / (float) inputByteSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the compression on the current thread, and returns after the end of the compression.
|
||||
* Should be run asynchronously (not on Server Thread).
|
||||
* @throws Exception if an error occurs during compression.
|
||||
*/
|
||||
public void compress() throws Exception {
|
||||
destFile.getParentFile().mkdirs();
|
||||
|
||||
try(ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), BUFFER_SIZE))) {
|
||||
zipOutStream = zipStream;
|
||||
zipOutStream.setLevel(compressionLevel);
|
||||
|
||||
synchronized (stateLock) {
|
||||
startTime = System.nanoTime();
|
||||
started = true;
|
||||
}
|
||||
|
||||
for (Entry entry : entriesToCompress) {
|
||||
entry.zip();
|
||||
}
|
||||
|
||||
synchronized (stateLock) {
|
||||
finished = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
synchronized (stateLock) {
|
||||
exception = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private long addEntry(String currentEntry) {
|
||||
final File currentFile = new File(srcDir, currentEntry);
|
||||
if (!currentFile.exists())
|
||||
return 0;
|
||||
if (currentFile.isDirectory()) {
|
||||
if (!currentEntry.isEmpty()) { // it's not the zip root directory
|
||||
currentEntry += "/";
|
||||
entriesToCompress.add(new Entry(currentFile, currentEntry));
|
||||
}
|
||||
|
||||
long sum = 0;
|
||||
for (String child : currentFile.list()) {
|
||||
String childEntry = currentEntry + child;
|
||||
if (filter.test(new File(currentFile, child), childEntry))
|
||||
sum += addEntry(childEntry);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
else { // is a file
|
||||
entriesToCompress.add(new Entry(currentFile, currentEntry));
|
||||
return currentFile.length();
|
||||
}
|
||||
}
|
||||
|
||||
private class Entry {
|
||||
File file;
|
||||
String entry;
|
||||
Entry(File f, String e) {
|
||||
file = f;
|
||||
entry = e;
|
||||
}
|
||||
|
||||
void zip() throws IOException {
|
||||
ZipEntry zipEntry = new ZipEntry(entry);
|
||||
BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
|
||||
if (attributes.isDirectory()) {
|
||||
zipOutStream.putNextEntry(zipEntry);
|
||||
zipOutStream.closeEntry();
|
||||
}
|
||||
else {
|
||||
|
||||
zipEntry.setTime(attributes.lastModifiedTime().toMillis());
|
||||
zipOutStream.putNextEntry(zipEntry);
|
||||
|
||||
try {
|
||||
Files.copy(file.toPath(), zipOutStream);
|
||||
}
|
||||
finally {
|
||||
zipOutStream.closeEntry();
|
||||
}
|
||||
|
||||
synchronized (stateLock) {
|
||||
elapsedByte += attributes.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user