3 import jalview.bin.Cache;
4 import jalview.gui.Desktop;
5 import jalview.gui.JvOptionPane;
6 import jalview.util.MessageManager;
9 import java.io.IOException;
10 import java.text.SimpleDateFormat;
11 import java.util.ArrayList;
12 import java.util.HashMap;
14 import java.util.TreeMap;
17 * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
18 * User configurable options are:
19 * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
20 * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM
21 * (see BackupFilesPresetEntry)
24 public class BackupFiles
27 // labels for saved params in Cache and .jalview_properties
28 public static final String NS = "BACKUPFILES";
30 public static final String ENABLED = NS + "_ENABLED";
32 public static final String NUM_PLACEHOLDER = "%n";
34 private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
36 private static final String TEMP_FILE_EXT = ".tmp";
38 // file - File object to be backed up and then updated (written over)
41 // enabled - default flag as to whether to do the backup file roll (if not
42 // defined in preferences)
43 private static boolean enabled;
45 // confirmDelete - default flag as to whether to confirm with the user before
46 // deleting old backup/version files
47 private static boolean confirmDelete;
49 // defaultSuffix - default template to use to append to basename of file
50 private String suffix;
52 // noMax - flag to turn off a maximum number of files
53 private boolean noMax;
55 // defaultMax - default max number of backup files
58 // defaultDigits - number of zero-led digits to use in the filename
61 // reverseOrder - set to true to make newest (latest) files lowest number
62 // (like rolled log files)
63 private boolean reverseOrder;
65 // temp saved file to become new saved file
66 private File tempFile;
68 // flag set to see if file save to temp file was successful
69 private boolean tempFileWriteSuccess;
71 // array of files to be deleted, with extra information
72 private ArrayList<File> deleteFiles = new ArrayList<>();
74 // date formatting for modification times
75 private static final SimpleDateFormat sdf = new SimpleDateFormat(
76 "yyyy-MM-dd HH:mm:ss");
78 public BackupFiles(String filename)
80 this(new File(filename));
83 // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and
85 public BackupFiles(File file)
89 BackupFilesPresetEntry bfpe = BackupFilesPresetEntry.getSavedBackupEntry();
90 this.suffix = bfpe.suffix;
91 this.noMax = bfpe.keepAll;
92 this.max = bfpe.rollMax;
93 this.digits = bfpe.digits;
94 this.reverseOrder = bfpe.reverse;
96 // create a temp file to save new data in
102 String tempfilename = file.getName();
103 File tempdir = file.getParentFile();
104 temp = File.createTempFile(tempfilename, TEMP_FILE_EXT + "_newfile",
109 temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
111 } catch (IOException e)
114 "Could not create temp file to save into (IOException)");
115 } catch (Exception e)
117 System.out.println("Exception ctreating temp file for saving");
119 this.setTempFile(temp);
122 public static void classInit()
124 setEnabled(Cache.getDefault(ENABLED, true));
125 BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
126 .getSavedBackupEntry();
127 setConfirmDelete(bfpe.confirmDelete);
130 public static void setEnabled(boolean flag)
135 public static boolean getEnabled()
141 public static void setConfirmDelete(boolean flag)
143 confirmDelete = flag;
146 public static boolean getConfirmDelete()
149 return confirmDelete;
152 // set, get and rename temp file into place
153 public void setTempFile(File temp)
155 this.tempFile = temp;
158 public File getTempFile()
163 public String getTempFilePath()
168 path = this.getTempFile().getCanonicalPath();
169 } catch (IOException e)
172 "IOException when getting Canonical Path of temp file '"
173 + this.getTempFile().getName() + "'");
178 public boolean setWriteSuccess(boolean flag)
180 boolean old = this.tempFileWriteSuccess;
181 this.tempFileWriteSuccess = flag;
185 public boolean getWriteSuccess()
187 return this.tempFileWriteSuccess;
190 public boolean renameTempFile()
192 return tempFile.renameTo(file);
195 // roll the backupfiles
196 public boolean rollBackupFiles()
198 return this.rollBackupFiles(true);
201 public boolean rollBackupFiles(boolean tidyUp)
203 // file doesn't yet exist or backups are not enabled or template is null or
205 if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
206 || suffix.length() == 0)
216 dirFile = file.getParentFile();
217 dir = dirFile.getCanonicalPath();
218 } catch (Exception e)
221 "Could not get canonical path for file '" + file + "'");
224 String filename = file.getName();
225 String basename = filename;
228 // Create/move backups up one
232 // find existing backup files
233 BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
235 File[] backupFiles = dirFile.listFiles(bff);
236 int nextIndexNum = 0;
238 if (backupFiles.length == 0)
240 // No other backup files. Just need to move existing file to backupfile_1
245 TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
246 backupFiles, basename);
247 // bfTreeMap now a sorted list of <Integer index>,<File backupfile>
252 // backup style numbering
255 int tempMax = noMax ? -1 : max;
256 // noMax == true means no limits
257 // look for first "gap" in backupFiles
258 // if tempMax is -1 at this stage just keep going until there's a gap,
259 // then hopefully tempMax gets set to the right index (a positive
260 // integer so the loop breaks)...
261 // why do I feel a little uneasy about this loop?..
262 for (int i = 1; tempMax < 0 || i <= max; i++)
264 if (!bfTreeMap.containsKey(i)) // first index without existent
271 File previousFile = null;
272 File fileToBeDeleted = null;
273 for (int n = tempMax; n > 0; n--)
275 String backupfilename = dir + File.separatorChar
276 + BackupFilenameParts.getBackupFilename(n, basename,
278 File backupfile_n = new File(backupfilename);
280 if (!backupfile_n.exists())
282 // no "oldest" file to delete
283 previousFile = backupfile_n;
284 fileToBeDeleted = null;
288 // check the modification time of this (backupfile_n) and the previous
289 // file (fileToBeDeleted) if the previous file is going to be deleted
290 if (fileToBeDeleted != null)
292 File replacementFile = backupfile_n;
293 long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
294 long replacementFileLMT = replacementFile.lastModified();
298 File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
301 if (fileToBeDeletedLMT > replacementFileLMT)
303 String fileToBeDeletedLMTString = sdf
304 .format(fileToBeDeletedLMT);
305 String replacementFileLMTString = sdf
306 .format(replacementFileLMT);
307 System.out.println("WARNING! I am set to delete backupfile "
308 + fileToBeDeleted.getName()
309 + " has modification time "
310 + fileToBeDeletedLMTString
311 + " which is newer than its replacement "
312 + replacementFile.getName()
313 + " with modification time "
314 + replacementFileLMTString);
316 boolean delete = confirmNewerDeleteFile(fileToBeDeleted,
317 replacementFile, true);
321 // User has confirmed delete -- no need to add it to the list
322 fileToBeDeleted.delete();
326 fileToBeDeleted.renameTo(oldestTempFile);
331 fileToBeDeleted.renameTo(oldestTempFile);
332 addDeleteFile(oldestTempFile);
335 } catch (Exception e)
338 "Error occurred, probably making new temp file for '"
339 + fileToBeDeleted.getName() + "'");
344 fileToBeDeleted = null;
347 if (!noMax && n == tempMax && backupfile_n.exists())
349 fileToBeDeleted = backupfile_n;
353 if (previousFile != null)
355 ret = ret && backupfile_n.renameTo(previousFile);
359 previousFile = backupfile_n;
362 // index to use for the latest backup
367 // version style numbering (with earliest file deletion if max files
370 bfTreeMap.values().toArray(backupFiles);
372 // noMax == true means keep all backup files
373 if ((!noMax) && bfTreeMap.size() >= max)
375 // need to delete some files to keep number of backups to designated
377 int numToDelete = bfTreeMap.size() - max + 1;
378 // the "replacement" file is the latest backup file being kept (it's
379 // not replacing though)
380 File replacementFile = numToDelete < backupFiles.length
381 ? backupFiles[numToDelete]
383 for (int i = 0; i < numToDelete; i++)
385 // check the deletion files for modification time of the last
386 // backupfile being saved
387 File fileToBeDeleted = backupFiles[i];
388 boolean delete = true;
390 boolean newer = false;
391 if (replacementFile != null)
393 long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
394 long replacementFileLMT = replacementFile != null
395 ? replacementFile.lastModified()
397 if (fileToBeDeletedLMT > replacementFileLMT)
399 String fileToBeDeletedLMTString = sdf
400 .format(fileToBeDeletedLMT);
401 String replacementFileLMTString = sdf
402 .format(replacementFileLMT);
405 .println("WARNING! I am set to delete backupfile '"
406 + fileToBeDeleted.getName()
407 + "' has modification time "
408 + fileToBeDeletedLMTString
409 + " which is newer than the oldest backupfile being kept '"
410 + replacementFile.getName()
411 + "' with modification time "
412 + replacementFileLMTString);
414 delete = confirmNewerDeleteFile(fileToBeDeleted,
415 replacementFile, false);
418 // User has confirmed delete -- no need to add it to the list
419 fileToBeDeleted.delete();
424 // keeping file, nothing to do!
430 addDeleteFile(fileToBeDeleted);
437 nextIndexNum = bfTreeMap.lastKey() + 1;
441 // Let's make the new backup file!! yay, got there at last!
442 String latestBackupFilename = dir + File.separatorChar
443 + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
445 ret |= file.renameTo(new File(latestBackupFilename));
455 private static File nextTempFile(String filename, File dirFile)
459 COUNT: for (int i = 1; i < 1000; i++)
461 File trythis = new File(dirFile,
462 filename + '~' + Integer.toString(i));
463 if (!trythis.exists())
472 temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile);
477 private void tidyUpFiles()
482 private static boolean confirmNewerDeleteFile(File fileToBeDeleted,
483 File replacementFile, boolean replace)
485 StringBuilder messageSB = new StringBuilder();
487 File ftbd = fileToBeDeleted;
488 String ftbdLMT = sdf.format(ftbd.lastModified());
489 String ftbdSize = Long.toString(ftbd.length());
491 File rf = replacementFile;
492 String rfLMT = sdf.format(rf.lastModified());
493 String rfSize = Long.toString(rf.length());
495 int confirmButton = JvOptionPane.NO_OPTION;
498 File saveFile = null;
501 saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile());
502 } catch (Exception e)
505 "Error when confirming to keep backup file newer than other backup files.");
508 messageSB.append(MessageManager.formatMessage(
509 "label.newerdelete_replacement_line", new String[]
510 { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
512 messageSB.append("\n\n");
513 messageSB.append(MessageManager.formatMessage(
514 "label.confirm_deletion_or_rename", new String[]
515 { ftbd.getName(), saveFile.getName() }));
516 String[] options = new String[] {
517 MessageManager.getString("label.delete"),
518 MessageManager.getString("label.rename") };
520 confirmButton = JvOptionPane.showOptionDialog(Desktop.desktop,
521 messageSB.toString(),
522 MessageManager.getString("label.backupfiles_confirm_delete"),
523 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
524 null, options, options[0]);
528 messageSB.append(MessageManager
529 .formatMessage("label.newerdelete_line", new String[]
530 { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
532 messageSB.append("\n\n");
533 messageSB.append(MessageManager
534 .formatMessage("label.confirm_deletion", new String[]
535 { ftbd.getName() }));
536 String[] options = new String[] {
537 MessageManager.getString("label.delete"),
538 MessageManager.getString("label.keep") };
540 confirmButton = JvOptionPane.showOptionDialog(Desktop.desktop,
541 messageSB.toString(),
542 MessageManager.getString("label.backupfiles_confirm_delete"),
543 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
544 null, options, options[0]);
548 // return should be TRUE if file is to be deleted
549 return (confirmButton == JvOptionPane.YES_OPTION);
552 private void deleteOldFiles()
554 if (deleteFiles != null && !deleteFiles.isEmpty())
556 boolean doDelete = false;
557 StringBuilder messageSB = null;
558 if (confirmDelete && deleteFiles.size() > 0)
560 messageSB = new StringBuilder();
561 messageSB.append(MessageManager
562 .getString("label.backupfiles_confirm_delete_old_files"));
563 for (int i = 0; i < deleteFiles.size(); i++)
565 File df = deleteFiles.get(i);
566 messageSB.append("\n");
567 messageSB.append(df.getName());
568 messageSB.append(" ");
569 messageSB.append(MessageManager.formatMessage("label.file_info",
571 { sdf.format(df.lastModified()),
572 Long.toString(df.length()) }));
575 int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
576 messageSB.toString(),
578 .getString("label.backupfiles_confirm_delete"),
579 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
581 doDelete = (confirmButton == JvOptionPane.YES_OPTION);
590 for (int i = 0; i < deleteFiles.size(); i++)
592 File fileToDelete = deleteFiles.get(i);
593 fileToDelete.delete();
594 System.out.println("DELETING '" + fileToDelete.getName() + "'");
603 private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
607 // sort the backup files (based on integer found in the suffix) using a
608 // precomputed Hashmap for speed
609 Map<Integer, File> bfHashMap = new HashMap<>();
610 for (int i = 0; i < backupFiles.length; i++)
612 File f = backupFiles[i];
613 BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
615 bfHashMap.put(bfp.indexNum(), f);
617 TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
618 bfTreeMap.putAll(bfHashMap);
622 public boolean rollBackupsAndRenameTempFile()
624 boolean write = this.getWriteSuccess();
626 boolean roll = false;
627 boolean rename = false;
630 roll = this.rollBackupFiles(false);
631 rename = this.renameTempFile();
635 * Not sure that this confirmation is desirable. By this stage the new file is
636 * already written successfully, but something (e.g. disk full) has happened while
637 * trying to roll the backup files, and most likely the filename needed will already
638 * be vacant so renaming the temp file is nearly always correct!
640 boolean okay = roll && rename;
643 StringBuilder messageSB = new StringBuilder();
644 messageSB.append(MessageManager.getString( "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"));
647 if (messageSB.length() > 0)
649 messageSB.append("\n");
651 messageSB.append(MessageManager.getString(
652 "label.backupfiles_confirm_save_new_saved_file_ok"));
656 if (messageSB.length() > 0)
658 messageSB.append("\n");
660 messageSB.append(MessageManager.getString(
661 "label.backupfiles_confirm_save_new_saved_file_not_ok"));
664 int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
665 messageSB.toString(),
667 .getString("label.backupfiles_confirm_save_file"),
668 JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
669 okay = confirmButton == JvOptionPane.OK_OPTION;
679 public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
680 String fileName, String suffix, int digits)
682 File[] backupFiles = null;
684 File file = new File(fileName);
689 dirFile = file.getParentFile();
690 } catch (Exception e)
693 "Could not get canonical path for file '" + file + "'");
694 return new TreeMap<>();
697 String filename = file.getName();
698 String basename = filename;
700 // find existing backup files
701 BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
703 backupFiles = dirFile.listFiles(bff); // is clone needed?
705 // sort the backup files (based on integer found in the suffix) using a
706 // precomputed Hashmap for speed
707 Map<Integer, File> bfHashMap = new HashMap<>();
708 for (int i = 0; i < backupFiles.length; i++)
710 File f = backupFiles[i];
711 BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
713 bfHashMap.put(bfp.indexNum(), f);
715 TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
716 bfTreeMap.putAll(bfHashMap);
722 private boolean addDeleteFile(File fileToBeDeleted, File originalFile,
723 boolean delete, boolean newer)
725 return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer);
728 private boolean addDeleteFile(File fileToBeDeleted)
731 int pos = deleteFiles.indexOf(fileToBeDeleted);
738 deleteFiles.add(fileToBeDeleted);