After 3 failed saves, offer to disable safe saves

* User is prompted to disable safe saves after three failed attempts
* Completely retooled basic settings to group settings logically
* Added setting for "atomic saves"
This commit is contained in:
Jonathan White 2018-01-17 20:13:13 -05:00
parent d7f408e455
commit 3db9a86a4c
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
7 changed files with 384 additions and 291 deletions

View File

@ -115,6 +115,7 @@ void Config::init(const QString& fileName)
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);

View File

@ -19,6 +19,7 @@
#include "Database.h"
#include <QFile>
#include <QSaveFile>
#include <QTemporaryFile>
#include <QTextStream>
#include <QTimer>
@ -482,40 +483,92 @@ Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilenam
* wrong moment.
*
* @param filePath Absolute path of the file to save
* @param keepOld Rename the original database file instead of deleting
* @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 keepOld)
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;
QTemporaryFile saveFile;
if (saveFile.open()) {
// write the database to the file
setEmitModified(false);
writer.writeDatabase(&saveFile, this);
setEmitModified(true);
setEmitModified(false);
writer.writeDatabase(device, this);
setEmitModified(true);
if (writer.hasError()) {
// the writer failed
return writer.errorString();
}
saveFile.close(); // flush to disk
if (keepOld) {
QFile::remove(filePath + ".old");
QFile::rename(filePath, filePath + ".old");
}
QFile::remove(filePath);
if (saveFile.rename(filePath)) {
// successfully saved database file
saveFile.setAutoRemove(false);
return {};
}
if (writer.hasError()) {
// the writer failed
return writer.errorString();
}
return {};
}
return saveFile.errorString();
/**
* Remove the old backup and replace it with a new one
* backups are named <filename>.old.kdbx4
*
* @param filePath Path to the file to backup
* @return
*/
bool Database::backupDatabase(QString filePath)
{
QString backupFilePath = filePath;
backupFilePath.replace(".kdbx", ".old.kdbx", Qt::CaseInsensitive);
if (!backupFilePath.endsWith(".old.kdbx")) {
// Fallback in case of poorly named file
backupFilePath = filePath + ".old";
}
QFile::remove(backupFilePath);
return QFile::copy(filePath, backupFilePath);
}
QSharedPointer<Kdf> Database::kdf() const

View File

