diff options
author | Patrick Klein <42714034+libklein@users.noreply.github.com> | 2021-11-08 01:41:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-08 01:41:17 +0300 |
commit | 84ff6a13f9cd11214396deb31d54955abbcd7b0b (patch) | |
tree | 0fa0d84475f9f79c4a8ce12d8582715b0f084940 /src | |
parent | 8d7e4918109b1e40660e2988e79e1cf15e899580 (diff) |
Allow specifing database backup paths. (#7035)
- Default backupFilePath is '{DB_FILENAME}.old.kdbx' to conform to existing standards
- Implement backupPathPattern tests.
- Show tooltip on how to format database backup location text field.
Diffstat (limited to 'src')
-rw-r--r-- | src/cli/Add.cpp | 2 | ||||
-rw-r--r-- | src/cli/AddGroup.cpp | 2 | ||||
-rw-r--r-- | src/cli/Create.cpp | 2 | ||||
-rw-r--r-- | src/cli/Edit.cpp | 2 | ||||
-rw-r--r-- | src/cli/Import.cpp | 2 | ||||
-rw-r--r-- | src/cli/Merge.cpp | 2 | ||||
-rw-r--r-- | src/cli/Move.cpp | 2 | ||||
-rw-r--r-- | src/cli/Remove.cpp | 2 | ||||
-rw-r--r-- | src/cli/RemoveGroup.cpp | 2 | ||||
-rw-r--r-- | src/core/Config.cpp | 6 | ||||
-rw-r--r-- | src/core/Config.h | 2 | ||||
-rw-r--r-- | src/core/Database.cpp | 56 | ||||
-rw-r--r-- | src/core/Database.h | 13 | ||||
-rw-r--r-- | src/core/Tools.cpp | 36 | ||||
-rw-r--r-- | src/core/Tools.h | 2 | ||||
-rw-r--r-- | src/gui/ApplicationSettingsWidget.cpp | 25 | ||||
-rw-r--r-- | src/gui/ApplicationSettingsWidget.h | 1 | ||||
-rw-r--r-- | src/gui/ApplicationSettingsWidgetGeneral.ui | 50 | ||||
-rw-r--r-- | src/gui/DatabaseWidget.cpp | 25 |
19 files changed, 188 insertions, 46 deletions
diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index e4bde05f3..409d2a0aa 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -121,7 +121,7 @@ int Add::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<Q } QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/AddGroup.cpp b/src/cli/AddGroup.cpp index 46837c77b..8fc9ceb39 100644 --- a/src/cli/AddGroup.cpp +++ b/src/cli/AddGroup.cpp @@ -63,7 +63,7 @@ int AddGroup::executeWithDatabase(QSharedPointer<Database> database, QSharedPoin newGroup->setParent(parentGroup); QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index 867471697..4f820d000 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -165,7 +165,7 @@ int Create::execute(const QStringList& arguments) } QString errorMessage; - if (!db->saveAs(databaseFilename, Database::Atomic, false, &errorMessage)) { + if (!db->saveAs(databaseFilename, Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Edit.cpp b/src/cli/Edit.cpp index 8941df5c5..ad1d701f7 100644 --- a/src/cli/Edit.cpp +++ b/src/cli/Edit.cpp @@ -126,7 +126,7 @@ int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< entry->endUpdate(); QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Writing the database failed: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index 4c1e99437..432507d20 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -75,7 +75,7 @@ int Import::execute(const QStringList& arguments) return EXIT_FAILURE; } - if (!db->saveAs(dbPath, Database::Atomic, false, &errorMessage)) { + if (!db->saveAs(dbPath, Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index 41e18ca0b..a80f454bb 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -95,7 +95,7 @@ int Merge::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer if (!changeList.isEmpty() && !parser->isSet(Merge::DryRunOption)) { QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Unable to save database to file : %1").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Move.cpp b/src/cli/Move.cpp index 350cc4b82..990f45691 100644 --- a/src/cli/Move.cpp +++ b/src/cli/Move.cpp @@ -65,7 +65,7 @@ int Move::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< entry->endUpdate(); QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/Remove.cpp b/src/cli/Remove.cpp index 6ed667fa1..ed6a27502 100644 --- a/src/cli/Remove.cpp +++ b/src/cli/Remove.cpp @@ -53,7 +53,7 @@ int Remove::executeWithDatabase(QSharedPointer<Database> database, QSharedPointe }; QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Unable to save database to file: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/cli/RemoveGroup.cpp b/src/cli/RemoveGroup.cpp index e1d1d7e69..df6bf4cfa 100644 --- a/src/cli/RemoveGroup.cpp +++ b/src/cli/RemoveGroup.cpp @@ -63,7 +63,7 @@ int RemoveGroup::executeWithDatabase(QSharedPointer<Database> database, QSharedP }; QString errorMessage; - if (!database->save(Database::Atomic, false, &errorMessage)) { + if (!database->save(Database::Atomic, QString(), &errorMessage)) { err << QObject::tr("Unable to save database to file: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 54fc59c81..a6d179504 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -61,6 +61,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = { {Config::AutoSaveOnExit,{QS("AutoSaveOnExit"), Roaming, true}}, {Config::AutoSaveNonDataChanges,{QS("AutoSaveNonDataChanges"), Roaming, true}}, {Config::BackupBeforeSave,{QS("BackupBeforeSave"), Roaming, false}}, + {Config::BackupFilePathPattern,{QS("BackupFilePathPattern"), Roaming, QString("{DB_FILENAME}.old.kdbx")}}, {Config::UseAtomicSaves,{QS("UseAtomicSaves"), Roaming, true}}, {Config::UseDirectWriteSaves,{QS("UseDirectWriteSaves"), Local, false}}, {Config::SearchLimitGroup,{QS("SearchLimitGroup"), Roaming, false}}, @@ -229,6 +230,11 @@ QVariant Config::get(ConfigKey key) return m_settings->value(cfg.name, defaultValue); } +QVariant Config::getDefault(Config::ConfigKey key) +{ + return configStrings[key].defaultValue; +} + bool Config::hasAccessError() { return m_settings->status() & QSettings::AccessError; diff --git a/src/core/Config.h b/src/core/Config.h index 8e5c14e35..1be8699ca 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -43,6 +43,7 @@ public: AutoSaveOnExit, AutoSaveNonDataChanges, BackupBeforeSave, + BackupFilePathPattern, UseAtomicSaves, UseDirectWriteSaves, SearchLimitGroup, @@ -195,6 +196,7 @@ public: ~Config() override; QVariant get(ConfigKey key); + QVariant getDefault(ConfigKey key); QString getFileName(); void set(ConfigKey key, const QVariant& value); void remove(ConfigKey key); diff --git a/src/core/Database.cpp b/src/core/Database.cpp index b30c3623a..5738a3b0a 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -178,10 +178,10 @@ bool Database::isSaving() * * @param error error message in case of failure * @param atomic Use atomic file transactions - * @param backup Backup the existing database file, if exists + * @param backupFilePath Absolute file path to write the backup file to. Pass an empty QString to disable backup. * @return true on success */ -bool Database::save(SaveAction action, bool backup, QString* error) +bool Database::save(SaveAction action, const QString& backupFilePath, QString* error) { Q_ASSERT(!m_data.filePath.isEmpty()); if (m_data.filePath.isEmpty()) { @@ -191,7 +191,7 @@ bool Database::save(SaveAction action, bool backup, QString* error) return false; } - return saveAs(m_data.filePath, action, backup, error); + return saveAs(m_data.filePath, action, backupFilePath, error); } /** @@ -209,10 +209,11 @@ bool Database::save(SaveAction action, bool backup, QString* error) * @param filePath Absolute path of the file to save * @param error error message in case of failure * @param atomic Use atomic file transactions - * @param backup Backup the existing database file, if exists + * @param backupFilePath Absolute path to the location where the backup should be stored. Passing an empty string + * disables backup. * @return true on success */ -bool Database::saveAs(const QString& filePath, SaveAction action, bool backup, QString* error) +bool Database::saveAs(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error) { // Disallow overlapping save operations if (isSaving()) { @@ -260,7 +261,7 @@ bool Database::saveAs(const QString& filePath, SaveAction action, bool backup, Q QFileInfo fileInfo(filePath); auto realFilePath = fileInfo.exists() ? fileInfo.canonicalFilePath() : fileInfo.absoluteFilePath(); bool isNewFile = !QFile::exists(realFilePath); - bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, action, backup, error); }); + bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, action, backupFilePath, error); }); if (ok) { markAsClean(); setFilePath(filePath); @@ -276,10 +277,10 @@ bool Database::saveAs(const QString& filePath, SaveAction action, bool backup, Q return ok; } -bool Database::performSave(const QString& filePath, SaveAction action, bool backup, QString* error) +bool Database::performSave(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error) { - if (backup) { - backupDatabase(filePath); + if (!backupFilePath.isNull()) { + backupDatabase(filePath, backupFilePath); } #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) @@ -337,7 +338,7 @@ bool Database::performSave(const QString& filePath, SaveAction action, bool back tempFile.setFileTime(createTime, QFile::FileBirthTime); #endif return true; - } else if (!backup || !restoreDatabase(filePath)) { + } else if (backupFilePath.isEmpty() || !restoreDatabase(filePath, backupFilePath)) { // Failed to copy new database in place, and // failed to restore from backup or backups disabled tempFile.setAutoRemove(false); @@ -485,23 +486,26 @@ void Database::releaseData() } /** - * Remove the old backup and replace it with a new one - * backups are named <filename>.old.<extension> + * Remove the old backup and replace it with a new one. Backup name is taken from destinationFilePath. + * Non-existing parent directories will be created automatically. * * @param filePath Path to the file to backup + * @param destinationFilePath Path to the backup destination file * @return true on success */ -bool Database::backupDatabase(const QString& filePath) +bool Database::backupDatabase(const QString& filePath, const QString& destinationFilePath) { - static auto re = QRegularExpression("(\\.[^.]+)$"); - - auto match = re.match(filePath); - auto backupFilePath = filePath; + // Ensure that the path to write to actually exists + auto parentDirectory = QFileInfo(destinationFilePath).absoluteDir(); + if (!parentDirectory.exists()) { + if (!QDir().mkpath(parentDirectory.absolutePath())) { + return false; + } + } auto perms = QFile::permissions(filePath); - backupFilePath = backupFilePath.replace(re, "") + ".old" + match.captured(1); - QFile::remove(backupFilePath); - bool res = QFile::copy(filePath, backupFilePath); - QFile::setPermissions(backupFilePath, perms); + QFile::remove(destinationFilePath); + bool res = QFile::copy(filePath, destinationFilePath); + QFile::setPermissions(destinationFilePath, perms); return res; } @@ -513,17 +517,13 @@ bool Database::backupDatabase(const QString& filePath) * @param filePath Path to the file to restore * @return true on success */ -bool Database::restoreDatabase(const QString& filePath) +bool Database::restoreDatabase(const QString& filePath, const QString& fromBackupFilePath) { - static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$"); - - auto match = re.match(filePath); auto perms = QFile::permissions(filePath); - auto backupFilePath = match.captured(1) + ".old" + match.captured(2); // Only try to restore if the backup file actually exists - if (QFile::exists(backupFilePath)) { + if (QFile::exists(fromBackupFilePath)) { QFile::remove(filePath); - if (QFile::copy(backupFilePath, filePath)) { + if (QFile::copy(fromBackupFilePath, filePath)) { return QFile::setPermissions(filePath, perms); } } diff --git a/src/core/Database.h b/src/core/Database.h index 24bb79db2..c42025f85 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -79,8 +79,11 @@ public: QSharedPointer<const CompositeKey> key, QString* error = nullptr, bool readOnly = false); - bool save(SaveAction action = Atomic, bool backup = false, QString* error = nullptr); - bool saveAs(const QString& filePath, SaveAction action = Atomic, bool backup = false, QString* error = nullptr); + bool save(SaveAction action = Atomic, const QString& backupFilePath = QString(), QString* error = nullptr); + bool saveAs(const QString& filePath, + SaveAction action = Atomic, + const QString& backupFilePath = QString(), + QString* error = nullptr); bool extract(QByteArray&, QString* error = nullptr); bool import(const QString& xmlExportPath, QString* error = nullptr); @@ -203,9 +206,9 @@ private: void createRecycleBin(); bool writeDatabase(QIODevice* device, QString* error = nullptr); - bool backupDatabase(const QString& filePath); - bool restoreDatabase(const QString& filePath); - bool performSave(const QString& filePath, SaveAction flags, bool backup, QString* error); + bool backupDatabase(const QString& filePath, const QString& destinationFilePath); + bool restoreDatabase(const QString& filePath, const QString& fromBackupFilePath); + bool performSave(const QString& filePath, SaveAction flags, const QString& backupFilePath, QString* error); void startModifiedTimer(); void stopModifiedTimer(); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 58a38f433..7c6d52a59 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -22,7 +22,10 @@ #include "config-keepassx.h" #include "git-info.h" +#include "core/Clock.h" + #include <QElapsedTimer> +#include <QFileInfo> #include <QImageReader> #include <QLocale> #include <QMetaProperty> @@ -376,4 +379,37 @@ namespace Tools } return result; } + + QString substituteBackupFilePath(QString pattern, const QString& databasePath) + { + // Fail if substitution fails + if (databasePath.isEmpty()) { + return {}; + } + + // Replace backup pattern + QFileInfo dbFileInfo(databasePath); + QString baseName = dbFileInfo.completeBaseName(); + + pattern.replace(QString("{DB_FILENAME}"), baseName); + + auto re = QRegularExpression(R"(\{TIME(?::([^\\]*))?\})"); + auto match = re.match(pattern); + while (match.hasMatch()) { + // Extract time format specifier + auto formatSpecifier = QString("dd_MM_yyyy_hh-mm-ss"); + if (!match.captured(1).isEmpty()) { + formatSpecifier = match.captured(1); + } + auto replacement = Clock::currentDateTime().toString(formatSpecifier); + pattern.replace(match.capturedStart(), match.capturedLength(), replacement); + match = re.match(pattern); + } + + // Replace escaped braces + pattern.replace("\\{", "{"); + pattern.replace("\\}", "}"); + + return pattern; + } } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 8ebcef1c5..cf3b3593d 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -75,6 +75,8 @@ namespace Tools } QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"}); + + QString substituteBackupFilePath(QString pattern, const QString& databasePath); } // namespace Tools #endif // KEEPASSX_TOOLS_H diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 100258120..ac96d23b4 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -19,6 +19,8 @@ #include "ApplicationSettingsWidget.h" #include "ui_ApplicationSettingsWidgetGeneral.h" #include "ui_ApplicationSettingsWidgetSecurity.h" +#include <QDesktopServices> +#include <QDir> #include "config-keepassx.h" @@ -28,6 +30,7 @@ #include "gui/MainWindow.h" #include "gui/osutils/OSUtils.h" +#include "FileDialog.h" #include "MessageBox.h" #ifdef Q_OS_MACOS #include "touchid/TouchID.h" @@ -112,6 +115,12 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) connect(m_generalUi->useAlternativeSaveCheckBox, SIGNAL(toggled(bool)), m_generalUi->alternativeSaveComboBox, SLOT(setEnabled(bool))); + connect(m_generalUi->backupBeforeSaveCheckBox, SIGNAL(toggled(bool)), + m_generalUi->backupFilePath, SLOT(setEnabled(bool))); + connect(m_generalUi->backupBeforeSaveCheckBox, SIGNAL(toggled(bool)), + m_generalUi->backupFilePathPicker, SLOT(setEnabled(bool))); + connect(m_generalUi->backupFilePathPicker, SIGNAL(pressed()), SLOT(selectBackupDirectory())); + connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); connect(m_secUi->clearSearchCheckBox, SIGNAL(toggled(bool)), @@ -188,6 +197,9 @@ void ApplicationSettingsWidget::loadSettings() m_generalUi->autoSaveOnExitCheckBox->setChecked(config()->get(Config::AutoSaveOnExit).toBool()); m_generalUi->autoSaveNonDataChangesCheckBox->setChecked(config()->get(Config::AutoSaveNonDataChanges).toBool()); m_generalUi->backupBeforeSaveCheckBox->setChecked(config()->get(Config::BackupBeforeSave).toBool()); + + m_generalUi->backupFilePath->setText(config()->get(Config::BackupFilePathPattern).toString()); + m_generalUi->useAlternativeSaveCheckBox->setChecked(!config()->get(Config::UseAtomicSaves).toBool()); m_generalUi->alternativeSaveComboBox->setCurrentIndex(config()->get(Config::UseDirectWriteSaves).toBool() ? 1 : 0); m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get(Config::AutoReloadOnChange).toBool()); @@ -326,6 +338,9 @@ void ApplicationSettingsWidget::saveSettings() config()->set(Config::AutoSaveOnExit, m_generalUi->autoSaveOnExitCheckBox->isChecked()); config()->set(Config::AutoSaveNonDataChanges, m_generalUi->autoSaveNonDataChangesCheckBox->isChecked()); config()->set(Config::BackupBeforeSave, m_generalUi->backupBeforeSaveCheckBox->isChecked()); + + config()->set(Config::BackupFilePathPattern, m_generalUi->backupFilePath->text()); + config()->set(Config::UseAtomicSaves, !m_generalUi->useAlternativeSaveCheckBox->isChecked()); config()->set(Config::UseDirectWriteSaves, m_generalUi->alternativeSaveComboBox->currentIndex() == 1); config()->set(Config::AutoReloadOnChange, m_generalUi->autoReloadOnChangeCheckBox->isChecked()); @@ -504,3 +519,13 @@ void ApplicationSettingsWidget::checkUpdatesToggled(bool checked) { m_generalUi->checkForUpdatesIncludeBetasCheckBox->setEnabled(checked); } + +void ApplicationSettingsWidget::selectBackupDirectory() +{ + auto backupDirectory = + FileDialog::instance()->getExistingDirectory(this, tr("Select backup storage directory"), QDir::homePath()); + if (!backupDirectory.isEmpty()) { + m_generalUi->backupFilePath->setText( + QDir(backupDirectory).filePath(config()->getDefault(Config::BackupFilePathPattern).toString())); + } +}
\ No newline at end of file diff --git a/src/gui/ApplicationSettingsWidget.h b/src/gui/ApplicationSettingsWidget.h index f36e5ef12..c5f2ed7e3 100644 --- a/src/gui/ApplicationSettingsWidget.h +++ b/src/gui/ApplicationSettingsWidget.h @@ -62,6 +62,7 @@ private slots: void systrayToggled(bool checked); void rememberDatabasesToggled(bool checked); void checkUpdatesToggled(bool checked); + void selectBackupDirectory(); private: QWidget* const m_secWidget; diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 0e18dfde6..7729e7c8f 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -58,8 +58,8 @@ <rect> <x>0</x> <y>0</y> - <width>581</width> - <height>924</height> + <width>664</width> + <height>1215</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_8"> @@ -279,6 +279,52 @@ </widget> </item> <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Backup destination</string> + </property> + <property name="buddy"> + <cstring>backupFilePath</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="backupFilePath"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Specifies the database backup file location. Occurences of "{DB_FILENAME}" are replaced with the filename of the saved database without extension. {TIME:<format>} is replaced with the backup time, see https://doc.qt.io/qt-5/qdatetime.html#toString. <format> defaults to format string "dd_MM_yyyy_hh-mm-ss".</string> + </property> + <property name="placeholderText"> + <string>{DB_FILENAME}.old.kdbx</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="backupFilePathPicker"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Choose...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"/> + </item> + <item> <widget class="QCheckBox" name="useAlternativeSaveCheckBox"> <property name="text"> <string>Use alternative saving method (may solve problems with Dropbox, Google Drive, GVFS, etc.)</string> diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 7a7d961ae..6d88a6613 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -27,6 +27,7 @@ #include <QProcess> #include <QSplitter> #include <QTextEdit> +#include <core/Tools.h> #include "autotype/AutoType.h" #include "core/EntrySearcher.h" @@ -1879,11 +1880,31 @@ bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName) } } + QString backupFilePath; + if (config()->get(Config::BackupBeforeSave).toBool()) { + backupFilePath = config()->get(Config::BackupFilePathPattern).toString(); + // Fall back to default + if (backupFilePath.isEmpty()) { + backupFilePath = config()->getDefault(Config::BackupFilePathPattern).toString(); + } + + QFileInfo dbFileInfo(m_db->filePath()); + backupFilePath = Tools::substituteBackupFilePath(backupFilePath, dbFileInfo.canonicalFilePath()); + if (!backupFilePath.isNull()) { + // Note that we cannot guarantee that backupFilePath is actually a valid filename. QT currently provides + // no function for this. Moreover, we don't check if backupFilePath is a file and not a directory. + // If this isn't the case, just let the backup fail. + if (QDir::isRelativePath(backupFilePath)) { + backupFilePath = QDir::cleanPath(dbFileInfo.absolutePath() + QDir::separator() + backupFilePath); + } + } + } + bool ok; if (fileName.isEmpty()) { - ok = m_db->save(saveAction, config()->get(Config::BackupBeforeSave).toBool(), &errorMessage); + ok = m_db->save(saveAction, backupFilePath, &errorMessage); } else { - ok = m_db->saveAs(fileName, saveAction, config()->get(Config::BackupBeforeSave).toBool(), &errorMessage); + ok = m_db->saveAs(fileName, saveAction, backupFilePath, &errorMessage); } // Return control |