Add auto-save delay per database (#9100)

Add a new propery autosaveDelay in Metadata of the db.
The property is saved in customData to not affect database structure as this setting is unique to keepasxc.
The propery sets delay to wait since last modification before saving.

Co-authored-by: jNullj <jNullj@users.noreply.github.com>
This commit is contained in:
jNullj 2023-07-08 15:49:03 +03:00 committed by GitHub
parent a0874a0d6d
commit 35baeaff79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 272 additions and 6 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -333,6 +333,7 @@ image::database_settings.png[]
* *Max. history size:* When the history of an entry gets above this size, it is truncated. For example, this happens when entries have large attachments. Set this value small to prevent the database from getting too large (we recommend 6 MiB).
* *Use recycle bin:* Select this check-box if you want deleted entries to move to the recycle bin instead of being permanently removed. The recycle bin will be created if it does not already exist after your first deletion. To delete entries permanently, you must empty the recycle bin manually.
* *Enable compression:* KeePassXC databases can be compressed before being encrypted. Compression reduces the size of the database and does not have any appreciable affect on speed. It is recommended to always save databases with compression.
* *Autosave delay:* Customize the automatic database save operation by delaying it for a set time since the last change. By default, this option is disabled for fast saving, but can be useful for large databases to avoid delays after each change.
3. Click the Security button in the left-hand menu bar to change your database credentials and change encryption settings.
+

View File

@ -2041,6 +2041,26 @@ Entries deleted from the recycle bin are
removed from the database.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Autosave delay since last change</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Autosave delay</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Autosave delay since last change in minutes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source> min</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Autosave delay since last change checkbox</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingsWidgetKeeShare</name>

View File

@ -28,10 +28,17 @@
const int Metadata::DefaultHistoryMaxItems = 10;
const int Metadata::DefaultHistoryMaxSize = 6 * 1024 * 1024;
const int Metadata::DefaultAutosaveDelayMin = 0;
// Fallback icon for return by reference
static const Metadata::CustomIconData NULL_ICON{};
namespace customDataKeys
{
static const QString savedSearch = QStringLiteral("KPXC_SavedSearch");
static const QString autosaveDelay = QStringLiteral("KPXC_autosaveDelayMin");
}; // namespace customDataKeys
Metadata::Metadata(QObject* parent)
: ModifiableObject(parent)
, m_customData(new CustomData(this))
@ -265,6 +272,19 @@ int Metadata::historyMaxSize() const
return m_data.historyMaxSize;
}
int Metadata::autosaveDelayMin() const
{
QString autosaveDelayMinStr = m_customData->value(customDataKeys::autosaveDelay);
if (autosaveDelayMinStr.isNull()) {
// data is not set yet, use default
return Metadata::DefaultAutosaveDelayMin;
}
bool ok; // check for QString to int op failuer
int autosaveDelayMin = autosaveDelayMinStr.toInt(&ok);
Q_ASSERT(ok);
return autosaveDelayMin;
}
CustomData* Metadata::customData()
{
return m_customData;
@ -478,6 +498,12 @@ void Metadata::setHistoryMaxSize(int value)
set(m_data.historyMaxSize, value);
}
void Metadata::setAutosaveDelayMin(int value)
{
Q_ASSERT(value >= 0 && value <= 420000000);
m_customData->set(customDataKeys::autosaveDelay, QString::number(value));
}
QDateTime Metadata::settingsChanged() const
{
return m_settingsChanged;
@ -494,7 +520,7 @@ void Metadata::addSavedSearch(const QString& name, const QString& searchtext)
auto searches = savedSearches();
searches.insert(name, searchtext);
auto json = QJsonDocument::fromVariant(searches);
m_customData->set("KPXC_SavedSearch", json.toJson());
m_customData->set(customDataKeys::savedSearch, json.toJson());
}
void Metadata::deleteSavedSearch(const QString& name)
@ -502,12 +528,12 @@ void Metadata::deleteSavedSearch(const QString& name)
auto searches = savedSearches();
searches.remove(name);
auto json = QJsonDocument::fromVariant(searches);
m_customData->set("KPXC_SavedSearch", json.toJson());
m_customData->set(customDataKeys::savedSearch, json.toJson());
}
QVariantMap Metadata::savedSearches()
{
auto searches = m_customData->value("KPXC_SavedSearch");
auto searches = m_customData->value(customDataKeys::savedSearch);
auto json = QJsonDocument::fromJson(searches.toUtf8());
return json.toVariant().toMap();
}

