Skip to content

Commit

Permalink
Merge pull request #1385 from keepassxreboot/refactor/saving
Browse files Browse the repository at this point in the history
Correct saving files to DropBox/Drive/OneDrive
  • Loading branch information
phoerious authored Jan 28, 2018
2 parents 490f771 + 8c8a61d commit bed921c
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 271 deletions.
4 changes: 3 additions & 1 deletion src/core/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ void Config::init(const QString& fileName)
m_defaults.insert("RememberLastDatabases", true);
m_defaults.insert("RememberLastKeyFiles", true);
m_defaults.insert("OpenPreviousDatabasesOnStartup", true);
m_defaults.insert("AutoSaveAfterEveryChange", false);
m_defaults.insert("AutoSaveAfterEveryChange", true);
m_defaults.insert("AutoReloadOnChange", true);
m_defaults.insert("AutoSaveOnExit", false);
m_defaults.insert("BackupBeforeSave", false);
m_defaults.insert("UseAtomicSaves", true);
m_defaults.insert("SearchLimitGroup", false);
m_defaults.insert("MinimizeOnCopy", false);
m_defaults.insert("UseGroupIconOnEntryCreation", false);
Expand Down
112 changes: 92 additions & 20 deletions src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <QFile>
#include <QSaveFile>
#include <QTemporaryFile>
#include <QTextStream>
#include <QTimer>
#include <QXmlStreamReader>
Expand Down Expand Up @@ -470,30 +471,101 @@ Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilenam
return Database::openDatabaseFile(databaseFilename, compositeKey);
}

QString Database::saveToFile(QString filePath)
/**
* Save the database to a file.
*
* This function uses QTemporaryFile instead of QSaveFile due to a bug
* in Qt (https://bugreports.qt.io/browse/QTBUG-57299) that may prevent
* the QSaveFile from renaming itself when using Dropbox, Drive, or OneDrive.
*
* The risk in using QTemporaryFile is that the rename function is not atomic
* and may result in loss of data if there is a crash or power loss at the
* wrong moment.
*
* @param filePath Absolute path of the file to save
* @param atomic Use atomic file transactions
* @param backup Backup the existing database file, if exists
* @return error string, if any
*/
QString Database::saveToFile(QString filePath, bool atomic, bool backup)
{
QString error;
if (atomic) {
QSaveFile saveFile(filePath);
if (saveFile.open(QIODevice::WriteOnly)) {
// write the database to the file
error = writeDatabase(&saveFile);
if (!error.isEmpty()) {
return error;
}

if (backup) {
backupDatabase(filePath);
}

if (saveFile.commit()) {
// successfully saved database file
return {};
}
}
error = saveFile.errorString();
} else {
QTemporaryFile tempFile;
if (tempFile.open()) {
// write the database to the file
error = writeDatabase(&tempFile);
if (!error.isEmpty()) {
return error;
}

tempFile.close(); // flush to disk

if (backup) {
backupDatabase(filePath);
}

// Delete the original db and move the temp file in place
QFile::remove(filePath);
if (tempFile.rename(filePath)) {
// successfully saved database file
tempFile.setAutoRemove(false);
return {};
}
}
error = tempFile.errorString();
}
// Saving failed
return error;
}

