package jalview.io;
import jalview.bin.Cache;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.util.MessageManager;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
+import java.util.Map;
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_SUFFIX - a template to insert after the file extension. Use '%n' to be replaced by a 0-led SUFFIX_DIGITS long integer.
+ * BACKUPFILES_NO_MAX - flag to turn off setting a maximum number of backup files to keep.
* 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.
+ * BACKUPFILES_CONFIRM_DELETE_OLD - if true then prompt/confirm with the user when deleting older backup/version files.
*/
public class BackupFiles
{
// labels for saved params in Cache and .jalview_properties
- private static String NS = "BACKUPFILES";
+ private static final String NS = "BACKUPFILES";
- public static String ENABLED = NS + "_ENABLED";
+ public static final String ENABLED = NS + "_ENABLED";
- public static String SUFFIX = NS + "_SUFFIX";
+ public static final String SUFFIX = NS + "_SUFFIX";
- public static String ROLL_MAX = NS + "_ROLL_MAX";
+ public static final String NO_MAX = NS + "_NO_MAX";
- public static String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS";
+ public static final String ROLL_MAX = NS + "_ROLL_MAX";
- protected static String NUM_PLACEHOLDER = "%n";
+ public static final String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS";
- public static String REVERSE_ORDER = NS + "_REVERSE_ORDER";
+ public static final String NUM_PLACEHOLDER = "%n";
- private static String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
+ public static final String REVERSE_ORDER = NS + "_REVERSE_ORDER";
+
+ public static final String CONFIRM_DELETE_OLD = NS + "_CONFIRM_DELETE_OLD";
+
+ private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
+
+ private static final String TEMP_FILE_EXT = ".tmp";
// 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;
+ private static boolean enabled;
+
+ // confirmDelete - default flag as to whether to confirm with the user before
+ // deleting old backup/version files
+ private static boolean confirmDelete;
// defaultSuffix - default template to use to append to basename of file
private String suffix;
+ // noMax - flag to turn off a maximum number of files
+ private boolean noMax;
+
// defaultMax - default max number of backup files
private int max;
// temp saved file to become new saved file
private File tempFile;
+ // flag set to see if file save to temp file was successful
+ private boolean tempFileWriteSuccess;
+
public BackupFiles(String filename)
{
this(new File(filename));
}
- // first time defaults for ENABLED, SUFFIX, ROLL_MAX, SUFFIX_DIGITS and
+ // first time defaults for SUFFIX, NO_MAX, 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);
+ this(file, ".v" + NUM_PLACEHOLDER, false, 4, 3, false);
}
- public BackupFiles(File file, boolean defaultEnabled,
- String defaultSuffix,
- int defaultMax, int defaultDigits, boolean defaultReverseOrder)
+ public BackupFiles(File file,
+ String defaultSuffix, boolean defaultNoMax, int defaultMax,
+ int defaultDigits,
+ boolean defaultReverseOrder)
{
+ classInit();
this.file = file;
- this.enabled = Cache.getDefault(ENABLED, defaultEnabled);
this.suffix = Cache.getDefault(SUFFIX, defaultSuffix);
+ this.noMax = Cache.getDefault(NO_MAX, defaultNoMax);
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;
+ File temp = null;
try
{
if (file != null)
{
String tempfilename = file.getName();
File tempdir = file.getParentFile();
- temp = File.createTempFile(tempfilename, ".tmp", tempdir);
+ temp = File.createTempFile(tempfilename, TEMP_FILE_EXT, tempdir);
}
else
{
- temp = File.createTempFile(DEFAULT_TEMP_FILE, ".tmp");
- setTempFile(temp);
+ temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
}
} catch (IOException e)
{
{
System.out.println("Exception ctreating temp file for saving");
}
+ this.setTempFile(temp);
+ }
+
+ public static void classInit()
+ {
+ setEnabled(Cache.getDefault(ENABLED, true));
+ setConfirmDelete(Cache.getDefault(CONFIRM_DELETE_OLD, true));
+ }
+
+ public static void setEnabled(boolean flag)
+ {
+ enabled = flag;
+ }
+
+ public static boolean getEnabled()
+ {
+ classInit();
+ return enabled;
+ }
+
+ public static void setConfirmDelete(boolean flag)
+ {
+ confirmDelete = flag;
+ }
+
+ public static boolean getConfirmDelete()
+ {
+ classInit();
+ return confirmDelete;
+ }
+
+ // set, get and rename temp file into place
+ public void setTempFile(File temp)
+ {
+ this.tempFile = temp;
+ }
+
+ public File getTempFile()
+ {
+ return tempFile;
+ }
+
+ public String getTempFilePath()
+ {
+ String path = null;
+ try
+ {
+ path = this.getTempFile().getCanonicalPath();
+ } catch (IOException e)
+ {
+ System.out.println(
+ "IOException when getting Canonical Path of temp file '"
+ + this.getTempFile().getName() + "'");
+ }
+ return path;
+ }
+
+ public boolean setWriteSuccess(boolean flag)
+ {
+ boolean old = this.tempFileWriteSuccess;
+ this.tempFileWriteSuccess = flag;
+ return old;
+ }
+
+ public boolean getWriteSuccess()
+ {
+ return this.tempFileWriteSuccess;
+ }
+ public boolean renameTempFile()
+ {
+ return tempFile.renameTo(file);
}
+
// roll the backupfiles
public boolean rollBackupFiles()
{
// file doesn't yet exist or backups are not enabled
- if ((!file.exists()) || (!enabled) || (max < 1))
+ if ((!file.exists()) || (!enabled) || (max < 0))
{
// nothing to do
return true;
}
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)
+ File[] oldFilesToDelete = null;
+
+ // find existing backup files
+ BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
+ digits);
+ File[] backupFiles = dirFile.listFiles(bff);
+ int nextIndexNum = 0;
+ String confirmDeleteExtraInfo = null;
+
+ if (backupFiles.length == 0)
{
- // backup style numbering
-
- int tempMax = max;
- // max == -1 means no limits
- if (max == -1)
- {
- // do something cleverer here (possibly)!
- tempMax = 10000;
- }
+ // No other backup files. Just need to move existing file to backupfile_1
+ nextIndexNum = 1;
+ }
+ else
+ {
+ TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(backupFiles, basename);
- for (int m = 0; m < tempMax; m++)
+ if (reverseOrder)
{
- 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())
+ // backup style numbering
+
+ File lastfile = null;
+ int tempMax = noMax ? -1 : max;
+ // noMax == true means no limits
+ // look for first "gap" in backupFiles
+ // if tempMax is -1 at this stage just keep going until there's a gap,
+ // then hopefully tempMax gets set to the right index (a positive
+ // integer so the loop breaks)...
+ // why do I feel a little uneasy about this loop?..
+ for (int i = 1; tempMax < 0 || i <= max; i++)
{
- lastfile = backupfile_n;
- continue;
+ if (!bfTreeMap.containsKey(i)) // first index without existent
+ // backupfile
+ {
+ tempMax = i;
+ }
}
- if (m == 0)
- { // Move the max backup to /tmp instead of deleting (Just In
- // Case)
- String tmpfile = "tmp-" + backupfile_n.getName();
- try
+ // for (int m = 0; m < tempMax; m++)
+ for (int n = tempMax; n > 0; n--)
+ {
+ // int n = tempMax - m;
+ String backupfilename = dir + File.separatorChar
+ + BackupFilenameFilter.getBackupFilename(n, basename,
+ suffix, digits);
+ File backupfile_n = new File(backupfilename);
+
+ if (!backupfile_n.exists())
{
- File tmpFile = File.createTempFile(tmpfile, ".tmp");
- ret = ret && backupfile_n.renameTo(tmpFile);
- } catch (IOException e)
+ lastfile = backupfile_n;
+ continue;
+ }
+
+ // if (m == 0 && backupfile_n.exists())
+ if ((!noMax) && n == tempMax && backupfile_n.exists())
{
- System.out.println(
- "Could not create temp file '" + tmpfile + ".tmp'");
+ // move the largest (max) rolled file to a temp file and add to the delete list
+ try
+ {
+ File temp = File.createTempFile(backupfilename, TEMP_FILE_EXT,
+ dirFile);
+ backupfile_n.renameTo(temp);
+
+ oldFilesToDelete = new File[] { temp };
+ confirmDeleteExtraInfo = "(was " + backupfile_n.getName()
+ + ")";
+ } catch (IOException e)
+ {
+ System.out.println(
+ "IOException when creating temporary file for backupfilename");
+ }
}
- }
- else
- {
- // Just In Case
- if (lastfile != null)
+ else
{
- ret = ret && backupfile_n.renameTo(lastfile);
+ // Just In Case
+ if (lastfile != null)
+ {
+ ret = ret && backupfile_n.renameTo(lastfile);
+ }
}
+
+ lastfile = backupfile_n;
}
- lastfile = backupfile_n;
+ // index to use for the latest backup
+ nextIndexNum = 1;
}
-
- // 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)
+ else
{
- nextIndexNum = 1;
- } else {
+ // version style numbering (with earliest file deletion if max files
+ // reached)
- // and sort them (based on integer found in the suffix) using a
- // precomputed Hashmap for speed
- HashMap bfHashMap = new HashMap<Integer, File>();
- 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<Integer, File> 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)
+
+ // noMax == true means keep all backup files
+ if ((!noMax) && bfTreeMap.size() >= max)
{
// 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();
- }
+ int numToDelete = bfTreeMap.size() - max + 1;
+ oldFilesToDelete = Arrays.copyOfRange(backupFiles, 0,
+ numToDelete);
}
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);
}
}
+ deleteOldFiles(oldFilesToDelete, confirmDeleteExtraInfo);
+
+ // Let's make the new backup file!! yay, got there at last!
+ String latestBackupFilename = dir + File.separatorChar
+ + BackupFilenameFilter.getBackupFilename(nextIndexNum, basename,
+ suffix, digits);
+ File latestBackupFile = new File(latestBackupFilename);
+ ret = ret && file.renameTo(latestBackupFile);
+
return ret;
}
+ private void deleteOldFiles(File[] oldFilesToDelete, String confirmDeleteExtraInfo) {
+ if (oldFilesToDelete != null && oldFilesToDelete.length > 0)
+ {
+ // delete old backup/version files
+
+ boolean delete = false;
+ if (confirmDelete)
+ {
+ // Object[] confirmMessageArray = {};
+ StringBuilder confirmMessage = new StringBuilder();
+ confirmMessage.append(MessageManager
+ .getString("label.backupfiles_confirm_delete_old_files"));
+ for (File f : oldFilesToDelete)
+ {
+ confirmMessage.append("\n");
+ confirmMessage.append(f.getName());
+ }
+ if (confirmDeleteExtraInfo != null
+ && confirmDeleteExtraInfo.length() > 0)
+ {
+ confirmMessage.append("\n");
+ confirmMessage.append(confirmDeleteExtraInfo);
+ }
+ int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
+ confirmMessage.toString(),
+ MessageManager
+ .getString("label.backupfiles_confirm_delete"),
+ JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
+
+ delete = (confirm == JvOptionPane.YES_OPTION);
+ }
+ else
+ {
+ delete = true;
+ }
+
+ if (delete)
+ {
+ for (int i = 0; i < oldFilesToDelete.length; i++)
+ {
+ File fileToDelete = oldFilesToDelete[i];
+ fileToDelete.delete();
+ // System.out.println("DELETING '" + fileToDelete.getName() +
+ // "'");
+ }
+ }
+
+ }
+ }
+
+ private TreeMap sortBackupFilesAsTreeMap(File[] backupFiles, String basename) {
+ // sort the backup files (based on integer found in the suffix) using a
+ // precomputed Hashmap for speed
+ Map<Integer, File> bfHashMap = new HashMap<>();
+ for (int i = 0; i < backupFiles.length; i++)
+ {
+ File f = backupFiles[i];
+ BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, digits);
+ bfHashMap.put(bfp.indexNum(), f);
+ }
+ TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
+ bfTreeMap.putAll(bfHashMap);
+ return bfTreeMap;
+ }
+
+ public boolean rollBackupsAndRenameTempFile()
+ {
+ boolean write = this.getWriteSuccess();
+
+ boolean roll = false;
+ if (write) {
+ roll = this.rollBackupFiles();
+ } else {
+ return false;
+ }
+
+ /*
+ * Not sure that this confirmation is desirable. By this stage the new file is
+ * already written successfully, but something (e.g. disk full) has happened while
+ * trying to roll the backup files, and most likely the filename needed will already
+ * be vacant so renaming the temp file is nearly always correct!
+ */
+ if (!roll)
+ {
+ int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
+ MessageManager.getString(
+ "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"),
+ MessageManager.getString("label.backupfiles_confirm_save_file"),
+ JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
+
+ if (confirm == JvOptionPane.YES_OPTION)
+ {
+ roll = true;
+ }
+ }
+
+ boolean rename = false;
+ if (roll)
+ {
+ rename = this.renameTempFile();
+ }
+
+ return rename;
+ }
+
+ public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
+ String fileName,
+ String suffix, int digits)
+ {
+ File[] backupFiles = null;
+
+ File file = new File(fileName);
+
+ 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 new TreeMap<>();
+ }
+
+ String filename = file.getName();
+ String basename = filename;
+
+ // find existing backup files
+ BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, digits);
+ backupFiles = dirFile.listFiles(bff); // is clone needed?
+
+ // sort the backup files (based on integer found in the suffix) using a
+ // precomputed Hashmap for speed
+ Map<Integer, File> bfHashMap = new HashMap<>();
+ for (int i = 0; i < backupFiles.length; i++)
+ {
+ File f = backupFiles[i];
+ BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
+ digits);
+ bfHashMap.put(bfp.indexNum(), f);
+ }
+ TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
+ bfTreeMap.putAll(bfHashMap);
+
+ return bfTreeMap;
+ }
+
+
}