package nl.helixsoft.misc; import java.io.File; import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; import org.joda.time.LocalDate; import org.joda.time.Period; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; /** * Utility for culling old backup files. * <p> * Takes a list of files as input. The files are grouped by age. For each age-group one file is kept, the rest is discarded. * This way you only keep a small collection of historic backups. * <p> * TODO * [ ] Provide command-line options to adjust the bin cut-offs * [ ] Allow working on dated directories instead of files * [ ] Provide command-line option to extract date from filename instead of file creation time. * [ ] Better unit testing. */ public class BackupCull { /** * Comparator used to decide which file to keep from a Bin. * We give preference to certain dates, i.e. the first day of the month, on the first month of the year, etc. */ private static class PreferredFileComparator implements Comparator <File> { public int score (File a) { int score = 0; LocalDate t = new LocalDate (a.lastModified()); int dom = t.getDayOfMonth(); if ((dom % 7) == 1) score += 5; // prefer days 1, 8, 15, 22, 29 if (dom == 1) score += 5; // prefer day 1 even more int moy = t.getMonthOfYear(); if ((moy %3) == 1) score++; // prefer months 1, 4, 7, 10 if (moy == 1) score++; // prefer month 1 even more return score; } public int compare(File a, File b) { // From Java 7: Integer.compare (score(a), score(b)) return Integer.valueOf(score(b)).compareTo(Integer.valueOf(score(a))); } } /** * A Bin is a collection of Files below a certain age. * Bins are chained - Files that fall below a cut-off are passed to the next bin. */ private static class Bin { /** * With andThen... we can create a chain of Bins. */ public Bin andThen (LocalDate d) { next = new Bin(); next.end = d; return next; } /** * Try to add a File to this bin. If it's too old, it's automatically passed to the next bin. */ public void addFile(File f) { LocalDate ftime = new LocalDate (f.lastModified()); if (ftime.compareTo(end) < 0) { if (next != null) { next.addFile (f); } } else { files.add (f); } } /** * @param dryrun enables dry-run mode, the actions are printed but not actually executed. */ public void execute(boolean dryrun) { // keep the first File in the priorityQueue (it has the highest score). The others are marked for deletion. boolean first = true; for (File f : files) { if (first) { if (dryrun) System.out.println (" KEEP " + f); } else { if (dryrun) { System.out.println (" REM " + f); } else { System.out.print ("DELETE " + f); // perform actual delete if (!f.delete()) { System.out.print ("-> FAILED"); } System.out.println(); } } first = false; } if (dryrun) System.out.println (end); if (next != null) { next.execute(dryrun); } } Bin next; LocalDate end; PriorityQueue<File> files = new PriorityQueue<File>(16, new PreferredFileComparator()); } /** Command-line options */ public static class Options { @Option(name="-n", usage="Dry run. Show actions, but do not execute.") boolean dryRun = false; @Option(name="--help", aliases="-h", usage="Show usage") boolean help = false; @Argument(usage="Files to cull") List<File> files; } Options opts; /** All the work starts here */ public void run(String[] args) { opts = new Options(); CmdLineParser parser = new CmdLineParser(opts); try { parser.parseArgument(args); if (opts.help) throw new CmdLineException (parser, "Help requested"); if (opts.files == null) throw new CmdLineException (parser, "Expected at least one file"); } catch (CmdLineException ex) { System.err.println(ex.getMessage()); parser.printUsage(System.err); return; } LocalDate today = new LocalDate(); Bin chain = new Bin (); chain.end = today; chain. andThen (today.minus(Period.days(3))). andThen (today.minus(Period.days(7))). andThen (today.minus(Period.months(1))). andThen (today.minus(Period.months(3))). andThen (today.minus(Period.years(1))). andThen (today.minus(Period.years(2))). andThen (today.minus(Period.years(5))); for (File f : opts.files) { chain.addFile (f); } chain.execute(opts.dryRun); } public static void main(String[] args) { new BackupCull().run(args); } }