QString Database::writeDatabase(QIODevice* device)
{
KeePass2Writer writer;
QSaveFile saveFile(filePath);
if (saveFile.open(QIODevice::WriteOnly)) {
setEmitModified(false);
writer.writeDatabase(device, this);
setEmitModified(true);

// write the database to the file
setEmitModified(false);
writer.writeDatabase(&saveFile, this);
setEmitModified(true);

if (writer.hasError()) {
return writer.errorString();
}

if (saveFile.commit()) {
// successfully saved database file
return QString();
} else {
return saveFile.errorString();
}
} else {
return saveFile.errorString();
if (writer.hasError()) {
// the writer failed
return writer.errorString();
}
return {};
}

/**
* Remove the old backup and replace it with a new one
* backups are named <filename>.old.kdbx
*
* @param filePath Path to the file to backup
* @return
*/
bool Database::backupDatabase(QString filePath)
{
QString backupFilePath = filePath;
auto re = QRegularExpression("(?:\\.kdbx)?$", QRegularExpression::CaseInsensitiveOption);
backupFilePath.replace(re, ".old.kdbx");
QFile::remove(backupFilePath);
return QFile::copy(filePath, backupFilePath);
}

QSharedPointer<Kdf> Database::kdf() const
Expand Down
5 changes: 4 additions & 1 deletion src/core/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum class EntryReferenceType;
class Group;
class Metadata;
class QTimer;
class QIODevice;

struct DeletedObject
{
Expand Down Expand Up @@ -111,7 +112,7 @@ class Database : public QObject
void emptyRecycleBin();
void setEmitModified(bool value);
void merge(const Database* other);
QString saveToFile(QString filePath);
QString saveToFile(QString filePath, bool atomic = true, bool backup = false);

/**
* Returns a unique id that is only valid as long as the Database exists.
Expand Down Expand Up @@ -144,6 +145,8 @@ private slots:
Group* findGroupRecursive(const Uuid& uuid, Group* group);

void createRecycleBin();
QString writeDatabase(QIODevice* device);
bool backupDatabase(QString filePath);

Metadata* const m_metadata;
Group* m_rootGroup;
Expand Down
23 changes: 22 additions & 1 deletion src/gui/DatabaseTabWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/AsyncTask.h"
#include "format/CsvExporter.h"
#include "gui/Clipboard.h"
#include "gui/DatabaseWidget.h"
Expand All @@ -43,6 +44,7 @@ DatabaseManagerStruct::DatabaseManagerStruct()
: dbWidget(nullptr)
, modified(false)
, readOnly(false)
, saveAttempts(0)
{
}

Expand Down Expand Up @@ -322,12 +324,15 @@ bool DatabaseTabWidget::saveDatabase(Database* db, QString filePath)
}

dbStruct.dbWidget->blockAutoReload(true);
QString errorMessage = db->saveToFile(filePath);
// TODO: Make this async, but lock out the database widget to prevent re-entrance
bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
QString errorMessage = db->saveToFile(filePath, useAtomicSaves, config()->get("BackupBeforeSave").toBool());
dbStruct.dbWidget->blockAutoReload(false);

if (errorMessage.isEmpty()) {
// successfully saved database file
dbStruct.modified = false;
dbStruct.saveAttempts = 0;
dbStruct.fileInfo = QFileInfo(filePath);
dbStruct.dbWidget->databaseSaved();
updateTabName(db);
Expand All @@ -336,6 +341,22 @@ bool DatabaseTabWidget::saveDatabase(Database* db, QString filePath)
} else {
dbStruct.modified = true;
updateTabName(db);

if (++dbStruct.saveAttempts > 2 && useAtomicSaves) {
// Saving failed 3 times, issue a warning and attempt to resolve
auto choice = MessageBox::question(this, tr("Disable safe saves?"),
tr("KeePassXC has failed to save the database multiple times. "
"This is likely caused by file sync services holding a lock on "
"the save file.\nDisable safe saves and try again?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (choice == QMessageBox::Yes) {
config()->set("UseAtomicSaves", false);
return saveDatabase(db, filePath);
}
// Reset save attempts without changing anything
dbStruct.saveAttempts = 0;
}

emit messageTab(tr("Writing the database failed.").append("\n").append(errorMessage),
MessageWidget::Error);
return false;
Expand Down
1 change: 1 addition & 0 deletions src/gui/DatabaseTabWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct DatabaseManagerStruct
QFileInfo fileInfo;
bool modified;
bool readOnly;
int saveAttempts;
};

Q_DECLARE_TYPEINFO(DatabaseManagerStruct, Q_MOVABLE_TYPE);
Expand Down
4 changes: 4 additions & 0 deletions src/gui/SettingsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ void SettingsWidget::loadSettings()
config()->get("OpenPreviousDatabasesOnStartup").toBool());
m_generalUi->autoSaveAfterEveryChangeCheckBox->setChecked(config()->get("AutoSaveAfterEveryChange").toBool());
m_generalUi->autoSaveOnExitCheckBox->setChecked(config()->get("AutoSaveOnExit").toBool());
m_generalUi->backupBeforeSaveCheckBox->setChecked(config()->get("BackupBeforeSave").toBool());
m_generalUi->useAtomicSavesCheckBox->setChecked(config()->get("UseAtomicSaves").toBool());
m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get("AutoReloadOnChange").toBool());
m_generalUi->minimizeOnCopyCheckBox->setChecked(config()->get("MinimizeOnCopy").toBool());
m_generalUi->useGroupIconOnEntryCreationCheckBox->setChecked(config()->get("UseGroupIconOnEntryCreation").toBool());
Expand Down Expand Up @@ -193,6 +195,8 @@ void SettingsWidget::saveSettings()
config()->set("AutoSaveAfterEveryChange",
m_generalUi->autoSaveAfterEveryChangeCheckBox->isChecked());
config()->set("AutoSaveOnExit", m_generalUi->autoSaveOnExitCheckBox->isChecked());
config()->set("BackupBeforeSave", m_generalUi->backupBeforeSaveCheckBox->isChecked());
config()->set("UseAtomicSaves", m_generalUi->useAtomicSavesCheckBox->isChecked());
config()->set("AutoReloadOnChange", m_generalUi->autoReloadOnChangeCheckBox->isChecked());
config()->set("MinimizeOnCopy", m_generalUi->minimizeOnCopyCheckBox->isChecked());
config()->set("UseGroupIconOnEntryCreation",
Expand Down
Loading

0 comments on commit bed921c

Please sign in to comment.