mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-08 22:42:34 -04:00
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:
parent
ec82931573
commit
3b0b5d85e9
7 changed files with 50 additions and 12 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue