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

Allow specifing database backup paths. #7035

Merged
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
20 changes: 20 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@
<source>Monochrome</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select backup storage directory</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidgetGeneral</name>
Expand Down Expand Up @@ -440,6 +444,22 @@
<source>Directly write to database file (dangerous)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Backup destination</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Specifies the database backup file location. Occurences of &quot;{DB_FILENAME}&quot; are replaced with the filename of the saved database without extension. {TIME:&lt;format&gt;} is replaced with the backup time, see https://doc.qt.io/qt-5/qdatetime.html#toString. &lt;format&gt; defaults to format string &quot;dd_MM_yyyy_hh-mm-ss&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>{DB_FILENAME}.old.kdbx</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidgetSecurity</name>
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Add.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
libklein marked this conversation as resolved.
Show resolved Hide resolved
err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/AddGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Create.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Merge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Move.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Remove.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/RemoveGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/core/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Config : public QObject
AutoSaveOnExit,
AutoSaveNonDataChanges,
BackupBeforeSave,
BackupFilePathPattern,
UseAtomicSaves,
UseDirectWriteSaves,
SearchLimitGroup,
Expand Down Expand Up @@ -195,6 +196,7 @@ class Config : public QObject

~Config() override;
QVariant get(ConfigKey key);
QVariant getDefault(ConfigKey key);
QString getFileName();
void set(ConfigKey key, const QVariant& value);
void remove(ConfigKey key);
Expand Down
56 changes: 28 additions & 28 deletions src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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);
}

/**
Expand All @@ -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()) {
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
}
Expand Down
13 changes: 8 additions & 5 deletions src/core/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ class Database : public ModifiableObject
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);

Expand Down Expand Up @@ -203,9 +206,9 @@ public slots:
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();

Expand Down
36 changes: 36 additions & 0 deletions src/core/Tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/core/Tools.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading