diff --git a/pandalib-bungee/src/main/java/fr/pandacube/lib/bungee/backup/BungeeWorkdirProcess.java b/pandalib-bungee/src/main/java/fr/pandacube/lib/bungee/backup/BungeeWorkdirProcess.java index 9ea648c..74b7007 100644 --- a/pandalib-bungee/src/main/java/fr/pandacube/lib/bungee/backup/BungeeWorkdirProcess.java +++ b/pandalib-bungee/src/main/java/fr/pandacube/lib/bungee/backup/BungeeWorkdirProcess.java @@ -60,10 +60,4 @@ public class BungeeWorkdirProcess extends BackupProcess { return "workdir"; } - - - public void displayNextSchedule() { - Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on " - + DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext()))); - } } diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupCleaner.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupCleaner.java index 13a4cb9..d14c5aa 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupCleaner.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupCleaner.java @@ -16,10 +16,19 @@ import java.util.stream.Collectors; import static fr.pandacube.lib.chat.ChatStatic.text; +/** + * Cleanup a backup directory (i.e. removes old backup archives). + * It is possible to combine differents instances to affect which archive to keep or delete. + */ public abstract class BackupCleaner implements UnaryOperator> { private static final boolean testOnly = false; // if true, no files are deleted + /** + * Creates a {@link BackupCleaner} that keeps the n last archives in the backup directory. + * @param n the number of last archives to keep. + * @return a {@link BackupCleaner} that keeps the n last archives in the backup directory. + */ public static BackupCleaner KEEPING_N_LAST(int n) { return new BackupCleaner() { @Override @@ -32,15 +41,23 @@ public abstract class BackupCleaner implements UnaryOperator + * This cleaner divides each year into sections of n month. For each month, its compute a section id using the + * formula YEAR * (12 / n) + MONTH / n. It then keeps the first archive + * found in each section. + * + * @param n the interval in month between each kept archives. Must be a dividor of 12 (1, 2, 3, 4, 6 or 12). + * @return a {@link BackupCleaner} that keeps one archive every n month. + */ public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) { return new BackupCleaner() { @Override public TreeSet apply(TreeSet localDateTimes) { return localDateTimes.stream() .collect(Collectors.groupingBy( - ldt -> { - return ldt.getYear() * 4 + ldt.getMonthValue() / n; - }, + ldt -> ldt.getYear() * (12 / n) + ldt.getMonthValue() / n, TreeMap::new, Collectors.minBy(LocalDateTime::compareTo)) ) @@ -54,8 +71,13 @@ public abstract class BackupCleaner implements UnaryOperator backupQueue = new ArrayList<>(); /* package */ final AtomicReference runningBackup = new AtomicReference<>(); private final Timer schedulerTimer = new Timer(); + /** + * Instanciate a new backup manager. + * @param backupDirectory the root backup directory. + */ public BackupManager(File backupDirectory) { this.backupDirectory = backupDirectory; persist = new Persist(this); @@ -37,17 +46,26 @@ public class BackupManager extends TimerTask { schedulerTimer.scheduleAtFixedRate(this, new Date(nextMinute), 60_000); } - + /** + * Add a new backup process to the queue. + * @param process the backup process to add. + */ protected void addProcess(BackupProcess process) { process.displayNextSchedule(); backupQueue.add(process); } - + /** + * Gets the backup root directory. + * @return the backup root directory. + */ public File getBackupDirectory() { return backupDirectory; } + + + public synchronized void run() { BackupProcess tmp; if ((tmp = runningBackup.get()) != null) { @@ -65,7 +83,10 @@ public class BackupManager extends TimerTask { } - + /** + * Disables this backup manager, canceling scheduled backups. + * It will wait for a currently running backup to finish before returning. + */ public synchronized void onDisable() { schedulerTimer.cancel(); @@ -88,8 +109,6 @@ public class BackupManager extends TimerTask { } } } - - persist.save(); } diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java index 0641770..bc5e0f1 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java @@ -7,20 +7,30 @@ import fr.pandacube.lib.util.Log; import net.md_5.bungee.api.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.ArrayList; +import java.util.Date; import java.util.List; import java.util.function.BiPredicate; +/** + * A backup process. + */ public abstract class BackupProcess implements Comparable, Runnable { private final BackupManager backupManager; + /** + * The process identifier. + */ public final String identifier; - + /** + * The zip compressor. + */ protected ZipCompressor compressor = null; @@ -29,20 +39,37 @@ public abstract class BackupProcess implements Comparable, Runnab private BackupCleaner backupCleaner = null; private List ignoreList = new ArrayList<>(); - + /** + * Instanciates a new backup process. + * @param bm the associated backup manager. + * @param n the process identifier. + */ protected BackupProcess(BackupManager bm, final String n) { backupManager = bm; identifier = n; } + /** + * Gets the associated backup manager. + * @return the associated backup manager. + */ public BackupManager getBackupManager() { return backupManager; } + /** + * Gets the process identifier. + * @return the process identifier. + */ public String getIdentifier() { return identifier; } + /** + * Gets the displayname of this process. + * Default implementation returns {@link #getIdentifier()}. + * @return the displayname of this process. + */ protected String getDisplayName() { return getIdentifier(); } @@ -55,9 +82,11 @@ public abstract class BackupProcess implements Comparable, Runnab } - - - + /** + * Provides a predicate that tells if a provided file must be included in the archive or not. + * The default implementation returns a filter based on the content of {@link #getIgnoreList()}. + * @return a predicate. + */ public BiPredicate getFilenameFilter() { return (file, path) -> { for (String exclude : ignoreList) { @@ -75,47 +104,91 @@ public abstract class BackupProcess implements Comparable, Runnab }; } + /** + * Gets the source directory to backup. + * @return the source directory to backup. + */ public abstract File getSourceDir(); + /** + * Gets the directory in which to put the archives. + * @return the directory in which to put the archives. + */ protected abstract File getTargetDir(); + /** + * Called when the backup starts. + */ protected abstract void onBackupStart(); + /** + * Called when the backup ends. + * @param success true if the backup ended successfuly. + */ protected abstract void onBackupEnd(boolean success); - - - - + /** + * Tells if this backup process is enabled. + * A disabled backup process will not run. + * @return true if this backup process is enabled, false otherwise. + */ public boolean isEnabled() { return enabled; } + /** + * Sets the enabled status of this backup process. + * @param enabled the enabled status of this backup process. + */ public void setEnabled(boolean enabled) { this.enabled = enabled; } + /** + * Gets the string representation of the scheduling, using cron format. + * @return the string representation of the scheduling. + */ public String getScheduling() { return scheduling; } + /** + * Sets the string representation of the scheduling. + * @param scheduling the string representation of the scheduling, in the CRON format (without seconds). + */ public void setScheduling(String scheduling) { this.scheduling = scheduling; } + /** + * Gets the associated backup cleaner, that is executed at the end of this backup process. + * @return the associated backup cleaner. + */ public BackupCleaner getBackupCleaner() { return backupCleaner; } + /** + * Sets the backup cleaner of this backup process. + * @param backupCleaner the backup cleaner of this backup process. + */ public void setBackupCleaner(BackupCleaner backupCleaner) { this.backupCleaner = backupCleaner; } + /** + * Gets the current list of files that are ignored during the backup process. + * @return the current list of files that are ignored during the backup process. + */ public List getIgnoreList() { return ignoreList; } + /** + * Sets a new list of files that will be ignored during the backup process. + * @param ignoreList the new list of files that are ignored during the backup process. + */ public void setIgnoreList(List ignoreList) { this.ignoreList = ignoreList; } @@ -190,17 +263,18 @@ public abstract class BackupProcess implements Comparable, Runnab } + /** + * Logs the scheduling status of this backup process. + */ + public void displayNextSchedule() { + Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on " + + DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext()))); + } - - - - - - - public abstract void displayNextSchedule(); - - + /** + * A formatter used to format and parse the name of backup archives, based on a date and time. + */ public static final DateTimeFormatter dateFileNameFormatter = new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4) .appendValue(ChronoField.MONTH_OF_YEAR, 2) @@ -216,7 +290,10 @@ public abstract class BackupProcess implements Comparable, Runnab return dateFileNameFormatter.format(ZonedDateTime.now()); } - + /** + * Logs the progress of this currently running backup process. + * Logs nothing if this backup is not in progress. + */ public void logProgress() { if (compressor == null) return; @@ -224,10 +301,10 @@ public abstract class BackupProcess implements Comparable, Runnab } - - - - + /** + * Tells if this backup process could start now. + * @return true if this backup process could start now, false otherwise. + */ public boolean couldRunNow() { if (!isEnabled()) return false; @@ -239,26 +316,43 @@ public abstract class BackupProcess implements Comparable, Runnab } - - + /** + * Gets the time of the next scheduled run. + * @return the time, in millis-timestamp, of the next scheduled run, or {@link Long#MAX_VALUE} if it’s not scheduled. + */ public long getNext() { if (!hasNextScheduled()) return Long.MAX_VALUE; return getNextCompress(backupManager.persist.isDirtySince(identifier)); } + /** + * Tells if this backup is scheduled or not. + * @return true if this backup is scheduled, false otherwise. + */ public boolean hasNextScheduled() { return isEnabled() && isDirty(); } + /** + * Tells if the content to be backed up is dirty or not. The source data is not dirty if it has not changed since + * the last backup. + * @return the dirty status of the data to be backed-up by this backup process. + */ public boolean isDirty() { return backupManager.persist.isDirty(identifier); } + /** + * Sets the source data as dirty since now. + */ public void setDirtySinceNow() { backupManager.persist.setDirtySinceNow(identifier); } + /** + * Sets the source data as not dirty. + */ public void setNotDirty() { backupManager.persist.setNotDirty(identifier); } @@ -268,9 +362,9 @@ public abstract class BackupProcess implements Comparable, Runnab /** - * 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 + * Gets the millis-timestamp 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) diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/Persist.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/Persist.java index 75625c4..b27e5f4 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/Persist.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/Persist.java @@ -12,6 +12,11 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +/** + * Handles the data stored used for backup manager, like dirty status of data to be backed up. + * The data is stored using JSON format, in a file in the root backup directory. + * The file is updated on disk on every call to a {@code set*(...)} method. + */ public class Persist { private Map dirtySince = new HashMap<>(); @@ -19,17 +24,18 @@ public class Persist { private final File file; // private final Set dirtyWorldsSave = new HashSet<>(); - + + /** + * Creates a new instance, immediatly loading the data from the file if it exists, or creating an empty one if not. + * @param bm the associated backup manager. + */ public Persist(BackupManager bm) { file = new File(bm.getBackupDirectory(), "source-dirty-since.json"); load(); } - public void reload() { - load(); - } - protected void load() { + private void load() { boolean loaded = false; try (FileReader reader = new FileReader(file)) { dirtySince = Json.gson.fromJson(reader, new TypeToken>(){}.getType()); @@ -48,8 +54,8 @@ public class Persist { save(); } } - - public void save() { + + private void save() { try (FileWriter writer = new FileWriter(file, false)) { Json.gsonPrettyPrinting.toJson(dirtySince, writer); } @@ -57,27 +63,32 @@ public class Persist { Log.severe("could not save " + file, e); } } - - - - - public void setDirtySinceNow(String id) { + + /** + * Sets the backup process with the provided id as dirty. + * @param id the id of the backup process. + */ + public synchronized void setDirtySinceNow(String id) { dirtySince.put(id, System.currentTimeMillis()); save(); } - public void setNotDirty(String id) { + /** + * Sets the backup process with the provided id as not dirty. + * @param id the id of the backup process. + */ + public synchronized void setNotDirty(String id) { dirtySince.put(id, -1L); save(); } - - - public boolean isDirty(String id) { + + + public synchronized boolean isDirty(String id) { return isDirtySince(id) != -1; } - public long isDirtySince(String id) { + public synchronized long isDirtySince(String id) { if (!dirtySince.containsKey(id)) setDirtySinceNow(id); return dirtySince.get(id); diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/RotatedLogsBackupProcess.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/RotatedLogsBackupProcess.java index 4e59765..d596856 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/RotatedLogsBackupProcess.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/RotatedLogsBackupProcess.java @@ -120,10 +120,4 @@ public class RotatedLogsBackupProcess extends BackupProcess { 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()))); - } } diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java index c151c7e..d2e944a 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java @@ -20,7 +20,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -// TODO Add support for persisted last execution timestamps /** * Application wide task scheduler using Cron expression. */ @@ -78,8 +77,10 @@ public class CronScheduler { /** * Schedule a task. + * If a task with the provided taskId already exists, it will be replaced. * @param taskId the id of the task. - * @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values) + * @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values). + * See {@link CronExpression} for the format. * @param task the task to run. */ public static void schedule(String taskId, String cronExpression, Runnable task) { @@ -185,8 +186,6 @@ public class CronScheduler { catch (final JsonParseException e) { Log.severe("cannot load " + lastRunFile, e); } - finally { - } if (!loaded) { saveLastRuns(); diff --git a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/backup/PaperWorkdirProcess.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/backup/PaperWorkdirProcess.java index 5fe07c8..397dbce 100644 --- a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/backup/PaperWorkdirProcess.java +++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/backup/PaperWorkdirProcess.java @@ -53,10 +53,4 @@ public class PaperWorkdirProcess extends PaperBackupProcess { 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()))); - } }