Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
BackupFiles | 56 | 348 | 116 |
1 | /* | |
2 | * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) | |
3 | * Copyright (C) $$Year-Rel$$ The Jalview Authors | |
4 | * | |
5 | * This file is part of Jalview. | |
6 | * | |
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. | |
11 | * | |
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. | |
16 | * | |
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. | |
20 | */ | |
21 | package jalview.io; | |
22 | ||
23 | import java.io.File; | |
24 | import java.io.IOException; | |
25 | import java.nio.file.Files; | |
26 | import java.nio.file.Path; | |
27 | import java.nio.file.Paths; | |
28 | import java.nio.file.StandardCopyOption; | |
29 | import java.text.SimpleDateFormat; | |
30 | import java.util.ArrayList; | |
31 | import java.util.HashMap; | |
32 | import java.util.List; | |
33 | import java.util.Map; | |
34 | import java.util.TreeMap; | |
35 | import java.util.concurrent.CompletableFuture; | |
36 | import java.util.concurrent.ExecutionException; | |
37 | import java.util.concurrent.ExecutorService; | |
38 | import java.util.concurrent.Executors; | |
39 | import java.util.concurrent.Future; | |
40 | ||
41 | import jalview.bin.Cache; | |
42 | import jalview.bin.Console; | |
43 | import jalview.gui.Desktop; | |
44 | import jalview.gui.JvOptionPane; | |
45 | import jalview.util.MessageManager; | |
46 | import jalview.util.Platform; | |
47 | ||
48 | /* | |
49 | * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved. | |
50 | * User configurable options are: | |
51 | * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved. | |
52 | * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM | |
53 | * (see BackupFilesPresetEntry) | |
54 | */ | |
55 | ||
56 | public class BackupFiles | |
57 | { | |
58 | ||
59 | // labels for saved params in Cache and .jalview_properties | |
60 | public static final String NS = "BACKUPFILES"; | |
61 | ||
62 | public static final String ENABLED = NS + "_ENABLED"; | |
63 | ||
64 | public static final String NUM_PLACEHOLDER = "%n"; | |
65 | ||
66 | private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS; | |
67 | ||
68 | private static final String TEMP_FILE_EXT = ".tmp"; | |
69 | ||
70 | // file - File object to be backed up and then updated (written over) | |
71 | private File file; | |
72 | ||
73 | // enabled - default flag as to whether to do the backup file roll (if not | |
74 | // defined in preferences) | |
75 | private static boolean enabled; | |
76 | ||
77 | // confirmDelete - default flag as to whether to confirm with the user before | |
78 | // deleting old backup/version files | |
79 | private static boolean confirmDelete; | |
80 | ||
81 | // defaultSuffix - default template to use to append to basename of file | |
82 | private String suffix; | |
83 | ||
84 | // noMax - flag to turn off a maximum number of files | |
85 | private boolean noMax; | |
86 | ||
87 | // defaultMax - default max number of backup files | |
88 | private int max; | |
89 | ||
90 | // defaultDigits - number of zero-led digits to use in the filename | |
91 | private int digits; | |
92 | ||
93 | // reverseOrder - set to true to make newest (latest) files lowest number | |
94 | // (like rolled log files) | |
95 | private boolean reverseOrder; | |
96 | ||
97 | // temp saved file to become new saved file | |
98 | private File tempFile; | |
99 | ||
100 | // flag set to see if file save to temp file was successful | |
101 | private boolean tempFileWriteSuccess; | |
102 | ||
103 | // array of files to be deleted, with extra information | |
104 | private ArrayList<File> deleteFiles = new ArrayList<>(); | |
105 | ||
106 | // date formatting for modification times | |
107 | private static final SimpleDateFormat sdf = new SimpleDateFormat( | |
108 | "yyyy-MM-dd HH:mm:ss"); | |
109 | ||
110 | private static final String newTempFileSuffix = "_newfile"; | |
111 | ||
112 | private static final String oldTempFileSuffix = "_oldfile_tobedeleted"; | |
113 | ||
114 | // thread pool used for completablefutures | |
115 | private static final ExecutorService executorService = Executors | |
116 | .newFixedThreadPool(3); | |
117 | ||
118 | private static List<BackupFiles> savesInProgress = new ArrayList<>(); | |
119 | ||
120 | private CompletableFuture<Boolean> myFuture = null; | |
121 | ||
122 | 100 | private boolean addSaveInProgress() |
123 | { | |
124 | 100 | if (savesInProgress.contains(this)) |
125 | { | |
126 | 0 | return false; |
127 | } | |
128 | else | |
129 | { | |
130 | 100 | this.setMyFuture(); |
131 | 100 | savesInProgress.add(this); |
132 | 100 | return true; |
133 | } | |
134 | } | |
135 | ||
136 | 100 | private boolean removeSaveInProgress(boolean ret) |
137 | { | |
138 | 100 | if (savesInProgress.contains(this)) |
139 | { | |
140 | 100 | this.getMyFuture().complete(ret); |
141 | // remove all occurrences | |
142 | 200 | while (savesInProgress.remove(this)) |
143 | { | |
144 | } | |
145 | 100 | return true; |
146 | } | |
147 | 0 | return false; |
148 | } | |
149 | ||
150 | 100 | private static CompletableFuture<Boolean> getNewFuture() |
151 | { | |
152 | 100 | return new CompletableFuture<Boolean>() |
153 | { | |
154 | }; | |
155 | } | |
156 | ||
157 | 149 | private CompletableFuture<Boolean> getMyFuture() |
158 | { | |
159 | 149 | return this.myFuture; |
160 | } | |
161 | ||
162 | 100 | private void setMyFuture() |
163 | { | |
164 | 100 | this.myFuture = getNewFuture(); |
165 | } | |
166 | ||
167 | 24 | public static boolean hasSavesInProgress() |
168 | { | |
169 | 24 | boolean has = false; |
170 | 24 | for (CompletableFuture cf : savesInProgressCompletableFutures(true)) |
171 | { | |
172 | 27 | has |= !cf.isDone(); |
173 | } | |
174 | 24 | return has; |
175 | } | |
176 | ||
177 | 4 | public static List<File> savesInProgressFiles(boolean all) |
178 | { | |
179 | 4 | List<File> files = new ArrayList<>(); |
180 | 4 | for (BackupFiles bfile : savesInProgress) |
181 | { | |
182 | 6 | if (all || !bfile.getMyFuture().isDone()) |
183 | 4 | files.add(bfile.getFile()); |
184 | } | |
185 | 4 | return files; |
186 | } | |
187 | ||
188 | 31 | public static List<CompletableFuture<Boolean>> savesInProgressCompletableFutures( |
189 | boolean all) | |
190 | { | |
191 | 31 | List<CompletableFuture<Boolean>> cfs = new ArrayList<>(); |
192 | 31 | for (BackupFiles bfile : savesInProgress) |
193 | { | |
194 | 36 | if (all || !bfile.getMyFuture().isDone()) |
195 | 34 | cfs.add(bfile.getMyFuture()); |
196 | } | |
197 | 31 | return cfs; |
198 | } | |
199 | ||
200 | 0 | public static Future<Boolean> allSaved() |
201 | { | |
202 | 0 | CompletableFuture<Boolean> f = new CompletableFuture<>(); |
203 | ||
204 | 0 | executorService.submit(() -> { |
205 | 0 | for (BackupFiles buf : savesInProgress) |
206 | { | |
207 | 0 | boolean allSaved = true; |
208 | 0 | try |
209 | { | |
210 | 0 | allSaved &= buf.getMyFuture().get(); |
211 | } catch (InterruptedException e) | |
212 | { | |
213 | 0 | Console.debug("InterruptedException waiting for files to save", |
214 | e); | |
215 | } catch (ExecutionException e) | |
216 | { | |
217 | 0 | Console.debug("ExecutionException waiting for files to save", e); |
218 | } | |
219 | 0 | f.complete(allSaved); |
220 | } | |
221 | }); | |
222 | 0 | return f; |
223 | } | |
224 | ||
225 | 98 | public BackupFiles(String filename) |
226 | { | |
227 | 98 | this(new File(filename)); |
228 | } | |
229 | ||
230 | // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and | |
231 | // REVERSE_ORDER | |
232 | 100 | public BackupFiles(File file) |
233 | { | |
234 | 100 | classInit(); |
235 | 100 | if (file.getParentFile() == null) |
236 | { | |
237 | // filename probably in pwd represented with no parent -- fix it before | |
238 | // it's a problem | |
239 | 0 | file = file.getAbsoluteFile(); |
240 | } | |
241 | 100 | this.file = file; |
242 | ||
243 | // add this file from the save in progress stack | |
244 | 100 | addSaveInProgress(); |
245 | ||
246 | 100 | BackupFilesPresetEntry bfpe = BackupFilesPresetEntry |
247 | .getSavedBackupEntry(); | |
248 | 100 | this.suffix = bfpe.suffix; |
249 | 100 | this.noMax = bfpe.keepAll; |
250 | 100 | this.max = bfpe.rollMax; |
251 | 100 | this.digits = bfpe.digits; |
252 | 100 | this.reverseOrder = bfpe.reverse; |
253 | ||
254 | // create a temp file to save new data in | |
255 | 100 | File temp = null; |
256 | 100 | try |
257 | { | |
258 | 100 | if (file != null) |
259 | { | |
260 | 100 | String tempfilename = file.getName(); |
261 | 100 | File tempdir = file.getAbsoluteFile().getParentFile(); |
262 | 100 | tempdir.mkdirs(); |
263 | 100 | Console.trace( |
264 | "BACKUPFILES [file!=null] attempting to create temp file for " | |
265 | + tempfilename + " in dir " + tempdir); | |
266 | 100 | temp = File.createTempFile(tempfilename, |
267 | TEMP_FILE_EXT + newTempFileSuffix, tempdir); | |
268 | 100 | Console.debug( |
269 | "BACKUPFILES using temp file " + temp.getAbsolutePath()); | |
270 | } | |
271 | else | |
272 | { | |
273 | 0 | Console.trace( |
274 | "BACKUPFILES [file==null] attempting to create default temp file " | |
275 | + DEFAULT_TEMP_FILE + " with extension " | |
276 | + TEMP_FILE_EXT); | |
277 | 0 | temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT); |
278 | } | |
279 | } catch (IOException e) | |
280 | { | |
281 | 0 | Console.error("Could not create temp file to save to (IOException)"); |
282 | 0 | Console.error(e.getMessage()); |
283 | 0 | Console.debug(Cache.getStackTraceString(e)); |
284 | } catch (Exception e) | |
285 | { | |
286 | 0 | Console.error("Exception creating temp file for saving"); |
287 | 0 | Console.debug(Cache.getStackTraceString(e)); |
288 | } | |
289 | 100 | this.setTempFile(temp); |
290 | } | |
291 | ||
292 | 178 | private static void classInit() |
293 | { | |
294 | 178 | Console.initLogger(); |
295 | 178 | Console.trace("BACKUPFILES classInit"); |
296 | 178 | boolean e = Cache.getDefault(ENABLED, !Platform.isJS()); |
297 | 178 | setEnabled(e); |
298 | 178 | Console.trace("BACKUPFILES " + (e ? "enabled" : "disabled")); |
299 | 178 | BackupFilesPresetEntry bfpe = BackupFilesPresetEntry |
300 | .getSavedBackupEntry(); | |
301 | 178 | Console.trace("BACKUPFILES preset scheme " + bfpe.toString()); |
302 | 178 | setConfirmDelete(bfpe.confirmDelete); |
303 | 178 | Console.trace("BACKUPFILES confirm delete " + bfpe.confirmDelete); |
304 | } | |
305 | ||
306 | 178 | public static void setEnabled(boolean flag) |
307 | { | |
308 | 178 | enabled = flag; |
309 | } | |
310 | ||
311 | 78 | public static boolean getEnabled() |
312 | { | |
313 | 78 | classInit(); |
314 | 78 | return enabled; |
315 | } | |
316 | ||
317 | 178 | public static void setConfirmDelete(boolean flag) |
318 | { | |
319 | 178 | confirmDelete = flag; |
320 | } | |
321 | ||
322 | 0 | public static boolean getConfirmDelete() |
323 | { | |
324 | 0 | classInit(); |
325 | 0 | return confirmDelete; |
326 | } | |
327 | ||
328 | // set, get and rename temp file into place | |
329 | 100 | public void setTempFile(File temp) |
330 | { | |
331 | 100 | this.tempFile = temp; |
332 | } | |
333 | ||
334 | 192 | public File getTempFile() |
335 | { | |
336 | 192 | return tempFile; |
337 | } | |
338 | ||
339 | 190 | public String getTempFilePath() |
340 | { | |
341 | 190 | String path = null; |
342 | 190 | try |
343 | { | |
344 | 190 | path = this.getTempFile().getCanonicalPath(); |
345 | } catch (IOException e) | |
346 | { | |
347 | 0 | Console.error("IOException when getting Canonical Path of temp file '" |
348 | + this.getTempFile().getName() + "'"); | |
349 | 0 | Console.debug(Cache.getStackTraceString(e)); |
350 | } | |
351 | 190 | return path; |
352 | } | |
353 | ||
354 | 100 | public boolean setWriteSuccess(boolean flag) |
355 | { | |
356 | 100 | boolean old = this.tempFileWriteSuccess; |
357 | 100 | this.tempFileWriteSuccess = flag; |
358 | 100 | return old; |
359 | } | |
360 | ||
361 | 100 | public boolean getWriteSuccess() |
362 | { | |
363 | 100 | return this.tempFileWriteSuccess; |
364 | } | |
365 | ||
366 | 100 | public boolean renameTempFile() |
367 | { | |
368 | 100 | return moveFileToFile(tempFile, file); |
369 | } | |
370 | ||
371 | // roll the backupfiles | |
372 | 0 | public boolean rollBackupFiles() |
373 | { | |
374 | 0 | return this.rollBackupFiles(true); |
375 | } | |
376 | ||
377 | 100 | public boolean rollBackupFiles(boolean tidyUp) |
378 | { | |
379 | // file doesn't yet exist or backups are not enabled or template is null or | |
380 | // empty | |
381 | 100 | if ((!file.exists()) || (!enabled) || max < 0 || suffix == null |
382 | || suffix.length() == 0) | |
383 | { | |
384 | // nothing to do | |
385 | 62 | Console.debug("BACKUPFILES rollBackupFiles nothing to do." + ", " |
386 | 62 | + "filename: " + (file != null ? file.getName() : "null") |
387 | + ", " + "file exists: " + file.exists() + ", " + "enabled: " | |
388 | + enabled + ", " + "max: " + max + ", " + "suffix: '" + suffix | |
389 | + "'"); | |
390 | 62 | return true; |
391 | } | |
392 | ||
393 | 38 | Console.trace("BACKUPFILES rollBackupFiles starting"); |
394 | ||
395 | 38 | String dir = ""; |
396 | 38 | File dirFile; |
397 | 38 | try |
398 | { | |
399 | 38 | dirFile = file.getParentFile(); |
400 | 38 | dir = dirFile.getCanonicalPath(); |
401 | 38 | Console.trace("BACKUPFILES dir: " + dir); |
402 | } catch (Exception e) | |
403 | { | |
404 | 0 | Console.error("Could not get canonical path for file '" + file + "'"); |
405 | 0 | Console.error(e.getMessage()); |
406 | 0 | Console.debug(Cache.getStackTraceString(e)); |
407 | 0 | return false; |
408 | } | |
409 | 38 | String filename = file.getName(); |
410 | 38 | String basename = filename; |
411 | ||
412 | 38 | Console.trace("BACKUPFILES filename is " + filename); |
413 | 38 | boolean ret = true; |
414 | // Create/move backups up one | |
415 | ||
416 | 38 | deleteFiles.clear(); |
417 | ||
418 | // find existing backup files | |
419 | 38 | BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, |
420 | digits); | |
421 | 38 | File[] backupFiles = dirFile.listFiles(bff); |
422 | 38 | int nextIndexNum = 0; |
423 | ||
424 | 38 | Console.trace("BACKUPFILES backupFiles.length: " + backupFiles.length); |
425 | 38 | if (backupFiles.length == 0) |
426 | { | |
427 | // No other backup files. Just need to move existing file to backupfile_1 | |
428 | 10 | Console.trace( |
429 | "BACKUPFILES no existing backup files, setting index to 1"); | |
430 | 10 | nextIndexNum = 1; |
431 | } | |
432 | else | |
433 | { | |
434 | 28 | TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap( |
435 | backupFiles, basename); | |
436 | // bfTreeMap now a sorted list of <Integer index>,<File backupfile> | |
437 | // mappings | |
438 | ||
439 | 28 | if (reverseOrder) |
440 | { | |
441 | // backup style numbering | |
442 | 9 | Console.trace("BACKUPFILES rolling files in reverse order"); |
443 | ||
444 | 9 | int tempMax = noMax ? -1 : max; |
445 | // noMax == true means no limits | |
446 | // look for first "gap" in backupFiles | |
447 | // if tempMax is -1 at this stage just keep going until there's a gap, | |
448 | // then hopefully tempMax gets set to the right index (a positive | |
449 | // integer so the loop breaks)... | |
450 | // why do I feel a little uneasy about this loop?.. | |
451 | 27 | for (int i = 1; tempMax < 0 || i <= max; i++) |
452 | { | |
453 | 18 | if (!bfTreeMap.containsKey(i)) // first index without existent |
454 | // backupfile | |
455 | { | |
456 | 1 | tempMax = i; |
457 | } | |
458 | } | |
459 | ||
460 | 9 | File previousFile = null; |
461 | 9 | File fileToBeDeleted = null; |
462 | 27 | for (int n = tempMax; n > 0; n--) |
463 | { | |
464 | 18 | String backupfilename = dir + File.separatorChar |
465 | + BackupFilenameParts.getBackupFilename(n, basename, | |
466 | suffix, digits); | |
467 | 18 | File backupfile_n = new File(backupfilename); |
468 | ||
469 | 18 | if (!backupfile_n.exists()) |
470 | { | |
471 | // no "oldest" file to delete | |
472 | 1 | previousFile = backupfile_n; |
473 | 1 | fileToBeDeleted = null; |
474 | 1 | Console.trace("BACKUPFILES No oldest file to delete"); |
475 | 1 | continue; |
476 | } | |
477 | ||
478 | // check the modification time of this (backupfile_n) and the previous | |
479 | // file (fileToBeDeleted) if the previous file is going to be deleted | |
480 | 17 | if (fileToBeDeleted != null) |
481 | { | |
482 | 8 | File replacementFile = backupfile_n; |
483 | 8 | long fileToBeDeletedLMT = fileToBeDeleted.lastModified(); |
484 | 8 | long replacementFileLMT = replacementFile.lastModified(); |
485 | 8 | Console.trace("BACKUPFILES fileToBeDeleted is " |
486 | + fileToBeDeleted.getAbsolutePath()); | |
487 | 8 | Console.trace("BACKUPFILES replacementFile is " |
488 | + backupfile_n.getAbsolutePath()); | |
489 | ||
490 | 8 | try |
491 | { | |
492 | 8 | File oldestTempFile = nextTempFile(fileToBeDeleted.getName(), |
493 | dirFile); | |
494 | ||
495 | 8 | if (fileToBeDeletedLMT > replacementFileLMT) |
496 | { | |
497 | 0 | String fileToBeDeletedLMTString = sdf |
498 | .format(fileToBeDeletedLMT); | |
499 | 0 | String replacementFileLMTString = sdf |
500 | .format(replacementFileLMT); | |
501 | 0 | Console.warn("WARNING! I am set to delete backupfile " |
502 | + fileToBeDeleted.getName() | |
503 | + " has modification time " | |
504 | + fileToBeDeletedLMTString | |
505 | + " which is newer than its replacement " | |
506 | + replacementFile.getName() | |
507 | + " with modification time " | |
508 | + replacementFileLMTString); | |
509 | ||
510 | 0 | boolean delete = confirmNewerDeleteFile(fileToBeDeleted, |
511 | replacementFile, true); | |
512 | 0 | Console.trace("BACKUPFILES " |
513 | 0 | + (delete ? "confirmed" : "not") + " deleting file " |
514 | + fileToBeDeleted.getAbsolutePath() | |
515 | + " which is newer than " | |
516 | + replacementFile.getAbsolutePath()); | |
517 | ||
518 | 0 | if (delete) |
519 | { | |
520 | // User has confirmed delete -- no need to add it to the list | |
521 | 0 | fileToBeDeleted.delete(); |
522 | } | |
523 | else | |
524 | { | |
525 | 0 | Console.debug("BACKUPFILES moving " |
526 | + fileToBeDeleted.getAbsolutePath() + " to " | |
527 | + oldestTempFile.getAbsolutePath()); | |
528 | 0 | moveFileToFile(fileToBeDeleted, oldestTempFile); |
529 | } | |
530 | } | |
531 | else | |
532 | { | |
533 | 8 | Console.debug("BACKUPFILES going to move " |
534 | + fileToBeDeleted.getAbsolutePath() + " to " | |
535 | + oldestTempFile.getAbsolutePath()); | |
536 | 8 | moveFileToFile(fileToBeDeleted, oldestTempFile); |
537 | 8 | addDeleteFile(oldestTempFile); |
538 | } | |
539 | ||
540 | } catch (Exception e) | |
541 | { | |
542 | 0 | Console.error( |
543 | "Error occurred, probably making new temp file for '" | |
544 | + fileToBeDeleted.getName() + "'"); | |
545 | 0 | Console.error(Cache.getStackTraceString(e)); |
546 | } | |
547 | ||
548 | // reset | |
549 | 8 | fileToBeDeleted = null; |
550 | } | |
551 | ||
552 | 17 | if (!noMax && n == tempMax && backupfile_n.exists()) |
553 | { | |
554 | 8 | fileToBeDeleted = backupfile_n; |
555 | } | |
556 | else | |
557 | { | |
558 | 9 | if (previousFile != null) |
559 | { | |
560 | // using boolean '&' instead of '&&' as don't want moveFileToFile | |
561 | // attempt to be conditional (short-circuit) | |
562 | 9 | ret = ret & moveFileToFile(backupfile_n, previousFile); |
563 | } | |
564 | } | |
565 | ||
566 | 17 | previousFile = backupfile_n; |
567 | } | |
568 | ||
569 | // index to use for the latest backup | |
570 | 9 | nextIndexNum = 1; |
571 | } | |
572 | else // not reverse numbering | |
573 | { | |
574 | // version style numbering (with earliest file deletion if max files | |
575 | // reached) | |
576 | ||
577 | 19 | bfTreeMap.values().toArray(backupFiles); |
578 | 19 | StringBuilder bfsb = new StringBuilder(); |
579 | 82 | for (int i = 0; i < backupFiles.length; i++) |
580 | { | |
581 | 63 | if (bfsb.length() > 0) |
582 | { | |
583 | 44 | bfsb.append(", "); |
584 | } | |
585 | 63 | bfsb.append(backupFiles[i].getName()); |
586 | } | |
587 | 19 | Console.trace("BACKUPFILES backupFiles: " + bfsb.toString()); |
588 | ||
589 | // noMax == true means keep all backup files | |
590 | 19 | if ((!noMax) && bfTreeMap.size() >= max) |
591 | { | |
592 | 8 | Console.trace("BACKUPFILES noMax: " + noMax + ", " + "max: " + max |
593 | + ", " + "bfTreeMap.size(): " + bfTreeMap.size()); | |
594 | // need to delete some files to keep number of backups to designated | |
595 | // max. | |
596 | // Note that if the suffix is not numbered then do not delete any | |
597 | // backup files later or we'll delete the new backup file (there can | |
598 | // be only one). | |
599 | 8 | int numToDelete = suffix.indexOf(NUM_PLACEHOLDER) > -1 |
600 | ? bfTreeMap.size() - max + 1 | |
601 | : 0; | |
602 | 8 | Console.trace("BACKUPFILES numToDelete: " + numToDelete); |
603 | // the "replacement" file is the latest backup file being kept (it's | |
604 | // not replacing though) | |
605 | 8 | File replacementFile = numToDelete < backupFiles.length |
606 | ? backupFiles[numToDelete] | |
607 | : null; | |
608 | 16 | for (int i = 0; i < numToDelete; i++) |
609 | { | |
610 | // check the deletion files for modification time of the last | |
611 | // backupfile being saved | |
612 | 8 | File fileToBeDeleted = backupFiles[i]; |
613 | 8 | boolean delete = true; |
614 | ||
615 | 8 | Console.trace( |
616 | "BACKUPFILES fileToBeDeleted: " + fileToBeDeleted); | |
617 | ||
618 | 8 | boolean newer = false; |
619 | 8 | if (replacementFile != null) |
620 | { | |
621 | 8 | long fileToBeDeletedLMT = fileToBeDeleted.lastModified(); |
622 | 8 | long replacementFileLMT = replacementFile != null |
623 | ? replacementFile.lastModified() | |
624 | : Long.MAX_VALUE; | |
625 | 8 | if (fileToBeDeletedLMT > replacementFileLMT) |
626 | { | |
627 | 0 | String fileToBeDeletedLMTString = sdf |
628 | .format(fileToBeDeletedLMT); | |
629 | 0 | String replacementFileLMTString = sdf |
630 | .format(replacementFileLMT); | |
631 | ||
632 | 0 | Console.warn("WARNING! I am set to delete backupfile '" |
633 | + fileToBeDeleted.getName() | |
634 | + "' has modification time " | |
635 | + fileToBeDeletedLMTString | |
636 | + " which is newer than the oldest backupfile being kept '" | |
637 | + replacementFile.getName() | |
638 | + "' with modification time " | |
639 | + replacementFileLMTString); | |
640 | ||
641 | 0 | delete = confirmNewerDeleteFile(fileToBeDeleted, |
642 | replacementFile, false); | |
643 | 0 | if (delete) |
644 | { | |
645 | // User has confirmed delete -- no need to add it to the list | |
646 | 0 | fileToBeDeleted.delete(); |
647 | 0 | Console.debug("BACKUPFILES deleting fileToBeDeleted: " |
648 | + fileToBeDeleted); | |
649 | 0 | delete = false; |
650 | } | |
651 | else | |
652 | { | |
653 | // keeping file, nothing to do! | |
654 | 0 | Console.debug("BACKUPFILES keeping fileToBeDeleted: " |
655 | + fileToBeDeleted); | |
656 | } | |
657 | } | |
658 | } | |
659 | 8 | if (delete) |
660 | { | |
661 | 8 | addDeleteFile(fileToBeDeleted); |
662 | 8 | Console.debug("BACKUPFILES addDeleteFile(fileToBeDeleted): " |
663 | + fileToBeDeleted); | |
664 | } | |
665 | ||
666 | } | |
667 | ||
668 | } | |
669 | ||
670 | 19 | nextIndexNum = bfTreeMap.lastKey() + 1; |
671 | } | |
672 | } | |
673 | ||
674 | // Let's make the new backup file!! yay, got there at last! | |
675 | 38 | String latestBackupFilename = dir + File.separatorChar |
676 | + BackupFilenameParts.getBackupFilename(nextIndexNum, basename, | |
677 | suffix, digits); | |
678 | 38 | Console.trace("BACKUPFILES Moving old file [" + file |
679 | + "] to latestBackupFilename [" + latestBackupFilename + "]"); | |
680 | // using boolean '&' instead of '&&' as don't want moveFileToFile attempt to | |
681 | // be conditional (short-circuit) | |
682 | 38 | ret = ret & moveFileToFile(file, new File(latestBackupFilename)); |
683 | 38 | Console.debug( |
684 | "BACKUPFILES moving " + file + " to " + latestBackupFilename | |
685 | 38 | + " was " + (ret ? "" : "NOT ") + "successful"); |
686 | 38 | if (tidyUp) |
687 | { | |
688 | 0 | Console.debug("BACKUPFILES tidying up files"); |
689 | 0 | tidyUpFiles(); |
690 | } | |
691 | ||
692 | 38 | return ret; |
693 | } | |
694 | ||
695 | 8 | private static File nextTempFile(String filename, File dirFile) |
696 | throws IOException | |
697 | { | |
698 | 8 | File temp = null; |
699 | 8 | COUNT: for (int i = 1; i < 1000; i++) |
700 | { | |
701 | 8 | File trythis = new File(dirFile, |
702 | filename + '~' + Integer.toString(i)); | |
703 | 8 | if (!trythis.exists()) |
704 | { | |
705 | 8 | temp = trythis; |
706 | 8 | break COUNT; |
707 | } | |
708 | ||
709 | } | |
710 | 8 | if (temp == null) |
711 | { | |
712 | 0 | temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile); |
713 | } | |
714 | 8 | return temp; |
715 | } | |
716 | ||
717 | 100 | private void tidyUpFiles() |
718 | { | |
719 | 100 | deleteOldFiles(); |
720 | } | |
721 | ||
722 | 0 | private static boolean confirmNewerDeleteFile(File fileToBeDeleted, |
723 | File replacementFile, boolean replace) | |
724 | { | |
725 | 0 | StringBuilder messageSB = new StringBuilder(); |
726 | ||
727 | 0 | File ftbd = fileToBeDeleted; |
728 | 0 | String ftbdLMT = sdf.format(ftbd.lastModified()); |
729 | 0 | String ftbdSize = Long.toString(ftbd.length()); |
730 | ||
731 | 0 | File rf = replacementFile; |
732 | 0 | String rfLMT = sdf.format(rf.lastModified()); |
733 | 0 | String rfSize = Long.toString(rf.length()); |
734 | ||
735 | 0 | int confirmButton = JvOptionPane.NO_OPTION; |
736 | 0 | if (replace) |
737 | { | |
738 | 0 | File saveFile = null; |
739 | 0 | try |
740 | { | |
741 | 0 | saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile()); |
742 | } catch (Exception e) | |
743 | { | |
744 | 0 | Console.error( |
745 | "Error when confirming to keep backup file newer than other backup files."); | |
746 | 0 | e.printStackTrace(); |
747 | } | |
748 | 0 | messageSB.append(MessageManager.formatMessage( |
749 | "label.newerdelete_replacement_line", new String[] | |
750 | { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize, | |
751 | rfSize })); | |
752 | // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted and | |
753 | // replaced by apparently older file \n''{1}''\t(modified {3}, size | |
754 | // {5})."" | |
755 | 0 | messageSB.append("\n\n"); |
756 | 0 | messageSB.append(MessageManager.formatMessage( |
757 | "label.confirm_deletion_or_rename", new String[] | |
758 | { ftbd.getName(), saveFile.getName() })); | |
759 | // "Confirm deletion of ''{0}'' or rename to ''{1}''?" | |
760 | 0 | String[] options = new String[] { |
761 | MessageManager.getString("label.delete"), | |
762 | MessageManager.getString("label.rename") }; | |
763 | ||
764 | 0 | confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION |
765 | : JvOptionPane.showOptionDialog(Desktop.desktop, | |
766 | messageSB.toString(), | |
767 | MessageManager.getString( | |
768 | "label.backupfiles_confirm_delete"), | |
769 | // "Confirm delete" | |
770 | JvOptionPane.YES_NO_OPTION, | |
771 | JvOptionPane.WARNING_MESSAGE, null, options, | |
772 | options[0]); | |
773 | } | |
774 | else | |
775 | { | |
776 | 0 | messageSB.append(MessageManager |
777 | .formatMessage("label.newerdelete_line", new String[] | |
778 | { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize, | |
779 | rfSize })); | |
780 | // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted but | |
781 | // is newer than the oldest remaining backup file \n''{1}''\t(modified | |
782 | // {3}, size {5})." | |
783 | 0 | messageSB.append("\n\n"); |
784 | 0 | messageSB.append(MessageManager |
785 | .formatMessage("label.confirm_deletion", new String[] | |
786 | { ftbd.getName() })); | |
787 | // "Confirm deletion of ''{0}''?" | |
788 | 0 | String[] options = new String[] { |
789 | MessageManager.getString("label.delete"), | |
790 | MessageManager.getString("label.keep") }; | |
791 | ||
792 | 0 | confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION |
793 | : JvOptionPane.showOptionDialog(Desktop.desktop, | |
794 | messageSB.toString(), | |
795 | MessageManager.getString( | |
796 | "label.backupfiles_confirm_delete"), | |
797 | // "Confirm delete" | |
798 | JvOptionPane.YES_NO_OPTION, | |
799 | JvOptionPane.WARNING_MESSAGE, null, options, | |
800 | options[0]); | |
801 | } | |
802 | ||
803 | // return should be TRUE if file is to be deleted | |
804 | 0 | return (confirmButton == JvOptionPane.YES_OPTION); |
805 | } | |
806 | ||
807 | 100 | private void deleteOldFiles() |
808 | { | |
809 | 100 | if (deleteFiles != null && !deleteFiles.isEmpty()) |
810 | { | |
811 | 16 | boolean doDelete = false; |
812 | 16 | StringBuilder messageSB = null; |
813 | 16 | if (confirmDelete && deleteFiles.size() > 0) |
814 | { | |
815 | 0 | messageSB = new StringBuilder(); |
816 | 0 | messageSB.append(MessageManager |
817 | .getString("label.backupfiles_confirm_delete_old_files")); | |
818 | // "Delete the following older backup files? (see the Backups tab in | |
819 | // Preferences for more options)" | |
820 | 0 | for (int i = 0; i < deleteFiles.size(); i++) |
821 | { | |
822 | 0 | File df = deleteFiles.get(i); |
823 | 0 | messageSB.append("\n"); |
824 | 0 | messageSB.append(df.getName()); |
825 | 0 | messageSB.append(" "); |
826 | 0 | messageSB.append(MessageManager.formatMessage("label.file_info", |
827 | new String[] | |
828 | { sdf.format(df.lastModified()), | |
829 | Long.toString(df.length()) })); | |
830 | // "(modified {0}, size {1})" | |
831 | } | |
832 | ||
833 | 0 | int confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION |
834 | : JvOptionPane.showConfirmDialog(Desktop.desktop, | |
835 | messageSB.toString(), | |
836 | MessageManager.getString( | |
837 | "label.backupfiles_confirm_delete"), | |
838 | // "Confirm delete" | |
839 | JvOptionPane.YES_NO_OPTION, | |
840 | JvOptionPane.WARNING_MESSAGE); | |
841 | ||
842 | 0 | doDelete = (confirmButton == JvOptionPane.YES_OPTION); |
843 | } | |
844 | else | |
845 | { | |
846 | 16 | doDelete = true; |
847 | } | |
848 | ||
849 | 16 | if (doDelete) |
850 | { | |
851 | 32 | for (int i = 0; i < deleteFiles.size(); i++) |
852 | { | |
853 | 16 | File fileToDelete = deleteFiles.get(i); |
854 | 16 | Console.trace("BACKUPFILES about to delete fileToDelete:" |
855 | + fileToDelete); | |
856 | 16 | fileToDelete.delete(); |
857 | 16 | Console.warn("deleted '" + fileToDelete.getName() + "'"); |
858 | } | |
859 | } | |
860 | ||
861 | } | |
862 | ||
863 | 100 | deleteFiles.clear(); |
864 | } | |
865 | ||
866 | 28 | private TreeMap<Integer, File> sortBackupFilesAsTreeMap( |
867 | File[] backupFiles, String basename) | |
868 | { | |
869 | // sort the backup files (based on integer found in the suffix) using a | |
870 | // precomputed Hashmap for speed | |
871 | 28 | Map<Integer, File> bfHashMap = new HashMap<>(); |
872 | 108 | for (int i = 0; i < backupFiles.length; i++) |
873 | { | |
874 | 80 | File f = backupFiles[i]; |
875 | 80 | BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, |
876 | digits); | |
877 | 80 | bfHashMap.put(bfp.indexNum(), f); |
878 | } | |
879 | 28 | TreeMap<Integer, File> bfTreeMap = new TreeMap<>(); |
880 | 28 | bfTreeMap.putAll(bfHashMap); |
881 | 28 | return bfTreeMap; |
882 | } | |
883 | ||
884 | 100 | public boolean rollBackupsAndRenameTempFile() |
885 | { | |
886 | 100 | boolean write = this.getWriteSuccess(); |
887 | ||
888 | 100 | boolean roll = false; |
889 | 100 | boolean rename = false; |
890 | 100 | if (write) |
891 | { | |
892 | 100 | roll = this.rollBackupFiles(false); // tidyUpFiles at the end |
893 | 100 | rename = this.renameTempFile(); |
894 | } | |
895 | ||
896 | /* | |
897 | * Not sure that this confirmation is desirable. By this stage the new file is | |
898 | * already written successfully, but something (e.g. disk full) has happened while | |
899 | * trying to roll the backup files, and most likely the filename needed will already | |
900 | * be vacant so renaming the temp file is nearly always correct! | |
901 | */ | |
902 | 100 | boolean okay = roll && rename; |
903 | 100 | if (!okay) |
904 | { | |
905 | 0 | StringBuilder messageSB = new StringBuilder(); |
906 | 0 | messageSB.append(MessageManager.getString( |
907 | "label.backupfiles_confirm_save_file_backupfiles_roll_wrong")); | |
908 | // "Something possibly went wrong with the backups of this file." | |
909 | 0 | if (rename) |
910 | { | |
911 | 0 | if (messageSB.length() > 0) |
912 | { | |
913 | 0 | messageSB.append("\n"); |
914 | } | |
915 | 0 | messageSB.append(MessageManager.getString( |
916 | "label.backupfiles_confirm_save_new_saved_file_ok")); | |
917 | // "The new saved file seems okay." | |
918 | } | |
919 | else | |
920 | { | |
921 | 0 | if (messageSB.length() > 0) |
922 | { | |
923 | 0 | messageSB.append("\n"); |
924 | } | |
925 | 0 | messageSB.append(MessageManager.getString( |
926 | "label.backupfiles_confirm_save_new_saved_file_not_ok")); | |
927 | // "The new saved file might not be okay." | |
928 | } | |
929 | 0 | if (messageSB.length() > 0) |
930 | { | |
931 | 0 | messageSB.append("\n"); |
932 | } | |
933 | 0 | messageSB |
934 | .append(MessageManager.getString("label.continue_operation")); | |
935 | ||
936 | 0 | int confirmButton = Platform.isHeadless() ? JvOptionPane.OK_OPTION |
937 | : JvOptionPane.showConfirmDialog(Desktop.desktop, | |
938 | messageSB.toString(), | |
939 | MessageManager.getString( | |
940 | "label.backupfiles_confirm_save_file"), | |
941 | // "Confirm save file" | |
942 | JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE); | |
943 | 0 | okay = confirmButton == JvOptionPane.OK_OPTION; |
944 | } | |
945 | 100 | if (okay) |
946 | { | |
947 | 100 | tidyUpFiles(); |
948 | } | |
949 | ||
950 | // remove this file from the save in progress stack | |
951 | 100 | removeSaveInProgress(rename); |
952 | ||
953 | 100 | return rename; |
954 | } | |
955 | ||
956 | 21 | public static TreeMap<Integer, File> getBackupFilesAsTreeMap( |
957 | String fileName, String suffix, int digits) | |
958 | { | |
959 | 21 | File[] backupFiles = null; |
960 | ||
961 | 21 | File file = new File(fileName); |
962 | ||
963 | 21 | File dirFile; |
964 | 21 | try |
965 | { | |
966 | 21 | dirFile = file.getParentFile(); |
967 | } catch (Exception e) | |
968 | { | |
969 | 0 | Console.error("Could not get canonical path for file '" + file + "'"); |
970 | 0 | return new TreeMap<>(); |
971 | } | |
972 | ||
973 | 21 | String filename = file.getName(); |
974 | 21 | String basename = filename; |
975 | ||
976 | // find existing backup files | |
977 | 21 | BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, |
978 | digits); | |
979 | 21 | backupFiles = dirFile.listFiles(bff); // is clone needed? |
980 | ||
981 | // sort the backup files (based on integer found in the suffix) using a | |
982 | // precomputed Hashmap for speed | |
983 | 21 | Map<Integer, File> bfHashMap = new HashMap<>(); |
984 | 91 | for (int i = 0; i < backupFiles.length; i++) |
985 | { | |
986 | 70 | File f = backupFiles[i]; |
987 | 70 | BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, |
988 | digits); | |
989 | 70 | bfHashMap.put(bfp.indexNum(), f); |
990 | } | |
991 | 21 | TreeMap<Integer, File> bfTreeMap = new TreeMap<>(); |
992 | 21 | bfTreeMap.putAll(bfHashMap); |
993 | ||
994 | 21 | return bfTreeMap; |
995 | } | |
996 | ||
997 | /* | |
998 | private boolean addDeleteFile(File fileToBeDeleted, File originalFile, | |
999 | boolean delete, boolean newer) | |
1000 | { | |
1001 | return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer); | |
1002 | } | |
1003 | */ | |
1004 | 16 | private boolean addDeleteFile(File fileToBeDeleted) |
1005 | { | |
1006 | 16 | boolean ret = false; |
1007 | 16 | int pos = deleteFiles.indexOf(fileToBeDeleted); |
1008 | 16 | if (pos > -1) |
1009 | { | |
1010 | 0 | Console.debug("BACKUPFILES not adding file " |
1011 | + fileToBeDeleted.getAbsolutePath() | |
1012 | + " to the delete list (already at index" + pos + ")"); | |
1013 | 0 | return true; |
1014 | } | |
1015 | else | |
1016 | { | |
1017 | 16 | Console.debug("BACKUPFILES adding file " |
1018 | + fileToBeDeleted.getAbsolutePath() + " to the delete list"); | |
1019 | 16 | deleteFiles.add(fileToBeDeleted); |
1020 | } | |
1021 | 16 | return ret; |
1022 | } | |
1023 | ||
1024 | 4 | public File getFile() |
1025 | { | |
1026 | 4 | return file; |
1027 | } | |
1028 | ||
1029 | 155 | public static boolean moveFileToFile(File oldFile, File newFile) |
1030 | { | |
1031 | 155 | Console.initLogger(); |
1032 | 155 | boolean ret = false; |
1033 | 155 | Path oldPath = Paths.get(oldFile.getAbsolutePath()); |
1034 | 155 | Path newPath = Paths.get(newFile.getAbsolutePath()); |
1035 | 155 | try |
1036 | { | |
1037 | // delete destination file - not usually necessary but Just In Case... | |
1038 | 155 | Console.trace("BACKUPFILES deleting " + newFile.getAbsolutePath()); |
1039 | 155 | newFile.delete(); |
1040 | 155 | Console.trace("BACKUPFILES moving " + oldFile.getAbsolutePath() |
1041 | + " to " + newFile.getAbsolutePath()); | |
1042 | 155 | Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING); |
1043 | 155 | ret = true; |
1044 | 155 | Console.trace("BACKUPFILES move seems to have succeeded"); |
1045 | } catch (IOException e) | |
1046 | { | |
1047 | 0 | Console.warn("Could not move file '" + oldPath.toString() + "' to '" |
1048 | + newPath.toString() + "'"); | |
1049 | 0 | Console.error(e.getMessage()); |
1050 | 0 | Console.debug(Cache.getStackTraceString(e)); |
1051 | 0 | ret = false; |
1052 | } catch (Exception e) | |
1053 | { | |
1054 | 0 | Console.error(e.getMessage()); |
1055 | 0 | Console.debug(Cache.getStackTraceString(e)); |
1056 | 0 | ret = false; |
1057 | } | |
1058 | 155 | return ret; |
1059 | } | |
1060 | } |