2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import jalview.bin.Cache;
24 import jalview.gui.Desktop;
25 import jalview.gui.JvOptionPane;
26 import jalview.util.MessageManager;
29 import java.io.IOException;
30 import java.text.SimpleDateFormat;
31 import java.util.ArrayList;
32 import java.util.HashMap;
34 import java.util.TreeMap;
37 * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
38 * User configurable options are:
39 * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
40 * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM
41 * (see BackupFilesPresetEntry)
44 public class BackupFiles
47 // labels for saved params in Cache and .jalview_properties
48 public static final String NS = "BACKUPFILES";
50 public static final String ENABLED = NS + "_ENABLED";
52 public static final String NUM_PLACEHOLDER = "%n";
54 private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
56 private static final String TEMP_FILE_EXT = ".tmp";
58 // file - File object to be backed up and then updated (written over)
61 // enabled - default flag as to whether to do the backup file roll (if not
62 // defined in preferences)
63 private static boolean enabled;
65 // confirmDelete - default flag as to whether to confirm with the user before
66 // deleting old backup/version files
67 private static boolean confirmDelete;
69 // defaultSuffix - default template to use to append to basename of file
70 private String suffix;
72 // noMax - flag to turn off a maximum number of files
73 private boolean noMax;
75 // defaultMax - default max number of backup files
78 // defaultDigits - number of zero-led digits to use in the filename
81 // reverseOrder - set to true to make newest (latest) files lowest number
82 // (like rolled log files)
83 private boolean reverseOrder;
85 // temp saved file to become new saved file
86 private File tempFile;
88 // flag set to see if file save to temp file was successful
89 private boolean tempFileWriteSuccess;
91 // array of files to be deleted, with extra information
92 private ArrayList<File> deleteFiles = new ArrayList<>();
94 // date formatting for modification times
95 private static final SimpleDateFormat sdf = new SimpleDateFormat(
96 "yyyy-MM-dd HH:mm:ss");
98 public BackupFiles(String filename)
100 this(new File(filename));
103 // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and
105 public BackupFiles(File file)
109 BackupFilesPresetEntry bfpe = BackupFilesPresetEntry.getSavedBackupEntry();
110 this.suffix = bfpe.suffix;
111 this.noMax = bfpe.keepAll;
112 this.max = bfpe.rollMax;
113 this.digits = bfpe.digits;
114 this.reverseOrder = bfpe.reverse;
116 // create a temp file to save new data in
122 String tempfilename = file.getName();
123 File tempdir = file.getParentFile();
124 temp = File.createTempFile(tempfilename, TEMP_FILE_EXT + "_newfile",
129 temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
131 } catch (IOException e)
134 "Could not create temp file to save into (IOException)");
135 } catch (Exception e)
137 System.out.println("Exception ctreating temp file for saving");
139 this.setTempFile(temp);
142 public static void classInit()
144 setEnabled(Cache.getDefault(ENABLED, true));
145 BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
146 .getSavedBackupEntry();
147 setConfirmDelete(bfpe.confirmDelete);
150 public static void setEnabled(boolean flag)
155 public static boolean getEnabled()
161 public static void setConfirmDelete(boolean flag)
163 confirmDelete = flag;
166 public static boolean getConfirmDelete()
169 return confirmDelete;
172 // set, get and rename temp file into place
173 public void setTempFile(File temp)
175 this.tempFile = temp;
178 public File getTempFile()
183 public String getTempFilePath()
188 path = this.getTempFile().getCanonicalPath();
189 } catch (IOException e)
192 "IOException when getting Canonical Path of temp file '"
193 + this.getTempFile().getName() + "'");
198 public boolean setWriteSuccess(boolean flag)
200 boolean old = this.tempFileWriteSuccess;
201 this.tempFileWriteSuccess = flag;
205 public boolean getWriteSuccess()
207 return this.tempFileWriteSuccess;
210 public boolean renameTempFile()
212 return tempFile.renameTo(file);
215 // roll the backupfiles
216 public boolean rollBackupFiles()
218 return this.rollBackupFiles(true);
221 public boolean rollBackupFiles(boolean tidyUp)
223 // file doesn't yet exist or backups are not enabled or template is null or
225 if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
226 || suffix.length() == 0)
236 dirFile = file.getParentFile();
237 dir = dirFile.getCanonicalPath();
238 } catch (Exception e)
241 "Could not get canonical path for file '" + file + "'");
244 String filename = file.getName();
245 String basename = filename;
248 // Create/move backups up one
252 // find existing backup files
253 BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
255 File[] backupFiles = dirFile.listFiles(bff);
256 int nextIndexNum = 0;
258 if (backupFiles.length == 0)
260 // No other backup files. Just need to move existing file to backupfile_1
265 TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
266 backupFiles, basename);
267 // bfTreeMap now a sorted list of <Integer index>,<File backupfile>
272 // backup style numbering
275 int tempMax = noMax ? -1 : max;
276 // noMax == true means no limits
277 // look for first "gap" in backupFiles
278 // if tempMax is -1 at this stage just keep going until there's a gap,
279 // then hopefully tempMax gets set to the right index (a positive
280 // integer so the loop breaks)...
281 // why do I feel a little uneasy about this loop?..
282 for (int i = 1; tempMax < 0 || i <= max; i++)
284 if (!bfTreeMap.containsKey(i)) // first index without existent
291 File previousFile = null;
292 File fileToBeDeleted = null;
293 for (int n = tempMax; n > 0; n--)
295 String backupfilename = dir + File.separatorChar
296 + BackupFilenameParts.getBackupFilename(n, basename,
298 File backupfile_n = new File(backupfilename);
300 if (!backupfile_n.exists())
302 // no "oldest" file to delete
303 previousFile = backupfile_n;
304 fileToBeDeleted = null;
308 // check the modification time of this (backupfile_n) and the previous
309 // file (fileToBeDeleted) if the previous file is going to be deleted
310 if (fileToBeDeleted != null)
312 File replacementFile = backupfile_n;
313 long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
314 long replacementFileLMT = replacementFile.lastModified();
318 File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
321 if (fileToBeDeletedLMT > replacementFileLMT)
323 String fileToBeDeletedLMTString = sdf
324 .format(fileToBeDeletedLMT);
325 String replacementFileLMTString = sdf
326 .format(replacementFileLMT);
327 System.out.println("WARNING! I am set to delete backupfile "
328 + fileToBeDeleted.getName()
329 + " has modification time "
330 + fileToBeDeletedLMTString
331 + " which is newer than its replacement "
332 + replacementFile.getName()
333 + " with modification time "
334 + replacementFileLMTString);
336 boolean delete = confirmNewerDeleteFile(fileToBeDeleted,
337 replacementFile, true);
341 // User has confirmed delete -- no need to add it to the list
342 fileToBeDeleted.delete();
346 fileToBeDeleted.renameTo(oldestTempFile);
351 fileToBeDeleted.renameTo(oldestTempFile);
352 addDeleteFile(oldestTempFile);
355 } catch (Exception e)
358 "Error occurred, probably making new temp file for '"
359 + fileToBeDeleted.getName() + "'");
364 fileToBeDeleted = null;
367 if (!noMax && n == tempMax && backupfile_n.exists())
369 fileToBeDeleted = backupfile_n;
373 if (previousFile != null)
375 ret = ret && backupfile_n.renameTo(previousFile);
379 previousFile = backupfile_n;
382 // index to use for the latest backup
387 // version style numbering (with earliest file deletion if max files
390 bfTreeMap.values().toArray(backupFiles);
392 // noMax == true means keep all backup files
393 if ((!noMax) && bfTreeMap.size() >= max)
395 // need to delete some files to keep number of backups to designated
397 int numToDelete = bfTreeMap.size() - max + 1;
398 // the "replacement" file is the latest backup file being kept (it's
399 // not replacing though)
400 File replacementFile = numToDelete < backupFiles.length
401 ? backupFiles[numToDelete]
403 for (int i = 0; i < numToDelete; i++)
405 // check the deletion files for modification time of the last
406 // backupfile being saved
407 File fileToBeDeleted = backupFiles[i];
408 boolean delete = true;
410 boolean newer = false;
411 if (replacementFile != null)
413 long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
414 long replacementFileLMT = replacementFile != null
415 ? replacementFile.lastModified()
417 if (fileToBeDeletedLMT > replacementFileLMT)
419 String fileToBeDeletedLMTString = sdf
420 .format(fileToBeDeletedLMT);
421 String replacementFileLMTString = sdf
422 .format(replacementFileLMT);
425 .println("WARNING! I am set to delete backupfile '"
426 + fileToBeDeleted.getName()
427 + "' has modification time "
428 + fileToBeDeletedLMTString
429 + " which is newer than the oldest backupfile being kept '"
430 + replacementFile.getName()
431 + "' with modification time "
432 + replacementFileLMTString);
434 delete = confirmNewerDeleteFile(fileToBeDeleted,
435 replacementFile, false);
438 // User has confirmed delete -- no need to add it to the list
439 fileToBeDeleted.delete();
444 // keeping file, nothing to do!
450 addDeleteFile(fileToBeDeleted);
457 nextIndexNum = bfTreeMap.lastKey() + 1;
461 // Let's make the new backup file!! yay, got there at last!
462 String latestBackupFilename = dir + File.separatorChar
463 + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
465 ret |= file.renameTo(new File(latestBackupFilename));
475 private static File nextTempFile(String filename, File dirFile)
479 COUNT: for (int i = 1; i < 1000; i++)
481 File trythis = new File(dirFile,
482 filename + '~' + Integer.toString(i));
483 if (!trythis.exists())
492 temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile);
497 private void tidyUpFiles()
502 private static boolean confirmNewerDeleteFile(File fileToBeDeleted,
503 File replacementFile, boolean replace)
505 StringBuilder messageSB = new StringBuilder();
507 File ftbd = fileToBeDeleted;
508 String ftbdLMT = sdf.format(ftbd.lastModified());
509 String ftbdSize = Long.toString(ftbd.length());
511 File rf = replacementFile;
512 String rfLMT = sdf.format(rf.lastModified());
513 String rfSize = Long.toString(rf.length());
515 int confirmButton = JvOptionPane.NO_OPTION;
518 File saveFile = null;
521 saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile());
522 } catch (Exception e)
525 "Error when confirming to keep backup file newer than other backup files.");
528 messageSB.append(MessageManager.formatMessage(
529 "label.newerdelete_replacement_line", new String[]
530 { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
532 messageSB.append("\n\n");
533 messageSB.append(MessageManager.formatMessage(
534 "label.confirm_deletion_or_rename", new String[]
535 { ftbd.getName(), saveFile.getName() }));
536 String[] options = new String[] {
537 MessageManager.getString("label.delete"),
538 MessageManager.getString("label.rename") };
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 messageSB.append(MessageManager
549 .formatMessage("label.newerdelete_line", new String[]
550 { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
552 messageSB.append("\n\n");
553 messageSB.append(MessageManager
554 .formatMessage("label.confirm_deletion", new String[]
555 { ftbd.getName() }));
556 String[] options = new String[] {
557 MessageManager.getString("label.delete"),
558 MessageManager.getString("label.keep") };
560 confirmButton = JvOptionPane.showOptionDialog(Desktop.desktop,
561 messageSB.toString(),
562 MessageManager.getString("label.backupfiles_confirm_delete"),
563 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
564 null, options, options[0]);
568 // return should be TRUE if file is to be deleted
569 return (confirmButton == JvOptionPane.YES_OPTION);
572 private void deleteOldFiles()
574 if (deleteFiles != null && !deleteFiles.isEmpty())
576 boolean doDelete = false;
577 StringBuilder messageSB = null;
578 if (confirmDelete && deleteFiles.size() > 0)
580 messageSB = new StringBuilder();
581 messageSB.append(MessageManager
582 .getString("label.backupfiles_confirm_delete_old_files"));
583 for (int i = 0; i < deleteFiles.size(); i++)
585 File df = deleteFiles.get(i);
586 messageSB.append("\n");
587 messageSB.append(df.getName());
588 messageSB.append(" ");
589 messageSB.append(MessageManager.formatMessage("label.file_info",
591 { sdf.format(df.lastModified()),
592 Long.toString(df.length()) }));
595 int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
596 messageSB.toString(),
598 .getString("label.backupfiles_confirm_delete"),
599 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
601 doDelete = (confirmButton == JvOptionPane.YES_OPTION);
610 for (int i = 0; i < deleteFiles.size(); i++)
612 File fileToDelete = deleteFiles.get(i);
613 fileToDelete.delete();
614 System.out.println("DELETING '" + fileToDelete.getName() + "'");
623 private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
627 // sort the backup files (based on integer found in the suffix) using a
628 // precomputed Hashmap for speed
629 Map<Integer, File> bfHashMap = new HashMap<>();
630 for (int i = 0; i < backupFiles.length; i++)
632 File f = backupFiles[i];
633 BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
635 bfHashMap.put(bfp.indexNum(), f);
637 TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
638 bfTreeMap.putAll(bfHashMap);
642 public boolean rollBackupsAndRenameTempFile()
644 boolean write = this.getWriteSuccess();
646 boolean roll = false;
647 boolean rename = false;
650 roll = this.rollBackupFiles(false);
651 rename = this.renameTempFile();
655 * Not sure that this confirmation is desirable. By this stage the new file is
656 * already written successfully, but something (e.g. disk full) has happened while
657 * trying to roll the backup files, and most likely the filename needed will already
658 * be vacant so renaming the temp file is nearly always correct!
660 boolean okay = roll && rename;
663 StringBuilder messageSB = new StringBuilder();
664 messageSB.append(MessageManager.getString( "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"));
667 if (messageSB.length() > 0)
669 messageSB.append("\n");
671 messageSB.append(MessageManager.getString(
672 "label.backupfiles_confirm_save_new_saved_file_ok"));
676 if (messageSB.length() > 0)
678 messageSB.append("\n");
680 messageSB.append(MessageManager.getString(
681 "label.backupfiles_confirm_save_new_saved_file_not_ok"));
684 int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
685 messageSB.toString(),
687 .getString("label.backupfiles_confirm_save_file"),
688 JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
689 okay = confirmButton == JvOptionPane.OK_OPTION;
699 public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
700 String fileName, String suffix, int digits)
702 File[] backupFiles = null;
704 File file = new File(fileName);
709 dirFile = file.getParentFile();
710 } catch (Exception e)
713 "Could not get canonical path for file '" + file + "'");
714 return new TreeMap<>();
717 String filename = file.getName();
718 String basename = filename;
720 // find existing backup files
721 BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
723 backupFiles = dirFile.listFiles(bff); // is clone needed?
725 // sort the backup files (based on integer found in the suffix) using a
726 // precomputed Hashmap for speed
727 Map<Integer, File> bfHashMap = new HashMap<>();
728 for (int i = 0; i < backupFiles.length; i++)
730 File f = backupFiles[i];
731 BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
733 bfHashMap.put(bfp.indexNum(), f);
735 TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
736 bfTreeMap.putAll(bfHashMap);
742 private boolean addDeleteFile(File fileToBeDeleted, File originalFile,
743 boolean delete, boolean newer)
745 return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer);
748 private boolean addDeleteFile(File fileToBeDeleted)
751 int pos = deleteFiles.indexOf(fileToBeDeleted);
758 deleteFiles.add(fileToBeDeleted);