Compare commits

...

2 Commits

Author SHA1 Message Date
dd2b4467ed new StringUtil.asPatternInSentense method 2023-02-11 23:57:56 +01:00
6577367c27 Javadoc + some small refactoring 2023-02-11 23:40:36 +01:00
9 changed files with 233 additions and 83 deletions

View File

@ -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())));
}
}

View File

@ -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<TreeSet<LocalDateTime>> {
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<TreeSet<LocalDateTi
}
/**
* Creates a {@link BackupCleaner} that keeps one archive every n month.
* <p>
* This cleaner divides each year into sections of n month. For each month, its compute a section id using the
* formula <code><i>YEAR</i> * (12 / <i>n</i>) + <i>MONTH</i> / <i>n</i></code>. 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<LocalDateTime> apply(TreeSet<LocalDateTime> 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<TreeSet<LocalDateTi
}
/**
* Creates a new {@link BackupCleaner} that keeps the archives kept by this {@link BackupCleaner} or by the provided
* one.
* In other word, it makes a union operation with the set of archives kept by both original {@link BackupCleaner}.
* @param other the other {@link BackupCleaner} to merge with.
* @return a new {@link BackupCleaner}. The original ones are not affected.
*/
public BackupCleaner merge(BackupCleaner other) {
BackupCleaner self = this;
return new BackupCleaner() {
@ -70,8 +92,11 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
}
/**
* Performs the cleanup operation on the provided directory.
* @param archiveDir the backup directory to cleanup.
* @param compressDisplayName the displayname of the backup process that manages the backup directory. Used for logs.
*/
public void cleanupArchives(File archiveDir, String compressDisplayName) {
String[] files = archiveDir.list();

View File

@ -1,11 +1,8 @@
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;
@ -13,20 +10,32 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.LongStream;
/**
* Handles the backup processes.
*/
public class BackupManager extends TimerTask {
private final File backupDirectory;
/**
* The {@link Persist} instance of this {@link BackupManager}.
*/
protected final Persist persist;
/**
* The list of backup processes that are scheduled.
*/
protected final List<BackupProcess> backupQueue = new ArrayList<>();
/* package */ final AtomicReference<BackupProcess> 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();
}

View File

@ -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<BackupProcess>, 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<BackupProcess>, Runnab
private BackupCleaner backupCleaner = null;
private List<String> 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<BackupProcess>, 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<File, String> getFilenameFilter() {
return (file, path) -> {
for (String exclude : ignoreList) {
@ -75,47 +104,91 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, 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<String> 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<String> ignoreList) {
this.ignoreList = ignoreList;
}
@ -190,17 +263,18 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, 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<BackupProcess>, 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<BackupProcess>, 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<BackupProcess>, 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 its 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<BackupProcess>, 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)

View File

@ -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<String, Long> dirtySince = new HashMap<>();
@ -20,16 +25,17 @@ public class Persist {
// private final Set<String> 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<Map<String, Long>>(){}.getType());
@ -49,7 +55,7 @@ public class Persist {
}
}
public void save() {
private void save() {
try (FileWriter writer = new FileWriter(file, false)) {
Json.gsonPrettyPrinting.toJson(dirtySince, writer);
}
@ -59,25 +65,30 @@ public class Persist {
}
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);

View File

@ -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())));
}
}

View File

@ -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();

View File

@ -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())));
}
}

View File

@ -139,4 +139,24 @@ public class StringUtil {
return s -> Long.toString(operator.apply(Long.parseLong(s)));
}
/**
* Generate a {@link Pattern} with extra wrapping regex around the provided one to consider a sentense (like a chat
* message). For instance, the returned pattern will only match the expression at the beginning or end of sentence,
* or separated by the rest of it with space or another non-letter character.
* @param wordPattern the regex pattern to wrap.
* @param caseInsensitive if the pattern must match ignoring case.
* @return a {@link Pattern}. The matching will match 3 groups. The first group is the eventual non-letter separator
* before the matched word, the second one is the actual word, and the last one is the eventual non-letter separator
* after the matched word. Any additionnal pattern group between the 2nd and the last one are thoses provided in the
* wordPattern.
*/
public static Pattern asPatternInSentense(String wordPattern, boolean caseInsensitive) {
return Pattern.compile((caseInsensitive ? "(?i)" : "") + "(\\P{L}|^)(" + wordPattern + ")(\\P{L}|$)");
}
}