Correct behaviors when saving database fails

* Mark database dirty if saving fails
* Restore database file from backup if unsafe save fails between deleting database file and copying temporary file into place
* Improve error message display for opening and saving database files
* Do not automatically retry saving after failure. This prevents deletion of the backup database file and improves user awareness of issues.
This commit is contained in:
Jonathan White 2019-04-03 10:23:18 -04:00
parent ec82931573
commit 3b0b5d85e9
7 changed files with 50 additions and 12 deletions

View file

@ -222,6 +222,7 @@ bool Database::save(const QString& filePath, QString* error, bool atomic, bool b
return true; return true;
} }
} }
if (error) { if (error) {
*error = saveFile.errorString(); *error = saveFile.errorString();
} }
@ -246,18 +247,25 @@ bool Database::save(const QString& filePath, QString* error, bool atomic, bool b
// due to an undocumented difference in how the function handles // due to an undocumented difference in how the function handles
// errors. This prevents errors when saving across file systems. // errors. This prevents errors when saving across file systems.
if (tempFile.QFile::rename(filePath)) { if (tempFile.QFile::rename(filePath)) {
// successfully saved database file // successfully saved the database
tempFile.setAutoRemove(false); tempFile.setAutoRemove(false);
setFilePath(filePath); setFilePath(filePath);
return true; return true;
} else {
// restore the database from the backup
if (backup) {
restoreDatabase(filePath);
} }
} }
}
if (error) { if (error) {
*error = tempFile.errorString(); *error = tempFile.errorString();
} }
} }
// Saving failed // Saving failed
markAsModified();
return false; return false;
} }
@ -316,6 +324,24 @@ bool Database::backupDatabase(const QString& filePath)
return QFile::copy(filePath, backupFilePath); return QFile::copy(filePath, backupFilePath);
} }
/**
* Restores the database file from the backup file with
* name <filename>.old.<extension> to filePath. This will
* overwrite the existing file!
*
* @param filePath Path to the file to restore
* @return true on success
*/
bool Database::restoreDatabase(const QString& filePath)
{
static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$");
auto match = re.match(filePath);
auto backupFilePath = match.captured(1) + ".old" + match.captured(2);
QFile::remove(filePath);
return QFile::copy(backupFilePath, filePath);
}
bool Database::isReadOnly() const bool Database::isReadOnly() const
{ {
return m_data.isReadOnly; return m_data.isReadOnly;

View file

@ -172,6 +172,7 @@ private:
bool writeDatabase(QIODevice* device, QString* error = nullptr); bool writeDatabase(QIODevice* device, QString* error = nullptr);
bool backupDatabase(const QString& filePath); bool backupDatabase(const QString& filePath);
bool restoreDatabase(const QString& filePath);
Metadata* const m_metadata; Metadata* const m_metadata;
DatabaseData m_data; DatabaseData m_data;

View file

@ -150,7 +150,8 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
QString canonicalFilePath = fileInfo.canonicalFilePath(); QString canonicalFilePath = fileInfo.canonicalFilePath();
if (canonicalFilePath.isEmpty()) { if (canonicalFilePath.isEmpty()) {
emit messageGlobal(tr("The database file does not exist or is not accessible."), MessageWidget::Error); emit messageGlobal(tr("Failed to open %1. It either does not exist or is not accessible.").arg(filePath),
MessageWidget::Error);
return; return;
} }

View file

@ -89,6 +89,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
, m_databaseOpenWidget(new DatabaseOpenWidget(this)) , m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this)) , m_keepass1OpenWidget(new KeePass1OpenWidget(this))
, m_groupView(new GroupView(m_db.data(), m_mainSplitter)) , m_groupView(new GroupView(m_db.data(), m_mainSplitter))
, m_saveAttempts(0)
, m_fileWatcher(new DelayingFileWatcher(this)) , m_fileWatcher(new DelayingFileWatcher(this))
{ {
m_messageWidget->setHidden(true); m_messageWidget->setHidden(true);
@ -859,6 +860,7 @@ void DatabaseWidget::loadDatabase(bool accepted)
replaceDatabase(openWidget->database()); replaceDatabase(openWidget->database());
switchToMainView(); switchToMainView();
m_fileWatcher->restart(); m_fileWatcher->restart();
m_saveAttempts = 0;
emit databaseUnlocked(); emit databaseUnlocked();
} else { } else {
m_fileWatcher->stop(); m_fileWatcher->stop();
@ -1512,7 +1514,7 @@ EntryView* DatabaseWidget::entryView()
* @param attempt current save attempt or -1 to disable attempts * @param attempt current save attempt or -1 to disable attempts
* @return true on success * @return true on success
*/ */
bool DatabaseWidget::save(int attempt) bool DatabaseWidget::save()
{ {
// Never allow saving a locked database; it causes corruption // Never allow saving a locked database; it causes corruption
Q_ASSERT(!isLocked()); Q_ASSERT(!isLocked());
@ -1527,6 +1529,8 @@ bool DatabaseWidget::save(int attempt)
} }
blockAutoReload(true); blockAutoReload(true);
++m_saveAttempts;
// TODO: Make this async, but lock out the database widget to prevent re-entrance // TODO: Make this async, but lock out the database widget to prevent re-entrance
bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool(); bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
QString errorMessage; QString errorMessage;
@ -1534,14 +1538,11 @@ bool DatabaseWidget::save(int attempt)
blockAutoReload(false); blockAutoReload(false);
if (ok) { if (ok) {
m_saveAttempts = 0;
return true; return true;
} }
if (attempt >= 0 && attempt <= 2) { if (m_saveAttempts > 2 && useAtomicSaves) {
return save(attempt + 1);
}
if (attempt > 2 && useAtomicSaves) {
// Saving failed 3 times, issue a warning and attempt to resolve // Saving failed 3 times, issue a warning and attempt to resolve
auto result = MessageBox::question(this, auto result = MessageBox::question(this,
tr("Disable safe saves?"), tr("Disable safe saves?"),
@ -1552,11 +1553,15 @@ bool DatabaseWidget::save(int attempt)
MessageBox::Disable); MessageBox::Disable);
if (result == MessageBox::Disable) { if (result == MessageBox::Disable) {
config()->set("UseAtomicSaves", false); config()->set("UseAtomicSaves", false);
return save(attempt + 1); return save();
} }
} }
showMessage(tr("Writing the database failed.\n%1").arg(errorMessage), MessageWidget::Error); showMessage(tr("Writing the database failed: %1").arg(errorMessage),
MessageWidget::Error,
true,
MessageWidget::LongAutoHideTimeout);
return false; return false;
} }
@ -1585,8 +1590,9 @@ bool DatabaseWidget::saveAs()
// Ensure we don't recurse back into this function // Ensure we don't recurse back into this function
m_db->setReadOnly(false); m_db->setReadOnly(false);
m_db->setFilePath(newFilePath); m_db->setFilePath(newFilePath);
m_saveAttempts = 0;
if (!save(-1)) { if (!save()) {
// Failed to save, try again // Failed to save, try again
continue; continue;
} }

