Made backup manager more generic
This commit is contained in:
parent
9818bca757
commit
52467dc556
@ -36,5 +36,12 @@
|
|||||||
<artifactId>pandalib-chat</artifactId>
|
<artifactId>pandalib-chat</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Cron expression interpreter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.eitchnet</groupId>
|
||||||
|
<artifactId>cron</artifactId>
|
||||||
|
<version>1.6.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
package fr.pandacube.lib.core.backup;
|
||||||
|
|
||||||
import fr.pandacube.lib.chat.Chat;
|
import fr.pandacube.lib.chat.Chat;
|
||||||
import fr.pandacube.lib.util.Log;
|
import fr.pandacube.lib.util.Log;
|
||||||
import org.bukkit.ChatColor;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -89,7 +89,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
|
|||||||
String dateTimeStr = filename.substring(0, filename.length() - 4);
|
String dateTimeStr = filename.substring(0, filename.length() - 4);
|
||||||
LocalDateTime ldt;
|
LocalDateTime ldt;
|
||||||
try {
|
try {
|
||||||
ldt = LocalDateTime.parse(dateTimeStr, CompressProcess.dateFileNameFormatter);
|
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
|
||||||
} catch (DateTimeParseException e) {
|
} catch (DateTimeParseException e) {
|
||||||
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
|
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
|
||||||
continue;
|
continue;
|
@ -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())));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
package fr.pandacube.lib.core.backup;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
@ -59,6 +59,12 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<artifactId>pandalib-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Paper -->
|
<!-- Paper -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.papermc.paper</groupId>
|
<groupId>io.papermc.paper</groupId>
|
||||||
@ -70,13 +76,6 @@
|
|||||||
<artifactId>paper-mojangapi</artifactId>
|
<artifactId>paper-mojangapi</artifactId>
|
||||||
<version>${paper.version}-SNAPSHOT</version>
|
<version>${paper.version}-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Cron expression interpreter -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>ch.eitchnet</groupId>
|
|
||||||
<artifactId>cron</artifactId>
|
|
||||||
<version>1.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
@ -1,4 +1,6 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
package fr.pandacube.lib.paper.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.core.backup.BackupCleaner;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -7,7 +9,8 @@ import java.util.List;
|
|||||||
public class BackupConfig {
|
public class BackupConfig {
|
||||||
public boolean worldBackupEnabled = true;
|
public boolean worldBackupEnabled = true;
|
||||||
public boolean workdirBackupEnabled = true;
|
public boolean workdirBackupEnabled = true;
|
||||||
public String scheduling = "0 2 * * 1"; // cron format, here is everyday at 2am
|
public boolean logsBackupEnabled = true;
|
||||||
|
public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
|
||||||
public File backupDirectory = null;
|
public File backupDirectory = null;
|
||||||
public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||||
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
@ -0,0 +1,156 @@
|
|||||||
|
package fr.pandacube.lib.paper.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.core.backup.BackupManager;
|
||||||
|
import fr.pandacube.lib.core.backup.BackupProcess;
|
||||||
|
import fr.pandacube.lib.core.backup.RotatedLogsBackupProcess;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.event.world.WorldLoadEvent;
|
||||||
|
import org.bukkit.event.world.WorldSaveEvent;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class PaperBackupManager extends BackupManager implements Listener {
|
||||||
|
|
||||||
|
private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>();
|
||||||
|
|
||||||
|
BackupConfig config;
|
||||||
|
|
||||||
|
public PaperBackupManager(BackupConfig config) {
|
||||||
|
super(config.backupDirectory);
|
||||||
|
setConfig(config);
|
||||||
|
|
||||||
|
|
||||||
|
for (final World world : Bukkit.getWorlds()) {
|
||||||
|
initWorldProcess(world.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
initWorkdirProcess();
|
||||||
|
|
||||||
|
addProcess(new RotatedLogsBackupProcess(this, true, new File("logs"), "[0-9]{4}-[0-9]{2}-[0-9]{2}(-[0-9]+)?\\.log\\.gz"));
|
||||||
|
|
||||||
|
Bukkit.getServer().getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(BackupConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
for (BackupProcess process : backupQueue) {
|
||||||
|
if (process instanceof PaperWorkdirProcess) {
|
||||||
|
process.setEnabled(config.workdirBackupEnabled);
|
||||||
|
process.setBackupCleaner(config.workdirBackupCleaner);
|
||||||
|
process.setScheduling(config.scheduling);
|
||||||
|
}
|
||||||
|
else if (process instanceof PaperWorldProcess) {
|
||||||
|
process.setEnabled(config.worldBackupEnabled);
|
||||||
|
process.setBackupCleaner(config.worldBackupCleaner);
|
||||||
|
process.setScheduling(config.scheduling);
|
||||||
|
}
|
||||||
|
else if (process instanceof RotatedLogsBackupProcess) {
|
||||||
|
process.setEnabled(config.logsBackupEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
SchedulerUtil.runOnServerThreadAndWait(super::run);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDisable() {
|
||||||
|
|
||||||
|
// save dirty status of worlds
|
||||||
|
for (String wName : dirtyForSave) {
|
||||||
|
World w = Bukkit.getWorld(wName);
|
||||||
|
if (w != null)
|
||||||
|
compressWorlds.get(w.getName()).setDirtyAfterSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDisable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initWorldProcess(final String worldName) {
|
||||||
|
if (compressWorlds.containsKey(worldName))
|
||||||
|
return;
|
||||||
|
PaperWorldProcess process = new PaperWorldProcess(this, worldName);
|
||||||
|
process.setEnabled(config.worldBackupEnabled);
|
||||||
|
process.setBackupCleaner(config.worldBackupCleaner);
|
||||||
|
process.setScheduling(config.scheduling);
|
||||||
|
addProcess(process);
|
||||||
|
compressWorlds.put(worldName, process);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initWorkdirProcess() {
|
||||||
|
PaperWorkdirProcess process = new PaperWorkdirProcess(this);
|
||||||
|
process.setEnabled(config.workdirBackupEnabled);
|
||||||
|
process.setBackupCleaner(config.workdirBackupCleaner);
|
||||||
|
process.setScheduling(config.scheduling);
|
||||||
|
addProcess(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final Set<String> dirtyForSave = new HashSet<>();
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onWorldLoad(WorldLoadEvent event) {
|
||||||
|
initWorldProcess(event.getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onWorldSave(WorldSaveEvent event) {
|
||||||
|
if (event.getWorld().getLoadedChunks().length > 0
|
||||||
|
|| dirtyForSave.contains(event.getWorld().getName())) {
|
||||||
|
compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave();
|
||||||
|
dirtyForSave.remove(event.getWorld().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
|
||||||
|
dirtyForSave.add(event.getFrom().getName());
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package fr.pandacube.lib.paper.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.chat.Chat;
|
||||||
|
import fr.pandacube.lib.core.backup.BackupProcess;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
||||||
|
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Color;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
public abstract class PaperBackupProcess extends BackupProcess {
|
||||||
|
|
||||||
|
|
||||||
|
private BossBar bossBar;
|
||||||
|
|
||||||
|
protected PaperBackupProcess(PaperBackupManager bm, String id) {
|
||||||
|
super(bm, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaperBackupManager getBackupManager() {
|
||||||
|
return (PaperBackupManager) super.getBackupManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackupStart() {
|
||||||
|
bossBar = BossBar.bossBar(Chat.text("Archivage"), 0, Color.YELLOW, Overlay.NOTCHED_20);
|
||||||
|
AutoUpdatedBossBar auBossBar = new AutoUpdatedBossBar(bossBar, (bar) -> {
|
||||||
|
bar.setTitle(Chat.infoText("Archivage ")
|
||||||
|
.thenData(getDisplayName())
|
||||||
|
.thenText(" : ")
|
||||||
|
.then(compressor == null
|
||||||
|
? Chat.text("Démarrage...")
|
||||||
|
: compressor.getState()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
bar.setProgress(compressor == null ? 0 : compressor.getProgress());
|
||||||
|
});
|
||||||
|
auBossBar.scheduleUpdateTimeSyncThreadAsync(100, 100);
|
||||||
|
PerformanceAnalysisManager.getInstance().addBossBar(bossBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackupEnd(boolean success) {
|
||||||
|
Bukkit.getScheduler().runTaskLater(PandaLibPaper.getPlugin(), () -> {
|
||||||
|
PerformanceAnalysisManager.getInstance().removeBossBar(bossBar);
|
||||||
|
bossBar = null;
|
||||||
|
}, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package fr.pandacube.lib.paper.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public class PaperWorkdirProcess extends PaperBackupProcess {
|
||||||
|
|
||||||
|
protected PaperWorkdirProcess(PaperBackupManager bm) {
|
||||||
|
super(bm, "workdir");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BiPredicate<File, String> getFilenameFilter() {
|
||||||
|
return new BiPredicate<File, String>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(File file, String path) {
|
||||||
|
if (globalExcluded(file, path))
|
||||||
|
return false;
|
||||||
|
for (String exclude : getBackupManager().config.workdirIgnoreList) {
|
||||||
|
if (exclude.startsWith("/")) { // relative to source of workdir
|
||||||
|
if (path.matches(exclude.substring(1)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
String name = path.substring(path.lastIndexOf("/") + 1);
|
||||||
|
if (name.matches(exclude))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean globalExcluded(File file, String path) {
|
||||||
|
if (file.isDirectory() && new File(file, "level.dat").exists())
|
||||||
|
return true;
|
||||||
|
if (new File(getSourceDir(), "logs").equals(file))
|
||||||
|
return true;
|
||||||
|
if (file.isFile() && file.getName().endsWith(".lck"))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getSourceDir() {
|
||||||
|
return new File(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackupEnd(boolean success) {
|
||||||
|
if (success)
|
||||||
|
setDirtySinceNow();
|
||||||
|
super.onBackupEnd(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected File getTargetDir() {
|
||||||
|
return new File(getBackupManager().getBackupDirectory(), "workdir");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getDisplayName() {
|
||||||
|
return "workdir";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void displayNextSchedule() {
|
||||||
|
Log.info("[Backup] " + net.md_5.bungee.api.ChatColor.GRAY + getDisplayName() + net.md_5.bungee.api.ChatColor.RESET + " next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package fr.pandacube.lib.paper.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
|
||||||
|
import fr.pandacube.lib.paper.util.WorldUtil;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public class PaperWorldProcess extends PaperBackupProcess {
|
||||||
|
private final String worldName;
|
||||||
|
|
||||||
|
private boolean autoSave = true;
|
||||||
|
|
||||||
|
protected PaperWorldProcess(PaperBackupManager bm, final String n) {
|
||||||
|
super(bm, "worlds/" + n);
|
||||||
|
worldName = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private World getWorld() {
|
||||||
|
return Bukkit.getWorld(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BiPredicate<File, String> getFilenameFilter() {
|
||||||
|
return (f, s) -> true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getSourceDir() {
|
||||||
|
return WorldUtil.worldDir(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackupStart() {
|
||||||
|
World w = getWorld();
|
||||||
|
if (w == null)
|
||||||
|
return;
|
||||||
|
autoSave = w.isAutoSave();
|
||||||
|
w.setAutoSave(false);
|
||||||
|
super.onBackupStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackupEnd(boolean success) {
|
||||||
|
if (success)
|
||||||
|
setNotDirty();
|
||||||
|
SchedulerUtil.runOnServerThread(() -> {
|
||||||
|
World w = getWorld();
|
||||||
|
if (w == null)
|
||||||
|
return;
|
||||||
|
w.setAutoSave(autoSave);
|
||||||
|
});
|
||||||
|
super.onBackupEnd(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected File getTargetDir() {
|
||||||
|
return new File(getBackupManager().getBackupDirectory(), "worlds/" + worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void displayNextSchedule() {
|
||||||
|
if (hasNextScheduled()) {
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is dirty. Next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the specified world dirty for compress. Also makes the specified world clean for saving if nobody is connected there.
|
||||||
|
*/
|
||||||
|
public void setDirtyAfterSave() {
|
||||||
|
if (!isDirty()) { // don't set dirty if it is already
|
||||||
|
setDirtySinceNow();
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " was saved and is now dirty. Next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
|
||||||
|
.format(new Date(getNext()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,196 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
import fc.cron.CronExpression;
|
|
||||||
import fr.pandacube.lib.paper.PandaLibPaper;
|
|
||||||
import fr.pandacube.lib.util.Log;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.World;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.EventPriority;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
|
||||||
import org.bukkit.event.world.WorldLoadEvent;
|
|
||||||
import org.bukkit.event.world.WorldSaveEvent;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
public class BackupManager implements Runnable, Listener {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Persist persist;
|
|
||||||
|
|
||||||
private final List<CompressProcess> compressQueue = new ArrayList<>();
|
|
||||||
|
|
||||||
private final Set<String> compressWorlds = new HashSet<>();
|
|
||||||
|
|
||||||
/* package */ AtomicReference<CompressProcess> compressRunning = new AtomicReference<>();
|
|
||||||
|
|
||||||
private final Set<String> dirtyForSave = new HashSet<>();
|
|
||||||
|
|
||||||
BackupConfig config;
|
|
||||||
|
|
||||||
public BackupManager(BackupConfig config) {
|
|
||||||
setConfig(config);
|
|
||||||
persist = new Persist(this);
|
|
||||||
|
|
||||||
|
|
||||||
for (final World world : Bukkit.getWorlds()) {
|
|
||||||
initCompressProcess(Type.WORLDS, world.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
initCompressProcess(Type.WORKDIR, null);
|
|
||||||
|
|
||||||
Bukkit.getServer().getScheduler().runTaskTimer(PandaLibPaper.getPlugin(), this, (60 - Calendar.getInstance().get(Calendar.SECOND)) * 20L, 60 * 20L);
|
|
||||||
|
|
||||||
Bukkit.getServer().getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConfig(BackupConfig config) {
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void onDisable() {
|
|
||||||
|
|
||||||
if (compressRunning.get() != null) {
|
|
||||||
Log.warning("[Backup] Waiting after the end of a backup...");
|
|
||||||
CompressProcess tmp;
|
|
||||||
while ((tmp = compressRunning.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 (compressRunning.get() == null)
|
|
||||||
break;
|
|
||||||
Thread.sleep(500);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) { // could occur because of synchronization errors/interruption/...
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save dirty status of worlds
|
|
||||||
for (String wName : dirtyForSave) {
|
|
||||||
World w = Bukkit.getWorld(wName);
|
|
||||||
if (w != null)
|
|
||||||
persist.updateDirtyStatusAfterSave(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
persist.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCompressProcess(final Type type, final String worldName) {
|
|
||||||
if (!type.backupEnabled(config))
|
|
||||||
return;
|
|
||||||
if (type == Type.WORLDS) {
|
|
||||||
if (compressWorlds.contains(worldName))
|
|
||||||
return;
|
|
||||||
compressWorlds.add(worldName);
|
|
||||||
}
|
|
||||||
CompressProcess process = type == Type.WORLDS ? new CompressWorldProcess(this, worldName) : new CompressWorkdirProcess(this);
|
|
||||||
process.displayDirtynessStatus();
|
|
||||||
compressQueue.add(process);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
CompressProcess tmp;
|
|
||||||
if ((tmp = compressRunning.get()) != null) {
|
|
||||||
tmp.logProgress();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
compressQueue.sort(null);
|
|
||||||
for (CompressProcess process : compressQueue) {
|
|
||||||
if (System.currentTimeMillis() >= process.getNext() && process.couldRunNow()) {
|
|
||||||
process.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
/* package */ long getNextCompress(long dirtySince) {
|
|
||||||
if (dirtySince == -1)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
CronExpression parsedScheduling;
|
|
||||||
try {
|
|
||||||
parsedScheduling = new CronExpression(config.scheduling, false);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Log.severe("Invalid backup scheduling configuration '" + config.scheduling + "'.", e);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedScheduling.nextTimeAfter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(dirtySince), ZoneId.systemDefault()))
|
|
||||||
.toInstant()
|
|
||||||
.toEpochMilli();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onWorldLoad(WorldLoadEvent event) {
|
|
||||||
initCompressProcess(Type.WORLDS, event.getWorld().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onWorldSave(WorldSaveEvent event) {
|
|
||||||
if (event.getWorld().getLoadedChunks().length > 0
|
|
||||||
|| dirtyForSave.contains(event.getWorld().getName())) {
|
|
||||||
persist.updateDirtyStatusAfterSave(event.getWorld());
|
|
||||||
dirtyForSave.remove(event.getWorld().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
|
|
||||||
dirtyForSave.add(event.getFrom().getName());
|
|
||||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
||||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
|
||||||
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
import fr.pandacube.lib.chat.Chat;
|
|
||||||
import fr.pandacube.lib.paper.PandaLibPaper;
|
|
||||||
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
|
||||||
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar;
|
|
||||||
import fr.pandacube.lib.util.FileUtils;
|
|
||||||
import fr.pandacube.lib.util.Log;
|
|
||||||
import net.kyori.adventure.bossbar.BossBar;
|
|
||||||
import net.kyori.adventure.bossbar.BossBar.Color;
|
|
||||||
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.ChatColor;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeFormatterBuilder;
|
|
||||||
import java.time.temporal.ChronoField;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
|
|
||||||
public abstract class CompressProcess implements Comparable<CompressProcess>, Runnable {
|
|
||||||
protected final BackupManager backupManager;
|
|
||||||
public final Type type;
|
|
||||||
public final String name;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private ZipCompressor compressor = null;
|
|
||||||
|
|
||||||
protected CompressProcess(BackupManager bm, final Type t, final String n) {
|
|
||||||
backupManager = bm;
|
|
||||||
type = t;
|
|
||||||
name = n;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(final CompressProcess process) {
|
|
||||||
return Long.compare(getNext(), process.getNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public abstract BiPredicate<File, String> getFilenameFilter();
|
|
||||||
|
|
||||||
public abstract File getSourceDir();
|
|
||||||
|
|
||||||
protected abstract void onCompressStart();
|
|
||||||
|
|
||||||
protected abstract void onCompressEnd(boolean success);
|
|
||||||
|
|
||||||
protected abstract File getTargetDir();
|
|
||||||
|
|
||||||
protected abstract String getDisplayName();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
backupManager.compressRunning.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");
|
|
||||||
|
|
||||||
|
|
||||||
BossBar bossBar = BossBar.bossBar(Chat.text("Archivage"), 0, Color.YELLOW, Overlay.NOTCHED_20);
|
|
||||||
AutoUpdatedBossBar auBossBar = new AutoUpdatedBossBar(bossBar, (bar) -> {
|
|
||||||
bar.setTitle(Chat.infoText("Archivage ")
|
|
||||||
.thenData(getDisplayName())
|
|
||||||
.thenText(" : ")
|
|
||||||
.then(compressor == null
|
|
||||||
? Chat.text("Démarrage...")
|
|
||||||
: compressor.getState()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
bar.setProgress(compressor == null ? 0 : compressor.getProgress());
|
|
||||||
});
|
|
||||||
auBossBar.scheduleUpdateTimeSyncThreadAsync(100, 100);
|
|
||||||
|
|
||||||
onCompressStart();
|
|
||||||
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(PandaLibPaper.getPlugin(), () -> {
|
|
||||||
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
|
|
||||||
|
|
||||||
compressor = new ZipCompressor(sourceDir, target, 9, filter);
|
|
||||||
|
|
||||||
PerformanceAnalysisManager.getInstance().addBossBar(bossBar);
|
|
||||||
|
|
||||||
boolean success = false;
|
|
||||||
try {
|
|
||||||
compressor.compress();
|
|
||||||
|
|
||||||
success = true;
|
|
||||||
|
|
||||||
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
|
|
||||||
|
|
||||||
backupManager.persist.updateDirtyStatusAfterCompress(type, name);
|
|
||||||
|
|
||||||
displayDirtynessStatus();
|
|
||||||
|
|
||||||
try {
|
|
||||||
type.backupCleaner(backupManager.config).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 {
|
|
||||||
|
|
||||||
backupManager.compressRunning.set(null);
|
|
||||||
boolean successF = success;
|
|
||||||
Bukkit.getScheduler().runTask(PandaLibPaper.getPlugin(), () -> onCompressEnd(successF));
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(2000);
|
|
||||||
} catch(InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
PerformanceAnalysisManager.getInstance().removeBossBar(bossBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Throwable t) {
|
|
||||||
backupManager.compressRunning.set(null);
|
|
||||||
throw t;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void displayDirtynessStatus() {
|
|
||||||
if (hasNextScheduled() && type == Type.WORLDS) {
|
|
||||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is dirty. Next backup on "
|
|
||||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
|
||||||
}
|
|
||||||
else if (hasNextScheduled()) {
|
|
||||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
|
|
||||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static 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 (!type.backupEnabled(backupManager.config))
|
|
||||||
return false;
|
|
||||||
if (!backupManager.persist.isDirty(type, name))
|
|
||||||
return false;
|
|
||||||
if (getNext() > System.currentTimeMillis())
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public long getNext() {
|
|
||||||
if (!hasNextScheduled())
|
|
||||||
return Long.MAX_VALUE;
|
|
||||||
return backupManager.getNextCompress(backupManager.persist.isDirtySince(type, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasNextScheduled() {
|
|
||||||
return backupManager.persist.isDirty(type, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
|
|
||||||
public class CompressWorkdirProcess extends CompressProcess {
|
|
||||||
|
|
||||||
protected CompressWorkdirProcess(BackupManager bm) {
|
|
||||||
super(bm, Type.WORKDIR, Type.WORKDIR.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public BiPredicate<File, String> getFilenameFilter() {
|
|
||||||
return new SourceFileFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private class SourceFileFilter implements BiPredicate<File, String> {
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean test(File file, String path) {
|
|
||||||
if (globalExcluded(file, path))
|
|
||||||
return false;
|
|
||||||
for (String exclude : backupManager.config.workdirIgnoreList) {
|
|
||||||
if (exclude.startsWith("/")) { // relative to source of workdir
|
|
||||||
if (path.matches(exclude.substring(1)))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
String name = path.substring(path.lastIndexOf("/") + 1);
|
|
||||||
if (name.matches(exclude))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean globalExcluded(File file, String path) {
|
|
||||||
if (file.isDirectory() && new File(file, "level.dat").exists())
|
|
||||||
return true;
|
|
||||||
if (new File(getSourceDir(), "logs").equals(file))
|
|
||||||
return true;
|
|
||||||
if (file.isFile() && file.getName().endsWith(".lck"))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getSourceDir() {
|
|
||||||
return new File(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCompressStart() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCompressEnd(boolean success) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected File getTargetDir() {
|
|
||||||
return new File(backupManager.config.backupDirectory, type.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getDisplayName() {
|
|
||||||
return type.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
import fr.pandacube.lib.paper.util.WorldUtil;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.World;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
|
|
||||||
public class CompressWorldProcess extends CompressProcess {
|
|
||||||
|
|
||||||
private boolean autoSave = true;
|
|
||||||
|
|
||||||
protected CompressWorldProcess(BackupManager bm, final String n) {
|
|
||||||
super(bm, Type.WORLDS, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
private World getWorld() {
|
|
||||||
return Bukkit.getWorld(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public BiPredicate<File, String> getFilenameFilter() {
|
|
||||||
return (f, s) -> true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getSourceDir() {
|
|
||||||
return WorldUtil.worldDir(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCompressStart() {
|
|
||||||
World w = getWorld();
|
|
||||||
if (w == null)
|
|
||||||
return;
|
|
||||||
autoSave = w.isAutoSave();
|
|
||||||
w.setAutoSave(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCompressEnd(boolean success) {
|
|
||||||
World w = getWorld();
|
|
||||||
if (w == null)
|
|
||||||
return;
|
|
||||||
w.setAutoSave(autoSave);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected File getTargetDir() {
|
|
||||||
return new File(backupManager.config.backupDirectory, type + "/" + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getDisplayName() {
|
|
||||||
return type + "/" + name;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import org.bukkit.ChatColor;
|
|
||||||
import org.bukkit.World;
|
|
||||||
import org.bukkit.configuration.InvalidConfigurationException;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.yaml.snakeyaml.error.YAMLException;
|
|
||||||
|
|
||||||
import fr.pandacube.lib.paper.PandaLibPaper;
|
|
||||||
import fr.pandacube.lib.util.Log;
|
|
||||||
|
|
||||||
public class Persist extends YamlConfiguration {
|
|
||||||
protected final BackupManager backupManager;
|
|
||||||
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
// private final Set<String> dirtyWorldsSave = new HashSet<>();
|
|
||||||
|
|
||||||
public Persist(BackupManager bm) {
|
|
||||||
file = new File(PandaLibPaper.getPlugin().getDataFolder(), "backup_persist.yml");
|
|
||||||
backupManager = bm;
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reload() {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void load() {
|
|
||||||
boolean loaded = false;
|
|
||||||
try {
|
|
||||||
load(file);
|
|
||||||
loaded = true;
|
|
||||||
}
|
|
||||||
catch (final FileNotFoundException ignored) { }
|
|
||||||
catch (final IOException e) {
|
|
||||||
Log.severe("cannot load " + file, e);
|
|
||||||
}
|
|
||||||
catch (final InvalidConfigurationException e) {
|
|
||||||
if (e.getCause() instanceof YAMLException) Log.severe("Config file " + file + " isn't valid!", e);
|
|
||||||
else if (e.getCause() == null || e.getCause() instanceof ClassCastException) Log.severe("Config file " + file + " isn't valid!");
|
|
||||||
else Log.severe("cannot load " + file, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
options().copyDefaults(true);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
try {
|
|
||||||
save(file);
|
|
||||||
}
|
|
||||||
catch (final IOException e) {
|
|
||||||
Log.severe("could not save " + file, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make the specified world dirty for compress. Also makes the specified world clean for saving if nobody is connected there.
|
|
||||||
*/
|
|
||||||
public void updateDirtyStatusAfterSave(final World world) {
|
|
||||||
if (world == null)
|
|
||||||
return;
|
|
||||||
if (!isDirty(Type.WORLDS, world.getName())) { // don't set dirty if it is already
|
|
||||||
setDirtySinceNow(Type.WORLDS, world.getName());
|
|
||||||
Log.info("[Backup] " + ChatColor.GRAY + Type.WORLDS + "/" + world.getName() + ChatColor.RESET + " was saved and is now dirty. Next backup on "
|
|
||||||
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
|
|
||||||
.format(new Date(backupManager.getNextCompress(System.currentTimeMillis())))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the dirty status after the specified compress process is done.
|
|
||||||
* @param t the type of process
|
|
||||||
* @param n the name of the process (for instance, the name of the world)
|
|
||||||
*/
|
|
||||||
public void updateDirtyStatusAfterCompress(Type t, String n) {
|
|
||||||
if (t == Type.WORLDS)
|
|
||||||
setNotDirty(t, n);
|
|
||||||
else
|
|
||||||
setDirtySinceNow(t, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void setDirtySinceNow(Type t, String n) {
|
|
||||||
set(t + "." + n + ".dirty_since", System.currentTimeMillis());
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNotDirty(Type t, String n) {
|
|
||||||
if (t == Type.WORKDIR)
|
|
||||||
return; // WORKDIR are always considered dirty
|
|
||||||
set(t + "." + n + ".dirty_since", -1);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isDirty(Type t, String n) {
|
|
||||||
if (t == Type.WORKDIR)
|
|
||||||
return true;
|
|
||||||
return isDirtySince(t, n) != -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long isDirtySince(Type t, String n) {
|
|
||||||
if (!contains(t + "." + n + ".dirty_since"))
|
|
||||||
setDirtySinceNow(t, n);
|
|
||||||
return getLong(t + "." + n + ".dirty_since");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* type: // (worlds|others)
|
|
||||||
* name: // (root|plugin|<worldName>)
|
|
||||||
* dirty_since: (true|false)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package fr.pandacube.lib.paper.modules.backup;
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
WORLDS,
|
|
||||||
WORKDIR;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return name().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean backupEnabled(BackupConfig cfg) {
|
|
||||||
return switch (this) {
|
|
||||||
case WORLDS -> cfg.worldBackupEnabled;
|
|
||||||
case WORKDIR -> cfg.workdirBackupEnabled;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public BackupCleaner backupCleaner(BackupConfig cfg) {
|
|
||||||
return switch (this) {
|
|
||||||
case WORLDS -> cfg.worldBackupCleaner;
|
|
||||||
case WORKDIR -> cfg.workdirBackupCleaner;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user