ff23cbf2e81841eb16712769bbf32b68e1227696
[jalview.git] / src / jalview / io / BackupFiles.java
1 package jalview.io;
2
3 import jalview.bin.Cache;
4 import jalview.gui.Desktop;
5 import jalview.gui.JvOptionPane;
6 import jalview.util.MessageManager;
7
8 import java.io.File;
9 import java.io.IOException;
10 import java.util.Arrays;
11 import java.util.HashMap;
12 import java.util.TreeMap;
13
14 /*
15  * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
16  * User configurable options are:
17  * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
18  * BACKUPFILES_SUFFIX - a template to insert just before the file extension.  Use '%n' to be replaced by a 0-led SUFFIX_DIGITS long integer.
19  * BACKUPFILES_ROLL_MAX - the maximum number of backupfiles to keep for any one alignment or project file.
20  * BACKUPFILES_SUFFIX_DIGITS - the number of digits to insert replace %n with (e.g. BACKUPFILES_SUFFIX_DIGITS = 3 would make "001", "002", etc)
21  * 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. 
22  * BACKUPFILES_CONFIRM_DELETE_OLD - if true then prompt/confirm with the user when deleting older backup/version files.
23  */
24
25 public class BackupFiles
26 {
27
28   // labels for saved params in Cache and .jalview_properties
29   private static String NS = "BACKUPFILES";
30
31   public static String ENABLED = NS + "_ENABLED";
32
33   public static String SUFFIX = NS + "_SUFFIX";
34
35   public static String ROLL_MAX = NS + "_ROLL_MAX";
36
37   public static String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS";
38
39   protected static String NUM_PLACEHOLDER = "%n";
40
41   public static String REVERSE_ORDER = NS + "_REVERSE_ORDER";
42
43   public static String CONFIRM_DELETE_OLD = NS + "_CONFIRM_DELETE_OLD";
44
45   private static String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
46
47   // file - File object to be backed up and then updated (written over)
48   private File file;
49
50   // enabled - default flag as to whether to do the backup file roll (if not
51   // defined in preferences)
52   private static boolean enabled;
53
54   // confirmDelete - default flag as to whether to confirm with the user before
55   // deleting old backup/version files
56   private static boolean confirmDelete;
57
58   private static boolean classInit = false;
59
60   // defaultSuffix - default template to use to append to basename of file
61   private String suffix;
62
63   // defaultMax - default max number of backup files
64   private int max;
65
66   // defaultDigits - number of zero-led digits to use in the filename
67   private int digits;
68
69   // reverseOrder - set to true to make newest (latest) files lowest number
70   // (like rolled log files)
71   private boolean reverseOrder;
72
73   // temp saved file to become new saved file
74   private File tempFile;
75
76   // flag set to see if file save to temp file was successful
77   private boolean tempFileWriteSuccess;
78
79   public BackupFiles(String filename)
80   {
81     this(new File(filename));
82   }
83
84   // first time defaults for SUFFIX, ROLL_MAX, SUFFIX_DIGITS and REVERSE_ORDER
85   public BackupFiles(File file)
86   {
87     this(file, "-v" + NUM_PLACEHOLDER, 4, 3, false);
88   }
89
90   public BackupFiles(File file,
91           String defaultSuffix, int defaultMax, int defaultDigits,
92           boolean defaultReverseOrder)
93   {
94     classInit();
95     this.file = file;
96     this.suffix = Cache.getDefault(SUFFIX, defaultSuffix);
97     this.max = Cache.getDefault(ROLL_MAX, defaultMax);
98     this.digits = Cache.getDefault(SUFFIX_DIGITS, defaultDigits);
99     this.reverseOrder = Cache.getDefault(REVERSE_ORDER,
100             defaultReverseOrder);
101
102     // create a temp file to save new data in
103     File temp = null;
104     try
105     {
106       if (file != null)
107       {
108         String tempfilename = file.getName();
109         File tempdir = file.getParentFile();
110         temp = File.createTempFile(tempfilename, ".tmp", tempdir);
111       }
112       else
113       {
114         temp = File.createTempFile(DEFAULT_TEMP_FILE, ".tmp");
115       }
116     } catch (IOException e)
117     {
118       System.out.println(
119               "Could not create temp file to save into (IOException)");
120     } catch (Exception e)
121     {
122       System.out.println("Exception ctreating temp file for saving");
123     }
124     this.setTempFile(temp);
125   }
126
127   public static void classInit()
128   {
129     if (!classInit)
130     {
131       setEnabled(Cache.getDefault(ENABLED, true));
132       setConfirmDelete(Cache.getDefault(CONFIRM_DELETE_OLD, true));
133       classInit = true;
134     }
135   }
136
137   public static void setEnabled(boolean flag)
138   {
139     enabled = flag;
140   }
141
142   public static boolean getEnabled()
143   {
144     classInit();
145     return enabled;
146   }
147
148   public static void setConfirmDelete(boolean flag)
149   {
150     confirmDelete = flag;
151   }
152
153   public static boolean getConfirmDelete()
154   {
155     classInit();
156     return confirmDelete;
157   }
158
159   // set, get and rename temp file into place
160   public void setTempFile(File temp)
161   {
162     this.tempFile = temp;
163   }
164
165   public File getTempFile()
166   {
167     return tempFile;
168   }
169
170   public String getTempFilePath()
171   {
172     String path = null;
173     try
174     {
175       path = this.getTempFile().getCanonicalPath();
176     } catch (IOException e)
177     {
178       System.out.println(
179               "IOException when getting Canonical Path of temp file '"
180                       + this.getTempFile().getName() + "'");
181     }
182     return path;
183   }
184
185   public boolean setWriteSuccess(boolean flag)
186   {
187     boolean old = this.tempFileWriteSuccess;
188     this.tempFileWriteSuccess = flag;
189     return old;
190   }
191
192   public boolean getWriteSuccess()
193   {
194     return this.tempFileWriteSuccess;
195   }
196
197   public boolean renameTempFile()
198   {
199     return tempFile.renameTo(file);
200   }
201
202
203   // roll the backupfiles
204   public boolean rollBackupFiles()
205   {
206
207     // file doesn't yet exist or backups are not enabled
208     if ((!file.exists()) || (!enabled) || (max < -1))
209     {
210       // nothing to do
211       return true;
212     }
213
214     // split filename up to insert suffix template in the right place. template
215     // and backupMax can be set in .jalview_properties
216     String dir = "";
217     File dirFile;
218     try
219     {
220       dirFile = file.getParentFile();
221       dir = dirFile.getCanonicalPath();
222     } catch (Exception e)
223     {
224       System.out.println(
225               "Could not get canonical path for file '" + file + "'");
226       return false;
227     }
228     String filename = file.getName();
229     String basename = filename;
230     String extension = "";
231     int dotcharpos = filename.lastIndexOf('.');
232     // don't split of filenames with the last '.' at the very beginning or
233     // very end of the filename
234     if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1))
235     {
236       basename = filename.substring(0, dotcharpos);
237       extension = filename.substring(dotcharpos); // NOTE this includes the '.'
238     }
239
240     boolean ret = true;
241     // Create/move backups up one
242
243     File[] oldFilesToDelete = null;
244     
245     // find existing backup files
246     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
247             digits,
248             extension);
249     File[] backupFiles = dirFile.listFiles(bff);
250     int nextIndexNum = 0;
251     String confirmDeleteExtraInfo = null;
252     
253     if (backupFiles.length == 0)
254     {
255       // No other backup files. Just need to move existing file to backupfile_1
256       nextIndexNum = 1;
257     }
258     else
259     {
260
261       // sort the backup files (based on integer found in the suffix) using a
262       // precomputed Hashmap for speed
263       HashMap<Integer, File> bfHashMap = new HashMap<>();
264       for (int i = 0; i < backupFiles.length; i++)
265       {
266         File f = backupFiles[i];
267         BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, digits, extension);
268         bfHashMap.put(bfp.indexNum(), f);
269       }
270       TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
271       bfTreeMap.putAll(bfHashMap);
272
273       if (reverseOrder)
274       {
275         // backup style numbering
276
277         File lastfile = null;
278         int tempMax = max;
279         // max == -1 means no limits
280         // look for first "gap" in backupFiles
281         // if tempMax is -1 at this stage just keep going until there's a gap...
282         // why do I feel a little uneasy about this loop?..
283         for (int i = 1; tempMax < 0 || (max >= 0 && i <= max); i++)
284         {
285           if (!bfTreeMap.containsKey(i)) // first non-existent backupfile
286           {
287             tempMax = i;
288           }
289         }
290
291         for (int m = 0; m < tempMax; m++)
292         {
293           int n = tempMax - m;
294           String backupfilename = dir + File.separatorChar
295                   + BackupFilenameParts.getBackupFilename(n, basename,
296                           suffix, digits, extension);
297           File backupfile_n = new File(backupfilename);
298
299           if (!backupfile_n.exists())
300           {
301             lastfile = backupfile_n;
302             continue;
303           }
304
305           if (m == 0 && backupfile_n.exists())
306           {
307             // move the largest (max) rolled file to a temp file and add to the delete list
308             try
309             {
310               File temp = File.createTempFile(backupfilename, ".tmp",
311                     dirFile);
312               backupfile_n.renameTo(temp);
313
314               oldFilesToDelete = new File[] { temp };
315               confirmDeleteExtraInfo = "(was " + backupfile_n.getName()
316                       + ")";
317             } catch (IOException e)
318             {
319               System.out.println(
320                       "IOException when creating temporary file for backupfilename");
321             }
322           }
323           else
324           {
325             // Just In Case
326             if (lastfile != null)
327             {
328               ret = ret && backupfile_n.renameTo(lastfile);
329             }
330           }
331
332           lastfile = backupfile_n;
333         }
334
335         // index to use for the latest backup
336         nextIndexNum = 1;
337       }
338       else
339       {
340         // version style numbering (with earliest file deletion if max files
341         // reached)
342
343
344         bfTreeMap.values().toArray(backupFiles);
345
346         // max value of -1 means keep all backup files
347         if (bfTreeMap.size() >= max && max != -1)
348         {
349           // need to delete some files to keep number of backups to designated
350           // max
351           int numToDelete = bfTreeMap.size() - max + 1;
352           oldFilesToDelete = Arrays.copyOfRange(backupFiles, 0,
353                   numToDelete);
354
355         }
356
357         nextIndexNum = bfTreeMap.lastKey() + 1;
358
359       }
360     }
361
362     if (oldFilesToDelete != null && oldFilesToDelete.length > 0)
363     {
364       // delete old backup/version files
365
366       boolean delete = false;
367       if (confirmDelete)
368       {
369         // Object[] confirmMessageArray = {};
370         StringBuilder confirmMessage = new StringBuilder();
371         confirmMessage.append(MessageManager
372                 .getString("label.backupfiles_confirm_delete_old_files"));
373         for (File f : oldFilesToDelete)
374         {
375           confirmMessage.append("\n");
376           confirmMessage.append(f.getName());
377         }
378         if (confirmDeleteExtraInfo != null
379                 && confirmDeleteExtraInfo.length() > 0)
380         {
381           confirmMessage.append("\n");
382           confirmMessage.append(confirmDeleteExtraInfo);
383         }
384         int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
385                 confirmMessage.toString(),
386                 MessageManager
387                         .getString("label.backupfiles_confirm_delete"),
388                 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
389
390         delete = (confirm == JvOptionPane.YES_OPTION);
391       }
392       else
393       {
394         delete = true;
395       }
396
397       if (delete)
398       {
399         for (int i = 0; i < oldFilesToDelete.length; i++)
400         {
401           File fileToDelete = oldFilesToDelete[i];
402           fileToDelete.delete();
403           // System.out.println("DELETING '" + fileToDelete.getName() +
404           // "'");
405         }
406       }
407
408     }
409
410     // Let's make the new backup file!! yay, got there at last!
411     String latestBackupFilename = dir + File.separatorChar
412             + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
413                     suffix, digits, extension);
414     File latestBackupFile = new File(latestBackupFilename);
415     ret = ret && file.renameTo(latestBackupFile);
416
417     return ret;
418   }
419
420   public boolean rollBackupsAndRenameTempFile()
421   {
422     boolean write = this.getWriteSuccess();
423     
424     boolean roll = false;
425     if (write) {
426       roll = this.rollBackupFiles();
427     } else {
428       return false;
429     }
430     
431     /*
432      * Not sure that this confirmation is desirable.  By this stage the new file is
433      * already written successfully, but something (e.g. disk full) has happened while 
434      * trying to roll the backup files, and most likely the filename needed will already
435      * be vacant so renaming the temp file is nearly always correct!
436      */
437     if (!roll)
438     {
439       int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
440               MessageManager.getString(
441                       "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"),
442               MessageManager.getString("label.backupfiles_confirm_save_file"),
443               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
444
445       if (confirm == JvOptionPane.YES_OPTION)
446       {
447         roll = true;
448       }
449     }
450
451     boolean rename = false;
452     if (roll)
453     {
454       rename = this.renameTempFile();
455     }
456
457     return rename;
458   }
459
460 }
461