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/core | |
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/core')
-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 |
6 files changed, 82 insertions, 33 deletions
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 |