View File

@ -109,11 +109,13 @@ public:
int databaseKeyChangeForce() const;
int historyMaxItems() const;
int historyMaxSize() const;
int autosaveDelayMin() const;
CustomData* customData();
const CustomData* customData() const;
static const int DefaultHistoryMaxItems;
static const int DefaultHistoryMaxSize;
static const int DefaultAutosaveDelayMin;
void setGenerator(const QString& value);
void setName(const QString& value);
@ -150,6 +152,7 @@ public:
void setMasterKeyChangeForce(int value);
void setHistoryMaxItems(int value);
void setHistoryMaxSize(int value);
void setAutosaveDelayMin(int value);
void setUpdateDatetime(bool value);
void addSavedSearch(const QString& name, const QString& searchtext);
void deleteSavedSearch(const QString& name);

View File

@ -222,6 +222,10 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_blockAutoSave = false;
m_autosaveTimer = new QTimer(this);
m_autosaveTimer->setSingleShot(true);
connect(m_autosaveTimer, SIGNAL(timeout()), this, SLOT(onAutosaveDelayTimeout()));
m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
#ifdef WITH_XC_KEESHARE
@ -1528,13 +1532,42 @@ void DatabaseWidget::onGroupChanged()
void DatabaseWidget::onDatabaseModified()
{
if (!m_blockAutoSave && config()->get(Config::AutoSaveAfterEveryChange).toBool()) {
refreshSearch();
int autosaveDelayMs = m_db->metadata()->autosaveDelayMin() * 60 * 1000; // min to msec for QTimer
bool autosaveAfterEveryChangeConfig = config()->get(Config::AutoSaveAfterEveryChange).toBool();
if (autosaveDelayMs > 0 && autosaveAfterEveryChangeConfig) {
// reset delay when modified
m_autosaveTimer->start(autosaveDelayMs);
return;
}
if (!m_blockAutoSave && autosaveAfterEveryChangeConfig) {
save();
} else {
// Only block once, then reset
m_blockAutoSave = false;
}
refreshSearch();
}
void DatabaseWidget::onAutosaveDelayTimeout()
{
const bool isAutosaveDelayEnabled = m_db->metadata()->autosaveDelayMin() > 0;
const bool autosaveAfterEveryChangeConfig = config()->get(Config::AutoSaveAfterEveryChange).toBool();
if (!(isAutosaveDelayEnabled && autosaveAfterEveryChangeConfig)) {
// User might disable the delay/autosave while the timer is running
return;
}
if (!m_blockAutoSave) {
save();
} else {
// Only block once, then reset
m_blockAutoSave = false;
}
}
void DatabaseWidget::triggerAutosaveTimer()
{
m_autosaveTimer->stop();
QMetaObject::invokeMethod(m_autosaveTimer, "timeout");
}
QString DatabaseWidget::getCurrentSearch()
@ -1986,6 +2019,7 @@ bool DatabaseWidget::save()
if (performSave(errorMessage)) {
m_saveAttempts = 0;
m_blockAutoSave = false;
m_autosaveTimer->stop(); // stop autosave delay to avoid triggering another save
return true;
}

View File

@ -233,6 +233,7 @@ public slots:
int autoHideTimeout = MessageWidget::DefaultAutoHideTimeout);
void showErrorMessage(const QString& errorMessage);
void hideMessage();
void triggerAutosaveTimer();
protected:
void closeEvent(QCloseEvent* event) override;
@ -251,6 +252,7 @@ private slots:
void onEntryChanged(Entry* entry);
void onGroupChanged();
void onDatabaseModified();
void onAutosaveDelayTimeout();
void connectDatabaseSignals();
void loadDatabase(bool accepted);
void unlockDatabase(bool accepted);
@ -309,6 +311,9 @@ private:
// Autoreload
bool m_blockAutoSave;
// Autosave delay
QPointer<QTimer> m_autosaveTimer;
// Auto-Type related
QString m_searchStringForAutoType;
};

