diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 601c3fba93f..8a58d2a6257 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -490,7 +490,7 @@ public boolean quit() { context.clearDBMSSynchronizer(); } AutosaveManager.shutdown(context); - BackupManager.shutdown(context); + BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); context.getDatabasePath().map(Path::toAbsolutePath).map(Path::toString).ifPresent(filenames::add); } @@ -1235,7 +1235,7 @@ private boolean confirmClose(LibraryTab libraryTab) { } if (buttonType.equals(discardChanges)) { - BackupManager.discardBackup(libraryTab.getBibDatabaseContext()); + BackupManager.discardBackup(libraryTab.getBibDatabaseContext(), prefs.getFilePreferences().getBackupDirectory()); return true; } @@ -1312,7 +1312,7 @@ private void closeTab(LibraryTab libraryTab) { removeTab(libraryTab); } AutosaveManager.shutdown(context); - BackupManager.shutdown(context); + BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); } private void removeTab(LibraryTab libraryTab) { diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index bc0ac7fb296..c3a06b8c56b 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -289,7 +289,7 @@ public void installAutosaveManagerAndBackupManager() { AutosaveManager autosaveManager = AutosaveManager.start(bibDatabaseContext); autosaveManager.registerListener(new AutosaveUiManager(this)); } - if (isDatabaseReadyForBackup(bibDatabaseContext)) { + if (isDatabaseReadyForBackup(bibDatabaseContext) && preferencesService.getFilePreferences().shouldCreateBackup()) { BackupManager.start(bibDatabaseContext, Globals.entryTypesManager, preferencesService); } } @@ -706,7 +706,7 @@ private void saveDividerLocation(Number position) { public void cleanUp() { changeMonitor.ifPresent(DatabaseChangeMonitor::unregister); AutosaveManager.shutdown(bibDatabaseContext); - BackupManager.shutdown(bibDatabaseContext); + BackupManager.shutdown(bibDatabaseContext, preferencesService.getFilePreferences().getBackupDirectory(), preferencesService.getFilePreferences().shouldCreateBackup()); } /** diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index fffab647dd0..76d696aad4b 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -62,7 +62,7 @@ public class StateManager { private final OptionalObjectProperty activeSearchQuery = OptionalObjectProperty.empty(); private final ObservableMap searchResultMap = FXCollections.observableHashMap(); private final OptionalObjectProperty focusOwner = OptionalObjectProperty.empty(); - private final ObservableList>> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[] {task.getValue().progressProperty(), task.getValue().runningProperty()}); + private final ObservableList, Task>> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[] {task.getValue().progressProperty(), task.getValue().runningProperty()}); private final EasyBinding anyTaskRunning = EasyBind.reduce(backgroundTasks, tasks -> tasks.map(Pair::getValue).anyMatch(Task::isRunning)); private final EasyBinding anyTasksThatWillNotBeRecoveredRunning = EasyBind.reduce(backgroundTasks, tasks -> tasks.anyMatch(task -> !task.getKey().willBeRecoveredAutomatically() && task.getValue().isRunning())); private final EasyBinding tasksProgress = EasyBind.reduce(backgroundTasks, tasks -> tasks.map(Pair::getValue).filter(Task::isRunning).mapToDouble(Task::getProgress).average().orElse(1)); @@ -169,7 +169,7 @@ public ObservableList> getBackgroundTasks() { return EasyBind.map(backgroundTasks, Pair::getValue); } - public void addBackgroundTask(BackgroundTask backgroundTask, Task task) { + public void addBackgroundTask(BackgroundTask backgroundTask, Task task) { this.backgroundTasks.add(0, new Pair<>(backgroundTask, task)); } diff --git a/src/main/java/org/jabref/gui/backup/BackupResolverDialog.java b/src/main/java/org/jabref/gui/backup/BackupResolverDialog.java index bdd2fbd2d16..de43a067eb8 100644 --- a/src/main/java/org/jabref/gui/backup/BackupResolverDialog.java +++ b/src/main/java/org/jabref/gui/backup/BackupResolverDialog.java @@ -26,13 +26,13 @@ public class BackupResolverDialog extends FXDialog { private static final Logger LOGGER = LoggerFactory.getLogger(BackupResolverDialog.class); - public BackupResolverDialog(Path originalPath) { + public BackupResolverDialog(Path originalPath, Path backupDir) { super(AlertType.CONFIRMATION, Localization.lang("Backup found"), true); setHeaderText(null); getDialogPane().setMinHeight(180); getDialogPane().getButtonTypes().setAll(RESTORE_FROM_BACKUP, REVIEW_BACKUP, IGNORE_BACKUP); - Optional backupPathOpt = BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP); + Optional backupPathOpt = BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP, backupDir); String backupFilename = backupPathOpt.map(Path::getFileName).map(Path::toString).orElse(Localization.lang("File not found")); String content = new StringBuilder() .append(Localization.lang("A backup file for '%0' was found at [%1]", diff --git a/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java b/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java index 1cc32a198c1..5cc07a31c71 100644 --- a/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java +++ b/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java @@ -54,7 +54,7 @@ public void execute() { checkOverwriteKeysChosen(); if (!this.isCanceled) { - BackgroundTask backgroundTask = this.generateKeysInBackground(); + BackgroundTask backgroundTask = this.generateKeysInBackground(); backgroundTask.showToUser(true); backgroundTask.titleProperty().set(Localization.lang("Autogenerate citation keys")); backgroundTask.messageProperty().set(Localization.lang("%0/%1 entries", 0, entries.size())); @@ -93,8 +93,8 @@ private void checkOverwriteKeysChosen() { } } - private BackgroundTask generateKeysInBackground() { - return new BackgroundTask() { + private BackgroundTask generateKeysInBackground() { + return new BackgroundTask<>() { private NamedCompound compound; @Override diff --git a/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java b/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java index ceba43dd088..b1c4981e744 100644 --- a/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java +++ b/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java @@ -38,10 +38,10 @@ private BackupUIManager() { } public static Optional showRestoreBackupDialog(DialogService dialogService, Path originalPath, PreferencesService preferencesService) { - var actionOpt = showBackupResolverDialog(dialogService, originalPath); + var actionOpt = showBackupResolverDialog(dialogService, originalPath, preferencesService.getFilePreferences().getBackupDirectory()); return actionOpt.flatMap(action -> { if (action == BackupResolverDialog.RESTORE_FROM_BACKUP) { - BackupManager.restoreBackup(originalPath); + BackupManager.restoreBackup(originalPath, preferencesService.getFilePreferences().getBackupDirectory()); return Optional.empty(); } else if (action == BackupResolverDialog.REVIEW_BACKUP) { return showReviewBackupDialog(dialogService, originalPath, preferencesService); @@ -50,20 +50,20 @@ public static Optional showRestoreBackupDialog(DialogService dialo }); } - private static Optional showBackupResolverDialog(DialogService dialogService, Path originalPath) { - return DefaultTaskExecutor.runInJavaFXThread(() -> dialogService.showCustomDialogAndWait(new BackupResolverDialog(originalPath))); + private static Optional showBackupResolverDialog(DialogService dialogService, Path originalPath, Path backupDir) { + return DefaultTaskExecutor.runInJavaFXThread(() -> dialogService.showCustomDialogAndWait(new BackupResolverDialog(originalPath, backupDir))); } private static Optional showReviewBackupDialog(DialogService dialogService, Path originalPath, PreferencesService preferencesService) { try { - ImportFormatPreferences importFormatPreferences = Globals.prefs.getImportFormatPreferences(); + ImportFormatPreferences importFormatPreferences = preferencesService.getImportFormatPreferences(); // The database of the originalParserResult will be modified ParserResult originalParserResult = OpenDatabase.loadDatabase(originalPath, importFormatPreferences, Globals.getFileUpdateMonitor()); // This will be modified by using the `DatabaseChangesResolverDialog`. BibDatabaseContext originalDatabase = originalParserResult.getDatabaseContext(); - Path backupPath = BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP).orElseThrow(); + Path backupPath = BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP, preferencesService.getFilePreferences().getBackupDirectory()).orElseThrow(); BibDatabaseContext backupDatabase = OpenDatabase.loadDatabase(backupPath, importFormatPreferences, new DummyFileUpdateMonitor()).getDatabaseContext(); DatabaseChangeResolverFactory changeResolverFactory = new DatabaseChangeResolverFactory(dialogService, originalDatabase, preferencesService.getBibEntryPreferences()); diff --git a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java index 95fa69f1777..ca32a6a98df 100644 --- a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java +++ b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java @@ -116,7 +116,7 @@ boolean saveAs(Path file, SaveDatabaseMode mode) { final Path oldFile = databasePath.get(); context.setDatabasePath(oldFile); AutosaveManager.shutdown(context); - BackupManager.shutdown(context); + BackupManager.shutdown(context, this.preferences.getFilePreferences().getBackupDirectory(), preferences.getFilePreferences().shouldCreateBackup()); } // Set new location diff --git a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java index 403359dd390..75ee983ec31 100644 --- a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java +++ b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java @@ -192,9 +192,10 @@ private ParserResult loadDatabase(Path file) throws Exception { dialogService.notify(Localization.lang("Opening") + ": '" + file + "'"); preferencesService.getFilePreferences().setWorkingDirectory(fileToLoad.getParent()); + Path backupDir = preferencesService.getFilePreferences().getBackupDirectory(); ParserResult parserResult = null; - if (BackupManager.backupFileDiffers(fileToLoad)) { + if (BackupManager.backupFileDiffers(fileToLoad, backupDir)) { // In case the backup differs, ask the user what to do. // In case the user opted for restoring a backup, the content of the backup is contained in parserResult. parserResult = BackupUIManager.showRestoreBackupDialog(dialogService, fileToLoad, preferencesService).orElse(null); diff --git a/src/main/java/org/jabref/gui/preferences/general/GeneralTab.fxml b/src/main/java/org/jabref/gui/preferences/general/GeneralTab.fxml index a108e2fec39..8044c84f7e7 100644 --- a/src/main/java/org/jabref/gui/preferences/general/GeneralTab.fxml +++ b/src/main/java/org/jabref/gui/preferences/general/GeneralTab.fxml @@ -1,12 +1,18 @@ + + + + + + @@ -35,4 +41,22 @@ + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java b/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java index 6a28b84db3a..0f3811c47cb 100644 --- a/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java +++ b/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java @@ -3,6 +3,7 @@ import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; import org.jabref.gui.preferences.AbstractPreferenceTabView; import org.jabref.gui.preferences.PreferencesTab; @@ -24,6 +25,9 @@ public class GeneralTab extends AbstractPreferenceTabView i @FXML private CheckBox collectTelemetry; @FXML private CheckBox showAdvancedHints; + @FXML private CheckBox createBackup; + @FXML private TextField backupDirectory; + public GeneralTab() { ViewLoader.view(this) .root(this) @@ -36,7 +40,7 @@ public String getTabName() { } public void initialize() { - this.viewModel = new GeneralTabViewModel(dialogService, preferencesService.getGeneralPreferences(), preferencesService.getTelemetryPreferences()); + this.viewModel = new GeneralTabViewModel(preferencesService, dialogService); new ViewModelListCellFactory() .withText(Language::getDisplayName) @@ -56,5 +60,13 @@ public void initialize() { openLastStartup.selectedProperty().bindBidirectional(viewModel.openLastStartupProperty()); collectTelemetry.selectedProperty().bindBidirectional(viewModel.collectTelemetryProperty()); showAdvancedHints.selectedProperty().bindBidirectional(viewModel.showAdvancedHintsProperty()); + + createBackup.selectedProperty().bindBidirectional(viewModel.createBackupProperty()); + backupDirectory.textProperty().bindBidirectional(viewModel.backupDirectoryProperty()); + backupDirectory.disableProperty().bind(viewModel.createBackupProperty().not()); + } + + public void backupFileDirBrowse() { + viewModel.backupFileDirBrowse(); } } diff --git a/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java b/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java index 68130266435..b36ef455302 100644 --- a/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java @@ -1,5 +1,6 @@ package org.jabref.gui.preferences.general; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -10,15 +11,20 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleListProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.transformation.SortedList; import org.jabref.gui.DialogService; import org.jabref.gui.preferences.PreferenceTabViewModel; +import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.logic.l10n.Language; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseMode; +import org.jabref.preferences.FilePreferences; import org.jabref.preferences.GeneralPreferences; +import org.jabref.preferences.PreferencesService; import org.jabref.preferences.TelemetryPreferences; public class GeneralTabViewModel implements PreferenceTabViewModel { @@ -34,19 +40,25 @@ public class GeneralTabViewModel implements PreferenceTabViewModel { private final BooleanProperty openLastStartupProperty = new SimpleBooleanProperty(); private final BooleanProperty showAdvancedHintsProperty = new SimpleBooleanProperty(); + private final BooleanProperty createBackupProperty = new SimpleBooleanProperty(); + private final StringProperty backupDirectoryProperty = new SimpleStringProperty(""); + private final DialogService dialogService; private final GeneralPreferences generalPreferences; private final TelemetryPreferences telemetryPreferences; + private final FilePreferences filePreferences; private final List restartWarning = new ArrayList<>(); @SuppressWarnings("ReturnValueIgnored") - public GeneralTabViewModel(DialogService dialogService, GeneralPreferences generalPreferences, TelemetryPreferences telemetryPreferences) { + public GeneralTabViewModel(PreferencesService preferencesService, DialogService dialogService) { this.dialogService = dialogService; - this.generalPreferences = generalPreferences; - this.telemetryPreferences = telemetryPreferences; + this.generalPreferences = preferencesService.getGeneralPreferences(); + this.telemetryPreferences = preferencesService.getTelemetryPreferences(); + this.filePreferences = preferencesService.getFilePreferences(); } + @Override public void setValues() { languagesListProperty.setValue(new SortedList<>(FXCollections.observableArrayList(Language.values()), Comparator.comparing(Language::getDisplayName))); selectedLanguageProperty.setValue(generalPreferences.getLanguage()); @@ -60,8 +72,12 @@ public void setValues() { collectTelemetryProperty.setValue(telemetryPreferences.shouldCollectTelemetry()); openLastStartupProperty.setValue(generalPreferences.shouldOpenLastEdited()); showAdvancedHintsProperty.setValue(generalPreferences.shouldShowAdvancedHints()); + + createBackupProperty.setValue(filePreferences.shouldCreateBackup()); + backupDirectoryProperty.setValue(filePreferences.getBackupDirectory().toString()); } + @Override public void storeSettings() { Language newLanguage = selectedLanguageProperty.getValue(); if (newLanguage != generalPreferences.getLanguage()) { @@ -84,6 +100,9 @@ public void storeSettings() { generalPreferences.setShowAdvancedHints(showAdvancedHintsProperty.getValue()); telemetryPreferences.setCollectTelemetry(collectTelemetryProperty.getValue()); + + filePreferences.createBackupProperty().setValue(createBackupProperty.getValue()); + filePreferences.backupDirectoryProperty().setValue(Path.of(backupDirectoryProperty.getValue())); } @Override @@ -132,4 +151,19 @@ public BooleanProperty openLastStartupProperty() { public BooleanProperty showAdvancedHintsProperty() { return this.showAdvancedHintsProperty; } + + public BooleanProperty createBackupProperty() { + return this.createBackupProperty; + } + + public StringProperty backupDirectoryProperty() { + return this.backupDirectoryProperty; + } + + public void backupFileDirBrowse() { + DirectoryDialogConfiguration dirDialogConfiguration = + new DirectoryDialogConfiguration.Builder().withInitialDirectory(Path.of(backupDirectoryProperty().getValue())).build(); + dialogService.showDirectorySelectionDialog(dirDialogConfiguration) + .ifPresent(dir -> backupDirectoryProperty.setValue(dir.toString())); + } } diff --git a/src/main/java/org/jabref/logic/autosaveandbackup/BackupManager.java b/src/main/java/org/jabref/logic/autosaveandbackup/BackupManager.java index 9a8aafacc38..599ac265fc9 100644 --- a/src/main/java/org/jabref/logic/autosaveandbackup/BackupManager.java +++ b/src/main/java/org/jabref/logic/autosaveandbackup/BackupManager.java @@ -58,11 +58,9 @@ public class BackupManager { private final CoarseChangeFilter changeFilter; private final BibEntryTypesManager entryTypesManager; - // Contains a list of all backup paths // During a write, the less recent backup file is deleted private final Queue backupFilesQueue = new LinkedBlockingQueue<>(); - private boolean needsBackup = false; BackupManager(BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, PreferencesService preferences) { @@ -78,15 +76,15 @@ public class BackupManager { /** * Determines the most recent backup file name */ - static Path getBackupPathForNewBackup(Path originalPath) { - return BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(originalPath, BackupFileType.BACKUP); + static Path getBackupPathForNewBackup(Path originalPath, Path backupDir) { + return BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(originalPath, BackupFileType.BACKUP, backupDir); } /** * Determines the most recent existing backup file name */ - static Optional getLatestBackupPath(Path originalPath) { - return BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP); + static Optional getLatestBackupPath(Path originalPath, Path backupDir) { + return BackupFileUtil.getPathOfLatestExistingBackupFile(originalPath, BackupFileType.BACKUP, backupDir); } /** @@ -99,7 +97,7 @@ static Optional getLatestBackupPath(Path originalPath) { */ public static BackupManager start(BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, PreferencesService preferences) { BackupManager backupManager = new BackupManager(bibDatabaseContext, entryTypesManager, preferences); - backupManager.startBackupTask(); + backupManager.startBackupTask(preferences.getFilePreferences().getBackupDirectory()); runningInstances.add(backupManager); return backupManager; } @@ -109,19 +107,19 @@ public static BackupManager start(BibDatabaseContext bibDatabaseContext, BibEntr * * @param bibDatabaseContext Associated {@link BibDatabaseContext} */ - public static void discardBackup(BibDatabaseContext bibDatabaseContext) { - runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach( - BackupManager::discardBackup); + public static void discardBackup(BibDatabaseContext bibDatabaseContext, Path backupDir) { + runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach(backupManager -> backupManager.discardBackup(backupDir)); } /** * Shuts down the BackupManager which is associated with the given {@link BibDatabaseContext}. * * @param bibDatabaseContext Associated {@link BibDatabaseContext} + * @param createBackup True, if a backup should be created + * @param backupDir The path to the backup directory */ - public static void shutdown(BibDatabaseContext bibDatabaseContext) { - runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach( - BackupManager::shutdown); + public static void shutdown(BibDatabaseContext bibDatabaseContext, Path backupDir, boolean createBackup) { + runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach(backupManager -> backupManager.shutdown(backupDir, createBackup)); runningInstances.removeIf(instance -> instance.bibDatabaseContext == bibDatabaseContext); } @@ -137,8 +135,8 @@ public static void shutdown(BibDatabaseContext bibDatabaseContext) { * "default" return value in the good case. In case a discarded file exists, false is returned, too. * In the case of an exception true is returned to ensure that the user checks the output. */ - public static boolean backupFileDiffers(Path originalPath) { - Path discardedFile = determineDiscardedFile(originalPath); + public static boolean backupFileDiffers(Path originalPath, Path backupDir) { + Path discardedFile = determineDiscardedFile(originalPath, backupDir); if (Files.exists(discardedFile)) { try { Files.delete(discardedFile); @@ -148,10 +146,10 @@ public static boolean backupFileDiffers(Path originalPath) { } return false; } - return getLatestBackupPath(originalPath).map(latestBackupPath -> { + return getLatestBackupPath(originalPath, backupDir).map(latestBackupPath -> { FileTime latestBackupFileLastModifiedTime; try { - latestBackupFileLastModifiedTime = Files.getLastModifiedTime(latestBackupPath); + latestBackupFileLastModifiedTime = Files.getLastModifiedTime(latestBackupPath); } catch (IOException e) { LOGGER.debug("Could not get timestamp of backup file {}", latestBackupPath, e); // If we cannot get the timestamp, we do show any warning @@ -185,8 +183,8 @@ public static boolean backupFileDiffers(Path originalPath) { * * @param originalPath Path to the file which should be equalized to the backup file. */ - public static void restoreBackup(Path originalPath) { - Optional backupPath = getLatestBackupPath(originalPath); + public static void restoreBackup(Path originalPath, Path backupDir) { + Optional backupPath = getLatestBackupPath(originalPath, backupDir); if (backupPath.isEmpty()) { LOGGER.error("There is no backup file"); return; @@ -198,8 +196,8 @@ public static void restoreBackup(Path originalPath) { } } - Optional determineBackupPathForNewBackup() { - return bibDatabaseContext.getDatabasePath().map(BackupManager::getBackupPathForNewBackup); + Optional determineBackupPathForNewBackup(Path backupDir) { + return bibDatabaseContext.getDatabasePath().map(path -> BackupManager.getBackupPathForNewBackup(path, backupDir)); } /** @@ -207,7 +205,8 @@ Optional determineBackupPathForNewBackup() { * * SIDE EFFECT: Deletes oldest backup file * - * @param backupPath the path where the library should be backed up to + * + * @param backupPath the full path to the file where the library should be backed up to */ void performBackup(Path backupPath) { if (!needsBackup) { @@ -253,10 +252,8 @@ void performBackup(Path backupPath) { } } - private static Path determineDiscardedFile(Path file) { - return BackupFileUtil.getAppDataBackupDir().resolve( - BackupFileUtil.getUniqueFilePrefix(file) + "--" + file.getFileName() + "--discarded" - ); + private static Path determineDiscardedFile(Path file, Path backupDir) { + return backupDir.resolve(BackupFileUtil.getUniqueFilePrefix(file) + "--" + file.getFileName() + "--discarded"); } /** @@ -265,8 +262,8 @@ private static Path determineDiscardedFile(Path file) { * We do not delete any files, because the user might want to recover old backup files. * Therefore, we mark discarded backups by a --discarded file. */ - public void discardBackup() { - Path path = determineDiscardedFile(bibDatabaseContext.getDatabasePath().get()); + public void discardBackup(Path backupDir) { + Path path = determineDiscardedFile(bibDatabaseContext.getDatabasePath().get(), backupDir); try { Files.createFile(path); } catch (IOException e) { @@ -294,19 +291,18 @@ public synchronized void listen(@SuppressWarnings("unused") BibDatabaseContextCh } } - private void startBackupTask() { - fillQueue(); + private void startBackupTask(Path backupDir) { + fillQueue(backupDir); executor.scheduleAtFixedRate( - // We need to determine the backup path on each action, because we use the timestamp in the filename - () -> determineBackupPathForNewBackup().ifPresent(this::performBackup), - DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, - DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, - TimeUnit.SECONDS); + // We need to determine the backup path on each action, because we use the timestamp in the filename + () -> determineBackupPathForNewBackup(backupDir).ifPresent(path -> this.performBackup(path)), + DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, + DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, + TimeUnit.SECONDS); } - private void fillQueue() { - Path backupDir = BackupFileUtil.getAppDataBackupDir(); + private void fillQueue(Path backupDir) { if (!Files.exists(backupDir)) { return; } @@ -328,13 +324,18 @@ private void fillQueue() { /** * Unregisters the BackupManager from the eventBus of {@link BibDatabaseContext}. * This method should only be used when closing a database/JabRef in a normal way. + * + * @param backupDir The backup directory + * @param createBackup If the backup manager should still perform a backup */ - private void shutdown() { + private void shutdown(Path backupDir, boolean createBackup) { changeFilter.unregisterListener(this); changeFilter.shutdown(); executor.shutdown(); - // Ensure that backup is a recent one - determineBackupPathForNewBackup().ifPresent(this::performBackup); + if (createBackup) { + // Ensure that backup is a recent one + determineBackupPathForNewBackup(backupDir).ifPresent(this::performBackup); + } } } diff --git a/src/main/java/org/jabref/logic/util/io/BackupFileUtil.java b/src/main/java/org/jabref/logic/util/io/BackupFileUtil.java index c378386c1ed..d2b42eef98f 100644 --- a/src/main/java/org/jabref/logic/util/io/BackupFileUtil.java +++ b/src/main/java/org/jabref/logic/util/io/BackupFileUtil.java @@ -48,12 +48,13 @@ public static Path getAppDataBackupDir() { * (and configured in the preferences as "make backups") *

*/ - public static Path getPathForNewBackupFileAndCreateDirectory(Path targetFile, BackupFileType fileType) { + + public static Path getPathForNewBackupFileAndCreateDirectory(Path targetFile, BackupFileType fileType, Path backupDir) { String extension = "." + fileType.getExtensions().get(0); String timeSuffix = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH.mm.ss")); // We choose the data directory, because a ".bak" file should survive cache cleanups - Path directory = getAppDataBackupDir(); + Path directory = backupDir; try { Files.createDirectories(directory); } catch (IOException e) { @@ -66,18 +67,12 @@ public static Path getPathForNewBackupFileAndCreateDirectory(Path targetFile, Ba return directory.resolve(fileName); } - /** - * Finds the latest backup (.sav). If it does not exist, an empty optional is returned - * - * @param targetFile the full path of the file to backup - */ - public static Optional getPathOfLatestExistingBackupFile(Path targetFile, BackupFileType fileType) { + public static Optional getPathOfLatestExistingBackupFile(Path targetFile, BackupFileType fileType, Path backupDir) { // The code is similar to "getPathForNewBackupFileAndCreateDirectory" String extension = "." + fileType.getExtensions().get(0); - Path directory = getAppDataBackupDir(); - if (Files.notExists(directory)) { + if (Files.notExists(backupDir)) { // In case there is no app directory, we search in the directory of the bib file Path result = FileUtil.addExtension(targetFile, extension); if (Files.exists(result)) { @@ -91,11 +86,11 @@ public static Optional getPathOfLatestExistingBackupFile(Path targetFile, final String prefix = getUniqueFilePrefix(targetFile) + "--" + targetFile.getFileName(); Optional mostRecentFile; try { - mostRecentFile = Files.list(directory) - // just list the .sav belonging to the given targetFile - .filter(p -> p.getFileName().toString().startsWith(prefix)) - .sorted() - .reduce((first, second) -> second); + mostRecentFile = Files.list(backupDir) + // just list the .sav belonging to the given targetFile + .filter(p -> p.getFileName().toString().startsWith(prefix)) + .sorted() + .reduce((first, second) -> second); } catch (IOException e) { LOGGER.error("Could not determine most recent file", e); return Optional.empty(); diff --git a/src/main/java/org/jabref/preferences/FilePreferences.java b/src/main/java/org/jabref/preferences/FilePreferences.java index 264d778aeda..f204005afed 100644 --- a/src/main/java/org/jabref/preferences/FilePreferences.java +++ b/src/main/java/org/jabref/preferences/FilePreferences.java @@ -31,6 +31,8 @@ public class FilePreferences { private final BooleanProperty fulltextIndexLinkedFiles = new SimpleBooleanProperty(); private final ObjectProperty workingDirectory = new SimpleObjectProperty<>(); private final ObservableSet externalFileTypes = FXCollections.observableSet(new TreeSet<>(Comparator.comparing(ExternalFileType::getName))); + private final BooleanProperty createBackup = new SimpleBooleanProperty(); + private final ObjectProperty backupDiretory = new SimpleObjectProperty<>(); public FilePreferences(String user, String mainFileDirectory, @@ -40,7 +42,9 @@ public FilePreferences(String user, boolean downloadLinkedFiles, boolean fulltextIndexLinkedFiles, Path workingDirectory, - Set externalFileTypes) { + Set externalFileTypes, + boolean createBackup, + Path backupDirectory) { this.user.setValue(user); this.mainFileDirectory.setValue(mainFileDirectory); this.storeFilesRelativeToBibFile.setValue(storeFilesRelativeToBibFile); @@ -50,6 +54,8 @@ public FilePreferences(String user, this.fulltextIndexLinkedFiles.setValue(fulltextIndexLinkedFiles); this.workingDirectory.setValue(workingDirectory); this.externalFileTypes.addAll(externalFileTypes); + this.createBackup.setValue(createBackup); + this.backupDiretory.setValue(backupDirectory); } public String getUser() { @@ -147,4 +153,28 @@ public void setWorkingDirectory(Path workingDirectory) { public ObservableSet getExternalFileTypes() { return this.externalFileTypes; } + + public void setCreateBackup(boolean createBackup) { + this.createBackup.set(createBackup); + } + + public boolean shouldCreateBackup() { + return this.createBackup.getValue(); + } + + public BooleanProperty createBackupProperty() { + return this.createBackup; + } + + public ObjectProperty backupDirectoryProperty() { + return this.backupDiretory; + } + + public void setBackupDirectory(Path backupPath) { + this.backupDiretory.set(backupPath); + } + + public Path getBackupDirectory() { + return this.backupDiretory.getValue(); + } } diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 2ed223792a2..fb9a98a535c 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -100,6 +100,7 @@ import org.jabref.logic.util.OS; import org.jabref.logic.util.Version; import org.jabref.logic.util.io.AutoLinkPreferences; +import org.jabref.logic.util.io.BackupFileUtil; import org.jabref.logic.util.io.FileHistory; import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.database.BibDatabaseMode; @@ -192,6 +193,7 @@ public class JabRefPreferences implements PreferencesService { public static final String SIZE_X = "mainWindowSizeX"; public static final String POS_Y = "mainWindowPosY"; public static final String POS_X = "mainWindowPosX"; + public static final String LAST_EDITED = "lastEdited"; public static final String OPEN_LAST_EDITED = "openLastEdited"; public static final String LAST_FOCUSED = "lastFocused"; @@ -200,6 +202,8 @@ public class JabRefPreferences implements PreferencesService { public static final String LAST_USED_EXPORT = "lastUsedExport"; public static final String EXPORT_WORKING_DIRECTORY = "exportWorkingDirectory"; public static final String WORKING_DIRECTORY = "workingDirectory"; + public static final String BACKUP_DIRECTORY = "backupDirectory"; + public static final String CREATE_BACKUP = "createBackup"; public static final String KEYWORD_SEPARATOR = "groupKeywordSeparator"; public static final String AUTO_ASSIGN_GROUP = "autoAssignGroup"; @@ -587,6 +591,9 @@ private JabRefPreferences() { defaults.put(USE_XMP_PRIVACY_FILTER, Boolean.FALSE); defaults.put(WORKING_DIRECTORY, USER_HOME); defaults.put(EXPORT_WORKING_DIRECTORY, USER_HOME); + + defaults.put(CREATE_BACKUP, Boolean.TRUE); + // Remembers working directory of last import defaults.put(IMPORT_WORKING_DIRECTORY, USER_HOME); defaults.put(PREFS_EXPORT_PATH, USER_HOME); @@ -2113,8 +2120,10 @@ public FilePreferences getFilePreferences() { getBoolean(DOWNLOAD_LINKED_FILES), getBoolean(FULLTEXT_INDEX_LINKED_FILES), Path.of(get(WORKING_DIRECTORY)), - ExternalFileTypes.fromString(get(EXTERNAL_FILE_TYPES)) - ); + ExternalFileTypes.fromString(get(EXTERNAL_FILE_TYPES)), + getBoolean(CREATE_BACKUP), + // We choose the data directory, because a ".bak" file should survive cache cleanups + getPath(BACKUP_DIRECTORY, BackupFileUtil.getAppDataBackupDir())); EasyBind.listen(filePreferences.mainFileDirectoryProperty(), (obs, oldValue, newValue) -> put(MAIN_FILE_DIRECTORY, newValue)); EasyBind.listen(filePreferences.storeFilesRelativeToBibFileProperty(), (obs, oldValue, newValue) -> putBoolean(STORE_RELATIVE_TO_BIB, newValue)); @@ -2125,6 +2134,8 @@ public FilePreferences getFilePreferences() { EasyBind.listen(filePreferences.workingDirectoryProperty(), (obs, oldValue, newValue) -> put(WORKING_DIRECTORY, newValue.toString())); filePreferences.getExternalFileTypes().addListener((SetChangeListener) c -> put(EXTERNAL_FILE_TYPES, ExternalFileTypes.toStringList(filePreferences.getExternalFileTypes()))); + EasyBind.listen(filePreferences.createBackupProperty(), (obs, oldValue, newValue) -> putBoolean(CREATE_BACKUP, newValue)); + EasyBind.listen(filePreferences.backupDirectoryProperty(), (obs, oldValue, newValue) -> put(BACKUP_DIRECTORY, newValue.toString())); return filePreferences; } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 75b6a7a35d9..985647652c5 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2541,3 +2541,5 @@ Delete\ %0\ files=Delete %0 files Delete\ %0\ files\ permanently\ from\ disk,\ or\ just\ remove\ the\ files\ from\ the\ entry?\ Pressing\ Delete\ will\ delete\ the\ files\ permanently\ from\ disk.=Delete %0 files permanently from disk, or just remove the files from the entry? Pressing Delete will delete the files permanently from disk. Error\ accessing\ file\ '%0'.=Error accessing file '%0'. This\ operation\ requires\ selected\ linked\ files.=This operation requires selected linked files. + +Create\ backup=Create backup \ No newline at end of file diff --git a/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerDiscardedTest.java b/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerDiscardedTest.java index 7d90122bd86..ee60abcc067 100644 --- a/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerDiscardedTest.java +++ b/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerDiscardedTest.java @@ -40,10 +40,11 @@ class BackupManagerDiscardedTest { private SaveConfiguration saveConfiguration; private PreferencesService preferencesService; private BibEntryTypesManager bibEntryTypesManager; + private Path backupDir; @BeforeEach public void setup(@TempDir Path tempDir) throws Exception { - Path backupDir = tempDir.resolve("backups"); + this.backupDir = tempDir.resolve("backups"); Files.createDirectories(backupDir); testBib = tempDir.resolve("test.bib"); @@ -84,14 +85,14 @@ private void databaseModification() { } private void makeBackup() { - backupManager.determineBackupPathForNewBackup().ifPresent(backupManager::performBackup); + backupManager.determineBackupPathForNewBackup(backupDir).ifPresent(path -> backupManager.performBackup(path)); } @Test public void noDiscardingAChangeLeadsToNewerBackupBeReported() throws Exception { databaseModification(); makeBackup(); - assertTrue(BackupManager.backupFileDiffers(testBib)); + assertTrue(BackupManager.backupFileDiffers(testBib, backupDir)); } @Test @@ -99,14 +100,14 @@ public void noDiscardingASavedChange() throws Exception { databaseModification(); makeBackup(); saveDatabase(); - assertFalse(BackupManager.backupFileDiffers(testBib)); + assertFalse(BackupManager.backupFileDiffers(testBib, backupDir)); } @Test public void discardingAChangeLeadsToNewerBackupToBeIgnored() throws Exception { databaseModification(); makeBackup(); - backupManager.discardBackup(); - assertFalse(BackupManager.backupFileDiffers(testBib)); + backupManager.discardBackup(backupDir); + assertFalse(BackupManager.backupFileDiffers(testBib, backupDir)); } } diff --git a/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerTest.java b/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerTest.java index e99e3a3d8e8..7bf69d79f91 100644 --- a/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerTest.java +++ b/src/test/java/org/jabref/logic/autosaveandbackup/BackupManagerTest.java @@ -4,22 +4,47 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import org.jabref.logic.util.BackupFileType; import org.jabref.logic.util.io.BackupFileUtil; - +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.groups.event.GroupUpdatedEvent; +import org.jabref.model.metadata.MetaData; +import org.jabref.model.metadata.event.MetaDataChangedEvent; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Answers; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class BackupManagerTest { + Path backupDir; + + @BeforeEach + void setup(@TempDir Path tempDir) { + backupDir = tempDir.resolve("backup"); + } + @Test public void backupFileNameIsCorrectlyGeneratedInAppDataDirectory() { Path bibPath = Path.of("tmp", "test.bib"); - Path bakPath = BackupManager.getBackupPathForNewBackup(bibPath); + backupDir = BackupFileUtil.getAppDataBackupDir(); + Path bakPath = BackupManager.getBackupPathForNewBackup(bibPath, backupDir); // Pattern is "27182d3c--test.bib--", but the hashing is implemented differently on Linux than on Windows assertNotEquals("", bakPath); @@ -28,29 +53,29 @@ public void backupFileNameIsCorrectlyGeneratedInAppDataDirectory() { @Test public void backupFileIsEqualForNonExistingBackup() throws Exception { Path originalFile = Path.of(BackupManagerTest.class.getResource("no-autosave.bib").toURI()); - assertFalse(BackupManager.backupFileDiffers(originalFile)); + assertFalse(BackupManager.backupFileDiffers(originalFile, backupDir)); } @Test public void backupFileIsEqual() throws Exception { // Prepare test: Create backup file on "right" path Path source = Path.of(BackupManagerTest.class.getResource("no-changes.bib.bak").toURI()); - Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of(BackupManagerTest.class.getResource("no-changes.bib").toURI()), BackupFileType.BACKUP); + Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of(BackupManagerTest.class.getResource("no-changes.bib").toURI()), BackupFileType.BACKUP, backupDir); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); Path originalFile = Path.of(BackupManagerTest.class.getResource("no-changes.bib").toURI()); - assertFalse(BackupManager.backupFileDiffers(originalFile)); + assertFalse(BackupManager.backupFileDiffers(originalFile, backupDir)); } @Test public void backupFileDiffers() throws Exception { // Prepare test: Create backup file on "right" path Path source = Path.of(BackupManagerTest.class.getResource("changes.bib.bak").toURI()); - Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()), BackupFileType.BACKUP); + Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()), BackupFileType.BACKUP, backupDir); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); Path originalFile = Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()); - assertTrue(BackupManager.backupFileDiffers(originalFile)); + assertTrue(BackupManager.backupFileDiffers(originalFile, backupDir)); } @Test @@ -60,13 +85,13 @@ public void correctBackupFileDeterminedForMultipleBakFiles() throws Exception { // Prepare test: Create backup files on "right" path // most recent file does not have any changes - Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(noChangesBib, BackupFileType.BACKUP); + Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(noChangesBib, BackupFileType.BACKUP, backupDir); Files.copy(noChangesBibBak, target, StandardCopyOption.REPLACE_EXISTING); // create "older" .bak files containing changes for (int i = 0; i < 10; i++) { Path changesBibBak = Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()); - Path directory = BackupFileUtil.getAppDataBackupDir(); + Path directory = backupDir; String timeSuffix = "2020-02-03--00.00.0" + Integer.toString(i); String fileName = BackupFileUtil.getUniqueFilePrefix(noChangesBib) + "--no-changes.bib--" + timeSuffix + ".bak"; target = directory.resolve(fileName); @@ -74,7 +99,7 @@ public void correctBackupFileDeterminedForMultipleBakFiles() throws Exception { } Path originalFile = noChangesBib; - assertFalse(BackupManager.backupFileDiffers(originalFile)); + assertFalse(BackupManager.backupFileDiffers(originalFile, backupDir)); } @Test @@ -82,10 +107,10 @@ public void bakFileWithNewerTimeStampLeadsToDiff() throws Exception { Path changesBib = Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()); Path changesBibBak = Path.of(BackupManagerTest.class.getResource("changes.bib.bak").toURI()); - Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(changesBib, BackupFileType.BACKUP); + Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(changesBib, BackupFileType.BACKUP, backupDir); Files.copy(changesBibBak, target, StandardCopyOption.REPLACE_EXISTING); - assertTrue(BackupManager.backupFileDiffers(changesBib)); + assertTrue(BackupManager.backupFileDiffers(changesBib, backupDir)); } @Test @@ -93,12 +118,63 @@ public void bakFileWithOlderTimeStampDoesNotLeadToDiff() throws Exception { Path changesBib = Path.of(BackupManagerTest.class.getResource("changes.bib").toURI()); Path changesBibBak = Path.of(BackupManagerTest.class.getResource("changes.bib.bak").toURI()); - Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(changesBib, BackupFileType.BACKUP); + Path target = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(changesBib, BackupFileType.BACKUP, backupDir); Files.copy(changesBibBak, target, StandardCopyOption.REPLACE_EXISTING); // Make .bak file very old Files.setLastModifiedTime(target, FileTime.fromMillis(0)); - assertFalse(BackupManager.backupFileDiffers(changesBib)); + assertFalse(BackupManager.backupFileDiffers(changesBib, backupDir)); + } + + @Test + public void shouldNotCreateABackup(@TempDir Path customDir) throws Exception { + Path backupDir = customDir.resolve("subBackupDir"); + Files.createDirectories(backupDir); + + var database = new BibDatabaseContext(new BibDatabase()); + database.setDatabasePath(customDir.resolve("Bibfile.bib")); + + var preferences = mock(PreferencesService.class, Answers.RETURNS_DEEP_STUBS); + var filePreferences = mock(FilePreferences.class); + when(preferences.getFilePreferences()).thenReturn(filePreferences); + when(filePreferences.getBackupDirectory()).thenReturn(backupDir); + + BackupManager manager = BackupManager.start(database, mock(BibEntryTypesManager.class, Answers.RETURNS_DEEP_STUBS), preferences); + manager.listen(new MetaDataChangedEvent(new MetaData())); + + BackupManager.shutdown(database, backupDir, false); + + List files = Files.list(backupDir).toList(); + assertEquals(Collections.emptyList(), files); + } + + @Test + public void shouldCreateABackup(@TempDir Path customDir) throws Exception { + Path backupDir = customDir.resolve("subBackupDir"); + Files.createDirectories(backupDir); + + var database = new BibDatabaseContext(new BibDatabase()); + database.setDatabasePath(customDir.resolve("Bibfile.bib")); + + var preferences = mock(PreferencesService.class, Answers.RETURNS_DEEP_STUBS); + var filePreferences = mock(FilePreferences.class); + when(preferences.getFilePreferences()).thenReturn(filePreferences); + when(filePreferences.getBackupDirectory()).thenReturn(backupDir); + when(filePreferences.shouldCreateBackup()).thenReturn(true); + + BackupManager manager = BackupManager.start(database, mock(BibEntryTypesManager.class, Answers.RETURNS_DEEP_STUBS), preferences); + manager.listen(new MetaDataChangedEvent(new MetaData())); + + Optional fullBackupPath = manager.determineBackupPathForNewBackup(backupDir); + fullBackupPath.ifPresent(manager::performBackup); + manager.listen(new GroupUpdatedEvent(new MetaData())); + + BackupManager.shutdown(database, backupDir, true); + + List files = Files.list(backupDir).sorted().toList(); + // we only know the first backup path because the second one is created on shutdown + // due to timing issues we cannot test that reliable + assertEquals(fullBackupPath.get(), files.get(0)); } } diff --git a/src/test/java/org/jabref/logic/util/io/BackupFileUtilTest.java b/src/test/java/org/jabref/logic/util/io/BackupFileUtilTest.java index f44e6a45e1e..bedea46b580 100644 --- a/src/test/java/org/jabref/logic/util/io/BackupFileUtilTest.java +++ b/src/test/java/org/jabref/logic/util/io/BackupFileUtilTest.java @@ -6,7 +6,9 @@ import org.jabref.logic.util.BackupFileType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Answers; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -16,6 +18,13 @@ public class BackupFileUtilTest { + Path backupDir; + + @BeforeEach + void setup(@TempDir Path tempDir) { + backupDir = tempDir.resolve("backup"); + } + @Test void uniqueFilePrefix() { // We cannot test for a concrete hash code, because hashing implementation differs from environment to environment @@ -25,18 +34,20 @@ void uniqueFilePrefix() { @Test void getPathOfBackupFileAndCreateDirectoryReturnsAppDirectoryInCaseOfNoError() { String start = BackupFileUtil.getAppDataBackupDir().toString(); - String result = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of("test.bib"), BackupFileType.BACKUP).toString(); + backupDir = BackupFileUtil.getAppDataBackupDir(); + String result = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(Path.of("test.bib"), BackupFileType.BACKUP, backupDir).toString(); // We just check the prefix assertEquals(start, result.substring(0, start.length())); } @Test void getPathOfBackupFileAndCreateDirectoryReturnsSameDirectoryInCaseOfException() { + backupDir = BackupFileUtil.getAppDataBackupDir(); try (MockedStatic files = Mockito.mockStatic(Files.class, Answers.RETURNS_DEEP_STUBS)) { files.when(() -> Files.createDirectories(BackupFileUtil.getAppDataBackupDir())) .thenThrow(new IOException()); Path testPath = Path.of("tmp", "test.bib"); - Path result = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(testPath, BackupFileType.BACKUP); + Path result = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(testPath, BackupFileType.BACKUP, backupDir); // The intended fallback behavior is to put the .bak file in the same directory as the .bib file assertEquals(Path.of("tmp", "test.bib.bak"), result); }