View file

@ -144,7 +144,7 @@ signals:
public slots: public slots:
bool lock(); bool lock();
bool save(int attempt = 0); bool save();
bool saveAs(); bool saveAs();
void replaceDatabase(QSharedPointer<Database> db); void replaceDatabase(QSharedPointer<Database> db);
@ -255,6 +255,8 @@ private:
QUuid m_groupBeforeLock; QUuid m_groupBeforeLock;
QUuid m_entryBeforeLock; QUuid m_entryBeforeLock;
int m_saveAttempts;
// Search state // Search state
EntrySearcher* m_EntrySearcher; EntrySearcher* m_EntrySearcher;
QString m_lastSearchText; QString m_lastSearchText;

View file

@ -23,6 +23,7 @@
#include <QUrl> #include <QUrl>
const int MessageWidget::DefaultAutoHideTimeout = 6000; const int MessageWidget::DefaultAutoHideTimeout = 6000;
const int MessageWidget::LongAutoHideTimeout = 15000;
const int MessageWidget::DisableAutoHide = -1; const int MessageWidget::DisableAutoHide = -1;
MessageWidget::MessageWidget(QWidget* parent) MessageWidget::MessageWidget(QWidget* parent)

View file

@ -33,6 +33,7 @@ public:
int autoHideTimeout() const; int autoHideTimeout() const;
static const int DefaultAutoHideTimeout; static const int DefaultAutoHideTimeout;
static const int LongAutoHideTimeout;
static const int DisableAutoHide; static const int DisableAutoHide;
signals: signals: