PandaLib/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupCleaner.java

159 lines
6.3 KiB
Java

package fr.pandacube.lib.core.backup;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import static fr.pandacube.lib.chat.ChatStatic.text;
/**
* Cleanup a backup directory (i.e. removes old backup archives).
* It is possible to combine different 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
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
return archives.descendingSet().stream()
.limit(n)
.collect(Collectors.toCollection(TreeSet::new));
}
};
}
/**
* 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 divider 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 -> ldt.getYear() * (12 / n) + ldt.getMonthValue() / n,
TreeMap::new,
Collectors.minBy(LocalDateTime::compareTo))
)
.values()
.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toCollection(TreeSet::new));
}
};
}
/**
* 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() {
@Override
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
TreeSet<LocalDateTime> merged = new TreeSet<>();
merged.addAll(self.apply(archives));
merged.addAll(other.apply(archives));
return merged;
}
};
}
/**
* Performs the cleanup operation on the provided directory.
* @param archiveDir the backup directory to clean up.
* @param compressDisplayName the display name of the backup process that manages the backup directory. Used for logs.
*/
public void cleanupArchives(File archiveDir, String compressDisplayName) {
String[] files = archiveDir.list();
if (files == null)
return;
Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "...");
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
for (String filename : files) {
File file = new File(archiveDir, filename);
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename);
continue;
}
String dateTimeStr = filename.substring(0, filename.length() - 4);
LocalDateTime ldt;
try {
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
} catch (DateTimeParseException e) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
continue;
}
datedFiles.put(ldt, file);
}
TreeSet<LocalDateTime> keptFiles = apply(new TreeSet<>(datedFiles.keySet()));
Chat c = text("[Backup] ")
.then(text(compressDisplayName).gray())
.thenText(testOnly ? " Archive cleanup debug (no files are actually deleted):\n" : " Deleted archive files:\n");
boolean oneDeleted = false;
for (Entry<LocalDateTime, File> datedFile : datedFiles.entrySet()) {
if (keptFiles.contains(datedFile.getKey())) {
if (testOnly)
c.thenText("- " + datedFile.getValue().getName() + " ")
.thenSuccess("kept")
.thenText(".\n");
continue;
}
oneDeleted = true;
c.thenText("- " + datedFile.getValue().getName() + " ");
if (testOnly)
c.thenFailure("deleted")
.thenText(".\n");
else
datedFile.getValue().delete();
}
if (testOnly || oneDeleted)
Log.warning(c.getLegacyText());
Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned.");
}
}