Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backups: Handle discarded changes v2 #9457

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 32 additions & 23 deletions src/main/java/org/jabref/gui/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TimerTask;
Expand Down Expand Up @@ -132,7 +133,9 @@
import org.jabref.logic.undo.AddUndoableActionEvent;
import org.jabref.logic.undo.UndoChangeEvent;
import org.jabref.logic.undo.UndoRedoEvent;
import org.jabref.logic.util.BackupFileType;
import org.jabref.logic.util.OS;
import org.jabref.logic.util.io.BackupFileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.SpecialField;
Expand Down Expand Up @@ -404,8 +407,8 @@ private void tearDownJabRef(List<String> filenames) {
prefs.getGuiPreferences().getLastFilesOpened().clear();
} else {
Path focusedDatabase = getCurrentLibraryTab().getBibDatabaseContext()
.getDatabasePath()
.orElse(null);
.getDatabasePath()
.orElse(null);
prefs.getGuiPreferences().setLastFilesOpened(filenames);
prefs.getGuiPreferences().setLastFocusedFile(focusedDatabase);
}
Expand Down Expand Up @@ -608,9 +611,9 @@ public LibraryTab getLibraryTabAt(int i) {
*/
public List<LibraryTab> getLibraryTabs() {
return tabbedPane.getTabs().stream()
.filter(LibraryTab.class::isInstance)
.map(LibraryTab.class::cast)
.collect(Collectors.toList());
.filter(LibraryTab.class::isInstance)
.map(LibraryTab.class::cast)
.collect(Collectors.toList());
}

public void showLibraryTabAt(int i) {
Expand Down Expand Up @@ -1068,10 +1071,10 @@ public void addParserResult(ParserResult parserResult, boolean focusPanel) {
} else {
// only add tab if library is not already open
Optional<LibraryTab> libraryTab = getLibraryTabs().stream()
.filter(p -> p.getBibDatabaseContext()
.getDatabasePath()
.equals(parserResult.getPath()))
.findFirst();
.filter(p -> p.getBibDatabaseContext()
.getDatabasePath()
.equals(parserResult.getPath()))
.findFirst();

if (libraryTab.isPresent()) {
tabbedPane.getSelectionModel().select(libraryTab.get());
Expand Down Expand Up @@ -1114,8 +1117,8 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) {
tabbedPane.getTabs().add(libraryTab);

libraryTab.setOnCloseRequest(event -> {
libraryTab.cancelLoading();
closeTab(libraryTab);
libraryTab.getDataLoadingTask().cancel();
event.consume();
});

Expand All @@ -1128,14 +1131,17 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) {
libraryTab.getUndoManager().registerListener(new UndoRedoEventManager());
}

/**
* Opens a new tab with existing data.
* Asynchronous loading is done at {@link #createLibraryTab(BackgroundTask, Path, PreferencesService, StateManager, JabRefFrame, ThemeManager)}.
*/
private void trackOpenNewDatabase(LibraryTab libraryTab) {
Globals.getTelemetryClient().ifPresent(client -> client.trackEvent(
"OpenNewDatabase",
Map.of(),
Map.of("NumberOfEntries", (double) libraryTab.getBibDatabaseContext().getDatabase().getEntryCount())));
}

public LibraryTab addTab(BibDatabaseContext databaseContext, boolean raisePanel) {
Objects.requireNonNull(databaseContext);

LibraryTab libraryTab = new LibraryTab(databaseContext, this, prefs, stateManager, themeManager);
LibraryTab libraryTab = new LibraryTab(this, prefs, stateManager, themeManager, databaseContext, importFormatReader);
addTab(libraryTab, raisePanel);
return libraryTab;
}
Expand Down Expand Up @@ -1166,10 +1172,10 @@ public FileHistoryMenu getFileHistory() {
*/
private boolean confirmClose(LibraryTab libraryTab) {
String filename = libraryTab.getBibDatabaseContext()
.getDatabasePath()
.map(Path::toAbsolutePath)
.map(Path::toString)
.orElse(Localization.lang("untitled"));
.getDatabasePath()
.map(Path::toAbsolutePath)
.map(Path::toString)
.orElse(Localization.lang("untitled"));

ButtonType saveChanges = new ButtonType(Localization.lang("Save changes"), ButtonBar.ButtonData.YES);
ButtonType discardChanges = new ButtonType(Localization.lang("Discard changes"), ButtonBar.ButtonData.NO);
Expand All @@ -1196,6 +1202,9 @@ private boolean confirmClose(LibraryTab libraryTab) {
// Save was cancelled or an error occurred.
return false;
}
if (response.isPresent() && response.get().equals(discardChanges)) {
BackupManager.setDiscardedFileExists(true);
}
return response.isEmpty() || !response.get().equals(cancel);
}

Expand All @@ -1204,10 +1213,10 @@ private boolean confirmClose(LibraryTab libraryTab) {
*/
private Boolean confirmEmptyEntry(LibraryTab libraryTab, BibDatabaseContext context) {
String filename = libraryTab.getBibDatabaseContext()
.getDatabasePath()
.map(Path::toAbsolutePath)
.map(Path::toString)
.orElse(Localization.lang("untitled"));
.getDatabasePath()
.map(Path::toAbsolutePath)
.map(Path::toString)
.orElse(Localization.lang("untitled"));

ButtonType deleteEmptyEntries = new ButtonType(Localization.lang("Delete empty entries"), ButtonBar.ButtonData.YES);
ButtonType keepEmptyEntries = new ButtonType(Localization.lang("Keep empty entries"), ButtonBar.ButtonData.NO);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -51,6 +53,8 @@ public class BackupManager {

private static final int DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS = 19;

private static boolean discardedFileExists = false;

private static Set<BackupManager> runningInstances = new HashSet<>();

private final BibDatabaseContext bibDatabaseContext;
Expand All @@ -66,6 +70,8 @@ public class BackupManager {

private boolean needsBackup = true;



private BackupManager(BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, PreferencesService preferences) {
this.bibDatabaseContext = bibDatabaseContext;
this.entryTypesManager = entryTypesManager;
Expand Down Expand Up @@ -128,9 +134,12 @@ public static void shutdown(BibDatabaseContext bibDatabaseContext) {
*/
public static boolean backupFileDiffers(Path originalPath) {
return getLatestBackupPath(originalPath).map(latestBackupPath -> {
if(latestBackupPath.toString().endsWith("--discarded.bak")){
return false;
}
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
Expand Down Expand Up @@ -177,6 +186,10 @@ public static void restoreBackup(Path originalPath) {
}
}

public static void setDiscardedFileExists(boolean discardedFileFlag){
discardedFileExists = discardedFileFlag;
}

private Optional<Path> determineBackupPathForNewBackup() {
return bibDatabaseContext.getDatabasePath().map(BackupManager::getBackupPathForNewBackup);
}
Expand All @@ -189,6 +202,7 @@ private Optional<Path> determineBackupPathForNewBackup() {
* @param backupPath the path where the library should be backed up to
*/
private void performBackup(Path backupPath) {

if (!needsBackup) {
return;
}
Expand All @@ -206,7 +220,7 @@ private void performBackup(Path backupPath) {
// code similar to org.jabref.gui.exporter.SaveDatabaseAction.saveDatabase
GeneralPreferences generalPreferences = preferences.getGeneralPreferences();
SavePreferences savePreferences = preferences.getSavePreferences()
.withMakeBackup(false);
.withMakeBackup(false);
Charset encoding = bibDatabaseContext.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8);
// We want to have successful backups only
// Thus, we do not use a plain "FileWriter", but the "AtomicFileWriter"
Expand Down Expand Up @@ -259,33 +273,67 @@ private void startBackupTask() {

private void fillQueue() {
Path backupDir = BackupFileUtil.getAppDataBackupDir();

if (!Files.exists(backupDir)) {
return;
}

bibDatabaseContext.getDatabasePath().ifPresent(databasePath -> {
// code similar to {@link org.jabref.logic.util.io.BackupFileUtil.getPathOfLatestExisingBackupFile}
final String prefix = BackupFileUtil.getUniqueFilePrefix(databasePath) + "--" + databasePath.getFileName();

try {
final String prefix = BackupFileUtil.getUniqueFilePrefix(databasePath) + "--" + databasePath.getFileName();

List<Path> allSavFiles = Files.list(backupDir)
// just list the .sav belonging to the given targetFile
.filter(p -> p.getFileName().toString().startsWith(prefix))
.sorted().toList();
// just list the .sav belonging to the given targetFile
.filter(p -> p.getFileName().toString().startsWith(prefix))
.sorted().toList();

backupFilesQueue.addAll(allSavFiles);
} catch (IOException e) {
LOGGER.error("Could not determine most recent file", e);
}
});
}

/**
* Saves a --discarded.bak version of the original file at backup path which was discarded by user before jabRef shuts down
* This is done to let jabRef know that the last changes were discarded, when application is opened again
*/
private void saveDiscardedFile(){
bibDatabaseContext.getDatabasePath().ifPresent(databasePath -> {
getLatestBackupPath(databasePath).ifPresent(latestBackupPath -> {
String timeSuffix = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH.mm.ss"));
GeneralPreferences generalPreferences = preferences.getGeneralPreferences();
SavePreferences savePreferences = preferences.getSavePreferences()
.withMakeBackup(false);
Charset encoding = bibDatabaseContext.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8);
String backupPathString = BackupFileUtil.getUniqueFilePrefix(databasePath) + "--" + databasePath.getFileName() + "--" + timeSuffix+ "--discarded.bak";
Path backupPath = latestBackupPath.getParent().resolve(backupPathString);

try (Writer writer = new AtomicFileWriter(backupPath, encoding, false)) {
BibWriter bibWriter = new BibWriter(writer, bibDatabaseContext.getDatabase().getNewLineSeparator());
new BibtexDatabaseWriter(bibWriter, generalPreferences, savePreferences, entryTypesManager)
.saveDatabase(bibDatabaseContext);

} catch (IOException e) {
logIfCritical(backupPath, e);
}
});
});
}
/**
* Unregisters the BackupManager from the eventBus of {@link BibDatabaseContext}.
* This method should only be used when closing a database/JabRef in a normal way.
*/
private void shutdown() {
changeFilter.unregisterListener(this);
changeFilter.shutdown();

executor.shutdown();

if(discardedFileExists){
saveDiscardedFile();
}
// Ensure that backup is a recent one
determineBackupPathForNewBackup().ifPresent(this::performBackup);
}
Expand Down