View File

@ -31,6 +31,7 @@ DatabaseSettingsWidgetGeneral::DatabaseSettingsWidgetGeneral(QWidget* parent)
connect(m_ui->historyMaxItemsCheckBox, SIGNAL(toggled(bool)), m_ui->historyMaxItemsSpinBox, SLOT(setEnabled(bool)));
connect(m_ui->historyMaxSizeCheckBox, SIGNAL(toggled(bool)), m_ui->historyMaxSizeSpinBox, SLOT(setEnabled(bool)));
connect(m_ui->autosaveDelayCheckBox, SIGNAL(toggled(bool)), m_ui->autosaveDelaySpinBox, SLOT(setEnabled(bool)));
}
DatabaseSettingsWidgetGeneral::~DatabaseSettingsWidgetGeneral() = default;
@ -62,6 +63,13 @@ void DatabaseSettingsWidgetGeneral::initialize()
m_ui->historyMaxSizeSpinBox->setEnabled(false);
m_ui->historyMaxSizeCheckBox->setChecked(false);
}
if (meta->autosaveDelayMin() > 0) {
m_ui->autosaveDelaySpinBox->setValue(meta->autosaveDelayMin());
m_ui->autosaveDelayCheckBox->setChecked(true);
} else {
m_ui->autosaveDelayCheckBox->setChecked(false);
m_ui->autosaveDelaySpinBox->setEnabled(false);
}
}
void DatabaseSettingsWidgetGeneral::uninitialize()
@ -132,6 +140,12 @@ bool DatabaseSettingsWidgetGeneral::save()
truncate = true;
}
int autosaveDelayMin = 0;
if (m_ui->autosaveDelayCheckBox->isChecked()) {
autosaveDelayMin = m_ui->autosaveDelaySpinBox->value();
}
meta->setAutosaveDelayMin(autosaveDelayMin);
if (truncate) {
const QList<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(false);
for (Entry* entry : allEntries) {

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>453</width>
<height>374</height>
<height>394</height>
</rect>
</property>
<property name="sizePolicy">
@ -202,6 +202,58 @@ removed from the database.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="autosaveDelayCheckBox">
<property name="toolTip">
<string>Autosave delay since last change</string>
</property>
<property name="accessibleName">
<string>Autosave delay since last change checkbox</string>
</property>
<property name="text">
<string>Autosave delay</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="autosaveDelaySpinBox">
<property name="toolTip">
<string>Autosave delay since last change in minutes</string>
</property>
<property name="accessibleName">
<string>Autosave delay since last change in minutes</string>
</property>
<property name="suffix">
<string> min</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>420000000</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>

View File

@ -1470,7 +1470,10 @@ void TestGui::testDatabaseSettings()
auto* dbSettingsStackedWidget = dbSettingsDialog->findChild<QStackedWidget*>("stackedWidget");
auto* transformRoundsSpinBox = dbSettingsDialog->findChild<QSpinBox*>("transformRoundsSpinBox");
auto advancedToggle = dbSettingsDialog->findChild<QCheckBox*>("advancedSettingsToggle");
auto* autosaveDelayCheckBox = dbSettingsDialog->findChild<QCheckBox*>("autosaveDelayCheckBox");
auto* autosaveDelaySpinBox = dbSettingsDialog->findChild<QSpinBox*>("autosaveDelaySpinBox");
auto* dbSettingsButtonBox = dbSettingsDialog->findChild<QDialogButtonBox*>("buttonBox");
int autosaveDelayTestValue = 2;
advancedToggle->setChecked(true);
QApplication::processEvents();
@ -1503,7 +1506,115 @@ void TestGui::testDatabaseSettings()
QCOMPARE(historyMaxSizeCheckBox->isChecked(), false);
QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
// Test loading default values and setting autosaveDelay
triggerAction("actionDatabaseSettings");
QVERIFY(autosaveDelayCheckBox->isChecked() == false);
autosaveDelayCheckBox->toggle();
autosaveDelaySpinBox->setValue(autosaveDelayTestValue);
QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
QTRY_COMPARE(m_db->metadata()->autosaveDelayMin(), autosaveDelayTestValue);
checkSaveDatabase();
// Test loading autosaveDelay non-default values
triggerAction("actionDatabaseSettings");
QTRY_COMPARE(autosaveDelayCheckBox->isChecked(), true);
QTRY_COMPARE(autosaveDelaySpinBox->value(), autosaveDelayTestValue);
QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
// test autosave delay
// 1 init
config()->set(Config::AutoSaveAfterEveryChange, true);
QSignalSpy writeDbSignalSpy(m_db.data(), &Database::databaseSaved);
// 2 create new entries
// 2.a) Click the new entry button and set the title
auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
QVERIFY(entryNewAction->isEnabled());
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QVERIFY(toolBar);
QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QVERIFY(editEntryWidget);
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QVERIFY(titleEdit);
QTest::keyClicks(titleEdit, "Test autosaveDelay 1");
// 2.b) Save changes
editEntryWidget->setCurrentPage(0);
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// 2.c) Make sure file was not modified yet
Tools::wait(150); // due to modify timer
QTRY_COMPARE(writeDbSignalSpy.count(), 0);
// 2.d) Create second entry to test delay timer reset
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
QTest::keyClicks(titleEdit, "Test autosaveDelay 2");
// 2.e) Save changes
editEntryWidget->setCurrentPage(0);
editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// 3 Double check both true negative and true positive
// 3.a) Test unmodified prior to delay timeout
Tools::wait(150); // due to modify timer
QTRY_COMPARE(writeDbSignalSpy.count(), 0);
// 3.b) Test modification time after expected
m_dbWidget->triggerAutosaveTimer();
QTRY_COMPARE(writeDbSignalSpy.count(), 1);
// 4 Test no delay when disabled autosave or autosaveDelay
// 4.a) create new entry
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
QTest::keyClicks(titleEdit, "Test autosaveDelay 3");
// 4.b) Save changes
editEntryWidget->setCurrentPage(0);
editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// 4.c) Start timer
Tools::wait(150); // due to modify timer
// 4.d) Disable autosave
config()->set(Config::AutoSaveAfterEveryChange, false);
// 4.e) Make sure changes are not saved
m_dbWidget->triggerAutosaveTimer();
QTRY_COMPARE(writeDbSignalSpy.count(), 1);
// 4.f) Repeat for autosaveDelay
config()->set(Config::AutoSaveAfterEveryChange, true);
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
QTest::keyClicks(titleEdit, "Test autosaveDelay 4");
editEntryWidget->setCurrentPage(0);
editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
Tools::wait(150); // due to modify timer
m_db->metadata()->setAutosaveDelayMin(0);
// 4.g) Make sure changes are not saved
m_dbWidget->triggerAutosaveTimer();
QTRY_COMPARE(writeDbSignalSpy.count(), 1);
// 5 Cleanup
config()->set(Config::AutoSaveAfterEveryChange, false);
}
void TestGui::testKeePass1Import()