@ -32,6 +32,7 @@ enum class EntryReferenceType;
class Group;
class Metadata;
class QTimer;
class QIODevice;
struct DeletedObject
{
@ -111,7 +112,7 @@ public:
void emptyRecycleBin();
void setEmitModified(bool value);
void merge(const Database* other);
QString saveToFile(QString filePath, bool keepOld = false);
QString saveToFile(QString filePath, bool atomic = true, bool backup = false);
/**
* Returns a unique id that is only valid as long as the Database exists.
@ -144,6 +145,8 @@ private:
Group* findGroupRecursive(const Uuid& uuid, Group* group);
void createRecycleBin();
QString writeDatabase(QIODevice* device);
bool backupDatabase(QString filePath);
Metadata* const m_metadata;
Group* m_rootGroup;

View File

@ -44,6 +44,7 @@ DatabaseManagerStruct::DatabaseManagerStruct()
: dbWidget(nullptr)
, modified(false)
, readOnly(false)
, saveAttempts(0)
{
}
@ -324,12 +325,14 @@ bool DatabaseTabWidget::saveDatabase(Database* db, QString filePath)
dbStruct.dbWidget->blockAutoReload(true);
// TODO: Make this async, but lock out the database widget to prevent re-entrance
QString errorMessage = db->saveToFile(filePath, config()->get("BackupBeforeSave").toBool());
bool useAtomicSaves = config()->get("UseAtomicSaves").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);
@ -338,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;

View File

@ -40,6 +40,7 @@ struct DatabaseManagerStruct
QFileInfo fileInfo;
bool modified;
bool readOnly;
int saveAttempts;
};
Q_DECLARE_TYPEINFO(DatabaseManagerStruct, Q_MOVABLE_TYPE);

View File

@ -118,6 +118,7 @@ void SettingsWidget::loadSettings()
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());
@ -195,6 +196,7 @@ void SettingsWidget::saveSettings()
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",

View File

@ -34,131 +34,134 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="singleInstanceCheckBox">
<property name="text">
<string>Start only a single instance of KeePassXC</string>
<widget class="QGroupBox" name="startupGroup">
<property name="title">
<string>Startup</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="rememberLastDatabasesCheckBox">
<property name="text">
<string>Remember last databases</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="rememberLastKeyFilesCheckBox">
<property name="text">
<string>Remember last key files and security dongles</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="openPreviousDatabasesOnStartupCheckBox">
<property name="text">
<string>Load previous databases on startup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoSaveOnExitCheckBox">
<property name="text">
<string>Automatically save on exit</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoSaveAfterEveryChangeCheckBox">
<property name="text">
<string>Automatically save after every change</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="backupBeforeSaveCheckBox">
<property name="text">
<string>Backup database file before saving</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoReloadOnChangeCheckBox">
<property name="text">
<string>Automatically reload the database when modified externally</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="minimizeOnCopyCheckBox">
<property name="text">
<string>Minimize when copying to clipboard</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeOnStartup">
<property name="text">
<string>Minimize window at application startup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
<property name="text">
<string>Use group icon on entry creation</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="ignoreGroupExpansionCheckBox">
<property name="text">
<string>Don't mark database as modified for non-data changes (e.g., expanding groups)</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="systraySettings" native="true">
<layout class="QVBoxLayout" name="systrayLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QCheckBox" name="singleInstanceCheckBox">
<property name="text">
<string>Start only a single instance of KeePassXC</string>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<property name="checked">
<bool>true</bool>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>30</height>
</size>
</widget>
</item>
<item>
<widget class="QCheckBox" name="rememberLastDatabasesCheckBox">
<property name="text">
<string>Remember last databases</string>
</property>
</spacer>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="rememberLastKeyFilesCheckBox">
<property name="text">
<string>Remember last key files and security dongles</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="openPreviousDatabasesOnStartupCheckBox">
<property name="text">
<string>Load previous databases on startup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeOnStartup">
<property name="text">
<string>Minimize window at application startup</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="saveGroup">
<property name="title">
<string>File Management</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="useAtomicSavesCheckBox">
<property name="text">
<string>Safely save database files (may be incompatible with DropBox, etc)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="backupBeforeSaveCheckBox">
<property name="text">
<string>Backup database file before saving</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoSaveAfterEveryChangeCheckBox">
<property name="text">
<string>Automatically save after every change</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoSaveOnExitCheckBox">
<property name="text">
<string>Automatically save on exit</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="ignoreGroupExpansionCheckBox">
<property name="text">
<string>Don't mark database as modified for non-data changes (e.g., expanding groups)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoReloadOnChangeCheckBox">
<property name="text">
<string>Automatically reload the database when modified externally</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="entryGroup">
<property name="title">
<string>Entry Management</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
<property name="text">
<string>Use group icon on entry creation</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="minimizeOnCopyCheckBox">
<property name="text">
<string>Minimize when copying to clipboard</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="detailsHideCheckBox">
@ -167,6 +170,15 @@
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="generalGroup">
<property name="title">
<string>General</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QCheckBox" name="systrayShowCheckBox">
<property name="text">
@ -175,114 +187,162 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<widget class="QWidget" name="systraySettings" native="true">
<layout class="QVBoxLayout" name="systrayLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeToTrayCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Hide window to system tray when minimized</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeOnCloseCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Hide window to system tray instead of app exit</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayDarkIconCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Dark system tray icon</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="languageLabelLayout_2">
<property name="spacing">
<number>0</number>
<number>15</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeToTrayCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="languageLabel_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Hide window to system tray when minimized</string>
<string>Language</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayMinimizeOnCloseCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Hide window to system tray instead of app exit</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="systrayDarkIconCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Dark system tray icon</string>
<widget class="QComboBox" name="languageComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
@ -291,52 +351,6 @@
</layout>
</widget>
</item>
<item>
<spacer name="trayIconSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>30</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="languageLabelLayout_2">
<property name="spacing">
<number>15</number>
</property>
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="languageLabel_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">