package jalview.io; import jalview.bin.Cache; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.TreeMap; /* * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved. * User configurable options are: * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved. * BACKUPFILES_SUFFIX - a template to insert just before the file extension. Use '%n' to be replaced by a 0-led SUFFIX_DIGITS long integer. * BACKUPFILES_ROLL_MAX - the maximum number of backupfiles to keep for any one alignment or project file. * BACKUPFILES_SUFFIX_DIGITS - the number of digits to insert replace %n with (e.g. BACKUPFILES_SUFFIX_DIGITS = 3 would make "001", "002", etc) * BACKUPFILES_REVERSE_ORDER - if true then "logfile" style numbering and file rolling will occur. If false then ever-increasing version numbering will occur, but old files will still be deleted if there are more than ROLL_MAX backup files. */ public class BackupFiles { // labels for saved params in Cache and .jalview_properties private static String NS = "BACKUPFILES"; public static String ENABLED = NS + "_ENABLED"; public static String SUFFIX = NS + "_SUFFIX"; public static String ROLL_MAX = NS + "_ROLL_MAX"; public static String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS"; protected static String NUM_PLACEHOLDER = "%n"; public static String REVERSE_ORDER = NS + "_REVERSE_ORDER"; private static String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS; // file - File object to be backed up and then updated (written over) private File file; // enabled - default flag as to whether to do the backup file roll (if not // defined in preferences) private boolean enabled; // defaultSuffix - default template to use to append to basename of file private String suffix; // defaultMax - default max number of backup files private int max; // defaultDigits - number of zero-led digits to use in the filename private int digits; // reverseOrder - set to true to make newest (latest) files lowest number // (like rolled log files) private boolean reverseOrder; // temp saved file to become new saved file private File tempFile; public BackupFiles(String filename) { this(new File(filename)); } // first time defaults for ENABLED, SUFFIX, ROLL_MAX, SUFFIX_DIGITS and // REVERSE_ORDER public BackupFiles(File file) { this(file, true, "-v" + NUM_PLACEHOLDER, 4, 3, false); } // set, get and rename temp file into place public void setTempFile(File temp) { this.tempFile = temp; } public File getTempFile() { return tempFile; } public boolean renameTempFile() { return tempFile.renameTo(file); } public BackupFiles(File file, boolean defaultEnabled, String defaultSuffix, int defaultMax, int defaultDigits, boolean defaultReverseOrder) { this.file = file; this.enabled = Cache.getDefault(ENABLED, defaultEnabled); this.suffix = Cache.getDefault(SUFFIX, defaultSuffix); this.max = Cache.getDefault(ROLL_MAX, defaultMax); this.digits = Cache.getDefault(SUFFIX_DIGITS, defaultDigits); this.reverseOrder = Cache.getDefault(REVERSE_ORDER, defaultReverseOrder); // create a temp file to save new data in File temp; try { if (file != null) { String tempfilename = file.getName(); File tempdir = file.getParentFile(); temp = File.createTempFile(tempfilename, ".tmp", tempdir); } else { temp = File.createTempFile(DEFAULT_TEMP_FILE, ".tmp"); setTempFile(temp); } } catch (IOException e) { System.out.println( "Could not create temp file to save into (IOException)"); } catch (Exception e) { System.out.println("Exception ctreating temp file for saving"); } } // roll the backupfiles public boolean rollBackupFiles() { // file doesn't yet exist or backups are not enabled if ((!file.exists()) || (!enabled) || (max < 1)) { // nothing to do return true; } // split filename up to insert suffix template in the right place. template // and backupMax can be set in .jalview_properties String dir = ""; File dirFile; try { dirFile = file.getParentFile(); dir = dirFile.getCanonicalPath(); } catch (Exception e) { System.out.println( "Could not get canonical path for file '" + file + "'"); return false; } String filename = file.getName(); String basename = filename; String extension = ""; int dotcharpos = filename.lastIndexOf('.'); // don't split of filenames with the last '.' at the very beginning or // very end of the filename if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1)) { basename = filename.substring(0, dotcharpos); extension = filename.substring(dotcharpos); // NOTE this includes the '.' } boolean ret = true; // Create/move backups up one String numString = null; File lastfile = null; if (reverseOrder) { // backup style numbering int tempMax = max; // max == -1 means no limits if (max == -1) { // do something cleverer here (possibly)! tempMax = 10000; } for (int m = 0; m < tempMax; m++) { int n = tempMax - m; String backupfilename = dir + File.separatorChar + BackupFilenameParts.getBackupFilename(n, basename, suffix, digits, extension); File backupfile_n = new File(backupfilename); if (!backupfile_n.exists()) { lastfile = backupfile_n; continue; } if (m == 0) { // Move the max backup to /tmp instead of deleting (Just In // Case) String tmpfile = "tmp-" + backupfile_n.getName(); try { File tmpFile = File.createTempFile(tmpfile, ".tmp"); ret = ret && backupfile_n.renameTo(tmpFile); } catch (IOException e) { System.out.println( "Could not create temp file '" + tmpfile + ".tmp'"); } } else { // Just In Case if (lastfile != null) { ret = ret && backupfile_n.renameTo(lastfile); } } lastfile = backupfile_n; } // now actually backup the important file! ret = ret && file.renameTo(lastfile); } else { // version style numbering (with earliest file deletion if max files // reached) // find existing backup files BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, digits, extension); File[] backupFiles = dirFile.listFiles(bff); int nextIndexNum; if (backupFiles.length == 0) { nextIndexNum = 1; } else { // and sort them (based on integer found in the suffix) using a // precomputed Hashmap for speed HashMap bfHashMap = new HashMap(); for (int i = 0; i < backupFiles.length; i++) { File f = backupFiles[i]; BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, digits, extension); bfHashMap.put(bfp.indexNum(), f); } TreeMap bfTreeMap = new TreeMap<>(); bfTreeMap.putAll(bfHashMap); bfTreeMap.values().toArray(backupFiles); // max value of -1 means keep all backup files if (bfTreeMap.size() >= max && max != -1) { // need to delete some files to keep number of backups to designated // max int numToDelete = bfTreeMap.size() - max; File[] filesToDelete = Arrays.copyOfRange(backupFiles, 0, numToDelete - 1); /****************************************** * CONFIRM THESE DELETIONS WITH THE USER! * ******************************************/ for (int i = 0; i < filesToDelete.length; i++) { File toDelete = filesToDelete[i]; toDelete.delete(); } } nextIndexNum = bfTreeMap.lastKey() + 1; // Let's make the new backup file!! yay, got there at last! String nextBackupFilename = dir + File.separatorChar + BackupFilenameParts.getBackupFilename(nextIndexNum, basename, suffix, digits, extension); File nextBackupFile = new File(nextBackupFilename); ret = ret && file.renameTo(nextBackupFile); } } return ret; } }