mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Move FileWatcher into Database class
* Fix #3506 * Fix #2389 * Fix #2536 * Fix #2230 Every database that has been opened now watch's it's own file. This allows the database class to manage file changes and detect fail conditions during saving. Additionally, all stakeholders of the database can listen for the database file changed notification and respond accordingly. Performed significant cleanup of the autoreload code within DatabaseWidget. Fixed several issues with handling changes due to merging, not merging, and other scenarios while reloading. Prevent database saves to the same file if there are changes on disk that have not been merged with the open database.
This commit is contained in:
parent
6b746913e4
commit
744b4abce8
@ -19,6 +19,7 @@
|
|||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
|
|
||||||
#include "core/Clock.h"
|
#include "core/Clock.h"
|
||||||
|
#include "core/FileWatcher.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Merger.h"
|
#include "core/Merger.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
@ -42,6 +43,7 @@ Database::Database()
|
|||||||
, m_data()
|
, m_data()
|
||||||
, m_rootGroup(nullptr)
|
, m_rootGroup(nullptr)
|
||||||
, m_timer(new QTimer(this))
|
, m_timer(new QTimer(this))
|
||||||
|
, m_fileWatcher(new FileWatcher(this))
|
||||||
, m_emitModified(false)
|
, m_emitModified(false)
|
||||||
, m_uuid(QUuid::createUuid())
|
, m_uuid(QUuid::createUuid())
|
||||||
{
|
{
|
||||||
@ -54,7 +56,9 @@ Database::Database()
|
|||||||
|
|
||||||
connect(m_metadata, SIGNAL(metadataModified()), this, SLOT(markAsModified()));
|
connect(m_metadata, SIGNAL(metadataModified()), this, SLOT(markAsModified()));
|
||||||
connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified()));
|
connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified()));
|
||||||
|
connect(this, SIGNAL(databaseOpened()), SLOT(updateCommonUsernames()));
|
||||||
connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames()));
|
connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames()));
|
||||||
|
connect(m_fileWatcher, SIGNAL(fileChanged()), SIGNAL(databaseFileChanged()));
|
||||||
|
|
||||||
m_modified = false;
|
m_modified = false;
|
||||||
m_emitModified = true;
|
m_emitModified = true;
|
||||||
@ -116,6 +120,7 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
|
|||||||
emit databaseDiscarded();
|
emit databaseDiscarded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_initialized = false;
|
||||||
setEmitModified(false);
|
setEmitModified(false);
|
||||||
|
|
||||||
QFile dbFile(filePath);
|
QFile dbFile(filePath);
|
||||||
@ -138,8 +143,7 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeePass2Reader reader;
|
KeePass2Reader reader;
|
||||||
bool ok = reader.readDatabase(&dbFile, std::move(key), this);
|
if (!reader.readDatabase(&dbFile, std::move(key), this)) {
|
||||||
if (reader.hasError()) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = tr("Error while reading the database: %1").arg(reader.errorString());
|
*error = tr("Error while reading the database: %1").arg(reader.errorString());
|
||||||
}
|
}
|
||||||
@ -150,22 +154,23 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
|
|||||||
setFilePath(filePath);
|
setFilePath(filePath);
|
||||||
dbFile.close();
|
dbFile.close();
|
||||||
|
|
||||||
updateCommonUsernames();
|
|
||||||
|
|
||||||
setInitialized(ok);
|
|
||||||
markAsClean();
|
markAsClean();
|
||||||
|
|
||||||
|
m_initialized = true;
|
||||||
|
emit databaseOpened();
|
||||||
|
m_fileWatcher->start(canonicalFilePath());
|
||||||
setEmitModified(true);
|
setEmitModified(true);
|
||||||
return ok;
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the database to the current file path. It is an error to call this function
|
* Save the database to the current file path. It is an error to call this function
|
||||||
* if no file path has been defined.
|
* if no file path has been defined.
|
||||||
*
|
*
|
||||||
|
* @param error error message in case of failure
|
||||||
* @param atomic Use atomic file transactions
|
* @param atomic Use atomic file transactions
|
||||||
* @param backup Backup the existing database file, if exists
|
* @param backup Backup the existing database file, if exists
|
||||||
* @param error error message in case of failure
|
|
||||||
* @return true on success
|
* @return true on success
|
||||||
*/
|
*/
|
||||||
bool Database::save(QString* error, bool atomic, bool backup)
|
bool Database::save(QString* error, bool atomic, bool backup)
|
||||||
@ -194,15 +199,16 @@ bool Database::save(QString* error, bool atomic, bool backup)
|
|||||||
* wrong moment.
|
* wrong moment.
|
||||||
*
|
*
|
||||||
* @param filePath Absolute path of the file to save
|
* @param filePath Absolute path of the file to save
|
||||||
|
* @param error error message in case of failure
|
||||||
* @param atomic Use atomic file transactions
|
* @param atomic Use atomic file transactions
|
||||||
* @param backup Backup the existing database file, if exists
|
* @param backup Backup the existing database file, if exists
|
||||||
* @param error error message in case of failure
|
|
||||||
* @return true on success
|
* @return true on success
|
||||||
*/
|
*/
|
||||||
bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup)
|
bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup)
|
||||||
{
|
{
|
||||||
|
if (filePath == m_data.filePath) {
|
||||||
// Disallow saving to the same file if read-only
|
// Disallow saving to the same file if read-only
|
||||||
if (m_data.isReadOnly && filePath == m_data.filePath) {
|
if (m_data.isReadOnly) {
|
||||||
Q_ASSERT_X(false, "Database::saveAs", "Could not save, database file is read-only.");
|
Q_ASSERT_X(false, "Database::saveAs", "Could not save, database file is read-only.");
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = tr("Could not save, database file is read-only.");
|
*error = tr("Could not save, database file is read-only.");
|
||||||
@ -210,11 +216,35 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fail-safe check to make sure we don't overwrite underlying file changes
|
||||||
|
// that have not yet triggered a file reload/merge operation.
|
||||||
|
if (!m_fileWatcher->hasSameFileChecksum()) {
|
||||||
|
if (error) {
|
||||||
|
*error = tr("Database file has unmerged changes.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear read-only flag
|
// Clear read-only flag
|
||||||
setReadOnly(false);
|
setReadOnly(false);
|
||||||
|
m_fileWatcher->stop();
|
||||||
|
|
||||||
auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath;
|
auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath;
|
||||||
|
bool ok = performSave(canonicalFilePath, error, atomic, backup);
|
||||||
|
if (ok) {
|
||||||
|
setFilePath(filePath);
|
||||||
|
m_fileWatcher->start(canonicalFilePath);
|
||||||
|
} else {
|
||||||
|
// Saving failed, don't rewatch file since it does not represent our database
|
||||||
|
markAsModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Database::performSave(const QString& filePath, QString* error, bool atomic, bool backup)
|
||||||
|
{
|
||||||
if (atomic) {
|
if (atomic) {
|
||||||
QSaveFile saveFile(filePath);
|
QSaveFile saveFile(filePath);
|
||||||
if (saveFile.open(QIODevice::WriteOnly)) {
|
if (saveFile.open(QIODevice::WriteOnly)) {
|
||||||
@ -224,12 +254,11 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (backup) {
|
if (backup) {
|
||||||
backupDatabase(canonicalFilePath);
|
backupDatabase(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveFile.commit()) {
|
if (saveFile.commit()) {
|
||||||
// successfully saved database file
|
// successfully saved database file
|
||||||
setFilePath(filePath);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,28 +277,26 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool
|
|||||||
tempFile.close(); // flush to disk
|
tempFile.close(); // flush to disk
|
||||||
|
|
||||||
if (backup) {
|
if (backup) {
|
||||||
backupDatabase(canonicalFilePath);
|
backupDatabase(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the original db and move the temp file in place
|
// Delete the original db and move the temp file in place
|
||||||
QFile::remove(canonicalFilePath);
|
QFile::remove(filePath);
|
||||||
|
|
||||||
// Note: call into the QFile rename instead of QTemporaryFile
|
// Note: call into the QFile rename instead of QTemporaryFile
|
||||||
// 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(canonicalFilePath)) {
|
if (tempFile.QFile::rename(filePath)) {
|
||||||
// successfully saved the database
|
// successfully saved the database
|
||||||
tempFile.setAutoRemove(false);
|
tempFile.setAutoRemove(false);
|
||||||
setFilePath(filePath);
|
|
||||||
return true;
|
return true;
|
||||||
} else if (!backup || !restoreDatabase(canonicalFilePath)) {
|
} else if (!backup || !restoreDatabase(filePath)) {
|
||||||
// Failed to copy new database in place, and
|
// Failed to copy new database in place, and
|
||||||
// failed to restore from backup or backups disabled
|
// failed to restore from backup or backups disabled
|
||||||
tempFile.setAutoRemove(false);
|
tempFile.setAutoRemove(false);
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
|
*error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
|
||||||
}
|
}
|
||||||
markAsModified();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,7 +307,6 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Saving failed
|
// Saving failed
|
||||||
markAsModified();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +516,8 @@ void Database::setFilePath(const QString& filePath)
|
|||||||
if (filePath != m_data.filePath) {
|
if (filePath != m_data.filePath) {
|
||||||
QString oldPath = m_data.filePath;
|
QString oldPath = m_data.filePath;
|
||||||
m_data.filePath = filePath;
|
m_data.filePath = filePath;
|
||||||
|
// Don't watch for changes until the next open or save operation
|
||||||
|
m_fileWatcher->stop();
|
||||||
emit filePathChanged(oldPath, filePath);
|
emit filePathChanged(oldPath, filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
class Entry;
|
class Entry;
|
||||||
enum class EntryReferenceType;
|
enum class EntryReferenceType;
|
||||||
|
class FileWatcher;
|
||||||
class Group;
|
class Group;
|
||||||
class Metadata;
|
class Metadata;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
@ -144,9 +145,11 @@ signals:
|
|||||||
void groupRemoved();
|
void groupRemoved();
|
||||||
void groupAboutToMove(Group* group, Group* toGroup, int index);
|
void groupAboutToMove(Group* group, Group* toGroup, int index);
|
||||||
void groupMoved();
|
void groupMoved();
|
||||||
|
void databaseOpened();
|
||||||
void databaseModified();
|
void databaseModified();
|
||||||
void databaseSaved();
|
void databaseSaved();
|
||||||
void databaseDiscarded();
|
void databaseDiscarded();
|
||||||
|
void databaseFileChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void startModifiedTimer();
|
void startModifiedTimer();
|
||||||
@ -177,12 +180,14 @@ 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);
|
bool restoreDatabase(const QString& filePath);
|
||||||
|
bool performSave(const QString& filePath, QString* error, bool atomic, bool backup);
|
||||||
|
|
||||||
Metadata* const m_metadata;
|
Metadata* const m_metadata;
|
||||||
DatabaseData m_data;
|
DatabaseData m_data;
|
||||||
Group* m_rootGroup;
|
Group* m_rootGroup;
|
||||||
QList<DeletedObject> m_deletedObjects;
|
QList<DeletedObject> m_deletedObjects;
|
||||||
QPointer<QTimer> m_timer;
|
QPointer<QTimer> m_timer;
|
||||||
|
QPointer<FileWatcher> m_fileWatcher;
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
bool m_modified = false;
|
bool m_modified = false;
|
||||||
bool m_emitModified;
|
bool m_emitModified;
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include "FileWatcher.h"
|
#include "FileWatcher.h"
|
||||||
|
|
||||||
#include "core/Clock.h"
|
#include "core/Clock.h"
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
@ -27,36 +28,23 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const int FileChangeDelay = 500;
|
const int FileChangeDelay = 200;
|
||||||
const int TimerResolution = 100;
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
DelayingFileWatcher::DelayingFileWatcher(QObject* parent)
|
FileWatcher::FileWatcher(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_ignoreFileChange(false)
|
, m_ignoreFileChange(false)
|
||||||
{
|
{
|
||||||
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged()));
|
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(onWatchedFileChanged()));
|
||||||
connect(&m_fileUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges()));
|
connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged()));
|
||||||
connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), this, SIGNAL(fileChanged()));
|
connect(&m_fileChecksumTimer, SIGNAL(timeout()), SLOT(checkFileChecksum()));
|
||||||
m_fileChangeDelayTimer.setSingleShot(true);
|
m_fileChangeDelayTimer.setSingleShot(true);
|
||||||
m_fileUnblockTimer.setSingleShot(true);
|
m_fileIgnoreDelayTimer.setSingleShot(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DelayingFileWatcher::restart()
|
void FileWatcher::start(const QString& filePath, int checksumInterval)
|
||||||
{
|
{
|
||||||
m_fileWatcher.addPath(m_filePath);
|
stop();
|
||||||
}
|
|
||||||
|
|
||||||
void DelayingFileWatcher::stop()
|
|
||||||
{
|
|
||||||
m_fileWatcher.removePath(m_filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DelayingFileWatcher::start(const QString& filePath)
|
|
||||||
{
|
|
||||||
if (!m_filePath.isEmpty()) {
|
|
||||||
m_fileWatcher.removePath(m_filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX)
|
#if defined(Q_OS_LINUX)
|
||||||
struct statfs statfsBuf;
|
struct statfs statfsBuf;
|
||||||
@ -74,45 +62,80 @@ void DelayingFileWatcher::start(const QString& filePath)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_fileWatcher.addPath(filePath);
|
m_fileWatcher.addPath(filePath);
|
||||||
|
|
||||||
if (!filePath.isEmpty()) {
|
|
||||||
m_filePath = filePath;
|
m_filePath = filePath;
|
||||||
}
|
m_fileChecksum = calculateChecksum();
|
||||||
|
m_fileChecksumTimer.start(checksumInterval);
|
||||||
|
m_ignoreFileChange = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DelayingFileWatcher::ignoreFileChanges()
|
void FileWatcher::stop()
|
||||||
|
{
|
||||||
|
if (!m_filePath.isEmpty()) {
|
||||||
|
m_fileWatcher.removePath(m_filePath);
|
||||||
|
}
|
||||||
|
m_filePath.clear();
|
||||||
|
m_fileChecksum.clear();
|
||||||
|
m_fileChangeDelayTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileWatcher::pause()
|
||||||
{
|
{
|
||||||
m_ignoreFileChange = true;
|
m_ignoreFileChange = true;
|
||||||
m_fileChangeDelayTimer.stop();
|
m_fileChangeDelayTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DelayingFileWatcher::observeFileChanges(bool delayed)
|
void FileWatcher::resume()
|
||||||
{
|
{
|
||||||
int timeout = 0;
|
|
||||||
if (delayed) {
|
|
||||||
timeout = FileChangeDelay;
|
|
||||||
} else {
|
|
||||||
m_ignoreFileChange = false;
|
m_ignoreFileChange = false;
|
||||||
start(m_filePath);
|
// Add a short delay to start in the next event loop
|
||||||
}
|
if (!m_fileIgnoreDelayTimer.isActive()) {
|
||||||
if (timeout > 0 && !m_fileUnblockTimer.isActive()) {
|
m_fileIgnoreDelayTimer.start(0);
|
||||||
m_fileUnblockTimer.start(timeout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DelayingFileWatcher::onWatchedFileChanged()
|
void FileWatcher::onWatchedFileChanged()
|
||||||
{
|
{
|
||||||
if (m_ignoreFileChange) {
|
// Don't notify if we are ignoring events or already started a notification chain
|
||||||
// the client forcefully silenced us
|
if (shouldIgnoreChanges()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_fileChangeDelayTimer.isActive()) {
|
|
||||||
// we are waiting to fire the delayed fileChanged event, so nothing
|
|
||||||
// to do here
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_fileChangeDelayTimer.start(FileChangeDelay);
|
m_fileChecksum = calculateChecksum();
|
||||||
|
m_fileChangeDelayTimer.start(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileWatcher::shouldIgnoreChanges()
|
||||||
|
{
|
||||||
|
return m_filePath.isEmpty() || m_ignoreFileChange || m_fileIgnoreDelayTimer.isActive()
|
||||||
|
|| m_fileChangeDelayTimer.isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileWatcher::hasSameFileChecksum()
|
||||||
|
{
|
||||||
|
return calculateChecksum() == m_fileChecksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileWatcher::checkFileChecksum()
|
||||||
|
{
|
||||||
|
if (shouldIgnoreChanges()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSameFileChecksum()) {
|
||||||
|
onWatchedFileChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray FileWatcher::calculateChecksum()
|
||||||
|
{
|
||||||
|
QFile file(m_filePath);
|
||||||
|
if (file.open(QFile::ReadOnly)) {
|
||||||
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||||
|
if (hash.addData(&file)) {
|
||||||
|
return hash.result();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
BulkFileWatcher::BulkFileWatcher(QObject* parent)
|
BulkFileWatcher::BulkFileWatcher(QObject* parent)
|
||||||
@ -281,7 +304,7 @@ void BulkFileWatcher::observeFileChanges(bool delayed)
|
|||||||
{
|
{
|
||||||
int timeout = 0;
|
int timeout = 0;
|
||||||
if (delayed) {
|
if (delayed) {
|
||||||
timeout = TimerResolution;
|
timeout = FileChangeDelay;
|
||||||
} else {
|
} else {
|
||||||
const QDateTime current = Clock::currentDateTimeUtc();
|
const QDateTime current = Clock::currentDateTimeUtc();
|
||||||
for (const QString& key : m_watchedFilesIgnored.keys()) {
|
for (const QString& key : m_watchedFilesIgnored.keys()) {
|
||||||
|
@ -23,34 +23,39 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
class DelayingFileWatcher : public QObject
|
class FileWatcher : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit DelayingFileWatcher(QObject* parent = nullptr);
|
explicit FileWatcher(QObject* parent = nullptr);
|
||||||
|
|
||||||
void blockAutoReload(bool block);
|
void start(const QString& path, int checksumInterval = 1000);
|
||||||
void start(const QString& path);
|
|
||||||
|
|
||||||
void restart();
|
|
||||||
void stop();
|
void stop();
|
||||||
void ignoreFileChanges();
|
|
||||||
|
bool hasSameFileChecksum();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void fileChanged();
|
void fileChanged();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void observeFileChanges(bool delayed = false);
|
void pause();
|
||||||
|
void resume();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onWatchedFileChanged();
|
void onWatchedFileChanged();
|
||||||
|
void checkFileChecksum();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QByteArray calculateChecksum();
|
||||||
|
bool shouldIgnoreChanges();
|
||||||
|
|
||||||
QString m_filePath;
|
QString m_filePath;
|
||||||
QFileSystemWatcher m_fileWatcher;
|
QFileSystemWatcher m_fileWatcher;
|
||||||
|
QByteArray m_fileChecksum;
|
||||||
QTimer m_fileChangeDelayTimer;
|
QTimer m_fileChangeDelayTimer;
|
||||||
QTimer m_fileUnblockTimer;
|
QTimer m_fileIgnoreDelayTimer;
|
||||||
|
QTimer m_fileChecksumTimer;
|
||||||
bool m_ignoreFileChange;
|
bool m_ignoreFileChange;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,11 +200,14 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou
|
|||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(dbWidget, SIGNAL(databaseFilePathChanged(QString, QString)), SLOT(updateTabName()));
|
|
||||||
connect(dbWidget,
|
connect(dbWidget,
|
||||||
SIGNAL(requestOpenDatabase(QString, bool, QString, QString)),
|
SIGNAL(requestOpenDatabase(QString, bool, QString, QString)),
|
||||||
SLOT(addDatabaseTab(QString, bool, QString, QString)));
|
SLOT(addDatabaseTab(QString, bool, QString, QString)));
|
||||||
|
connect(dbWidget, SIGNAL(databaseFilePathChanged(QString, QString)), SLOT(updateTabName()));
|
||||||
connect(dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseTabFromSender()));
|
connect(dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseTabFromSender()));
|
||||||
|
connect(dbWidget,
|
||||||
|
SIGNAL(databaseReplaced(const QSharedPointer<Database>&, const QSharedPointer<Database>&)),
|
||||||
|
SLOT(updateTabName()));
|
||||||
connect(dbWidget, SIGNAL(databaseModified()), SLOT(updateTabName()));
|
connect(dbWidget, SIGNAL(databaseModified()), SLOT(updateTabName()));
|
||||||
connect(dbWidget, SIGNAL(databaseSaved()), SLOT(updateTabName()));
|
connect(dbWidget, SIGNAL(databaseSaved()), SLOT(updateTabName()));
|
||||||
connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(updateTabName()));
|
connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(updateTabName()));
|
||||||
|
@ -94,7 +94,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
|
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
|
||||||
, m_groupView(new GroupView(m_db.data(), m_mainSplitter))
|
, m_groupView(new GroupView(m_db.data(), m_mainSplitter))
|
||||||
, m_saveAttempts(0)
|
, m_saveAttempts(0)
|
||||||
, m_fileWatcher(new DelayingFileWatcher(this))
|
|
||||||
{
|
{
|
||||||
m_messageWidget->setHidden(true);
|
m_messageWidget->setHidden(true);
|
||||||
|
|
||||||
@ -199,7 +198,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
|
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
|
||||||
connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
|
connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
|
||||||
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
|
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
|
||||||
connect(m_fileWatcher.data(), SIGNAL(fileChanged()), this, SLOT(reloadDatabaseFile()));
|
|
||||||
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
|
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
@ -895,6 +893,7 @@ void DatabaseWidget::connectDatabaseSignals()
|
|||||||
connect(m_db.data(), SIGNAL(databaseModified()), SIGNAL(databaseModified()));
|
connect(m_db.data(), SIGNAL(databaseModified()), SIGNAL(databaseModified()));
|
||||||
connect(m_db.data(), SIGNAL(databaseModified()), SLOT(onDatabaseModified()));
|
connect(m_db.data(), SIGNAL(databaseModified()), SLOT(onDatabaseModified()));
|
||||||
connect(m_db.data(), SIGNAL(databaseSaved()), SIGNAL(databaseSaved()));
|
connect(m_db.data(), SIGNAL(databaseSaved()), SIGNAL(databaseSaved()));
|
||||||
|
connect(m_db.data(), SIGNAL(databaseFileChanged()), this, SLOT(reloadDatabaseFile()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::loadDatabase(bool accepted)
|
void DatabaseWidget::loadDatabase(bool accepted)
|
||||||
@ -908,14 +907,12 @@ void DatabaseWidget::loadDatabase(bool accepted)
|
|||||||
if (accepted) {
|
if (accepted) {
|
||||||
replaceDatabase(openWidget->database());
|
replaceDatabase(openWidget->database());
|
||||||
switchToMainView();
|
switchToMainView();
|
||||||
m_fileWatcher->restart();
|
|
||||||
m_saveAttempts = 0;
|
m_saveAttempts = 0;
|
||||||
emit databaseUnlocked();
|
emit databaseUnlocked();
|
||||||
if (config()->get("MinimizeAfterUnlock").toBool()) {
|
if (config()->get("MinimizeAfterUnlock").toBool()) {
|
||||||
window()->showMinimized();
|
window()->showMinimized();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_fileWatcher->stop();
|
|
||||||
if (m_databaseOpenWidget->database()) {
|
if (m_databaseOpenWidget->database()) {
|
||||||
m_databaseOpenWidget->database().reset();
|
m_databaseOpenWidget->database().reset();
|
||||||
}
|
}
|
||||||
@ -1063,7 +1060,6 @@ void DatabaseWidget::switchToOpenDatabase()
|
|||||||
|
|
||||||
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
|
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
|
||||||
{
|
{
|
||||||
updateFilePath(filePath);
|
|
||||||
m_databaseOpenWidget->load(filePath);
|
m_databaseOpenWidget->load(filePath);
|
||||||
setCurrentWidget(m_databaseOpenWidget);
|
setCurrentWidget(m_databaseOpenWidget);
|
||||||
}
|
}
|
||||||
@ -1091,14 +1087,12 @@ void DatabaseWidget::csvImportFinished(bool accepted)
|
|||||||
|
|
||||||
void DatabaseWidget::switchToImportKeepass1(const QString& filePath)
|
void DatabaseWidget::switchToImportKeepass1(const QString& filePath)
|
||||||
{
|
{
|
||||||
updateFilePath(filePath);
|
|
||||||
m_keepass1OpenWidget->load(filePath);
|
m_keepass1OpenWidget->load(filePath);
|
||||||
setCurrentWidget(m_keepass1OpenWidget);
|
setCurrentWidget(m_keepass1OpenWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::switchToImportOpVault(const QString& fileName)
|
void DatabaseWidget::switchToImportOpVault(const QString& fileName)
|
||||||
{
|
{
|
||||||
updateFilePath(fileName);
|
|
||||||
m_opVaultOpenWidget->load(fileName);
|
m_opVaultOpenWidget->load(fileName);
|
||||||
setCurrentWidget(m_opVaultOpenWidget);
|
setCurrentWidget(m_opVaultOpenWidget);
|
||||||
}
|
}
|
||||||
@ -1384,21 +1378,6 @@ bool DatabaseWidget::lock()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::updateFilePath(const QString& filePath)
|
|
||||||
{
|
|
||||||
m_fileWatcher->start(filePath);
|
|
||||||
m_db->setFilePath(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DatabaseWidget::blockAutoReload(bool block)
|
|
||||||
{
|
|
||||||
if (block) {
|
|
||||||
m_fileWatcher->ignoreFileChanges();
|
|
||||||
} else {
|
|
||||||
m_fileWatcher->observeFileChanges(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DatabaseWidget::reloadDatabaseFile()
|
void DatabaseWidget::reloadDatabaseFile()
|
||||||
{
|
{
|
||||||
if (!m_db || isLocked()) {
|
if (!m_db || isLocked()) {
|
||||||
@ -1417,22 +1396,20 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
if (result == MessageBox::No) {
|
if (result == MessageBox::No) {
|
||||||
// Notify everyone the database does not match the file
|
// Notify everyone the database does not match the file
|
||||||
m_db->markAsModified();
|
m_db->markAsModified();
|
||||||
// Rewatch the database file
|
|
||||||
m_fileWatcher->restart();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
auto db = QSharedPointer<Database>::create(m_db->filePath());
|
auto db = QSharedPointer<Database>::create(m_db->filePath());
|
||||||
if (db->open(database()->key(), &error, true)) {
|
if (db->open(database()->key(), &error)) {
|
||||||
if (m_db->isModified()) {
|
if (m_db->isModified()) {
|
||||||
// Ask if we want to merge changes into new database
|
// Ask if we want to merge changes into new database
|
||||||
auto result = MessageBox::question(
|
auto result = MessageBox::question(
|
||||||
this,
|
this,
|
||||||
tr("Merge Request"),
|
tr("Merge Request"),
|
||||||
tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"),
|
tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"),
|
||||||
MessageBox::Merge | MessageBox::Cancel,
|
MessageBox::Merge | MessageBox::Discard,
|
||||||
MessageBox::Merge);
|
MessageBox::Merge);
|
||||||
|
|
||||||
if (result == MessageBox::Merge) {
|
if (result == MessageBox::Merge) {
|
||||||
@ -1442,11 +1419,9 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QUuid groupBeforeReload;
|
QUuid groupBeforeReload = m_db->rootGroup()->uuid();
|
||||||
if (m_groupView && m_groupView->currentGroup()) {
|
if (m_groupView && m_groupView->currentGroup()) {
|
||||||
groupBeforeReload = m_groupView->currentGroup()->uuid();
|
groupBeforeReload = m_groupView->currentGroup()->uuid();
|
||||||
} else {
|
|
||||||
groupBeforeReload = m_db->rootGroup()->uuid();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QUuid entryBeforeReload;
|
QUuid entryBeforeReload;
|
||||||
@ -1454,19 +1429,15 @@ void DatabaseWidget::reloadDatabaseFile()
|
|||||||
entryBeforeReload = m_entryView->currentEntry()->uuid();
|
entryBeforeReload = m_entryView->currentEntry()->uuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isReadOnly = m_db->isReadOnly();
|
|
||||||
replaceDatabase(db);
|
replaceDatabase(db);
|
||||||
m_db->setReadOnly(isReadOnly);
|
|
||||||
restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
|
restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
|
||||||
|
m_blockAutoSave = false;
|
||||||
} else {
|
} else {
|
||||||
showMessage(tr("Could not open the new database file while attempting to autoreload.\nError: %1").arg(error),
|
showMessage(tr("Could not open the new database file while attempting to autoreload.\nError: %1").arg(error),
|
||||||
MessageWidget::Error);
|
MessageWidget::Error);
|
||||||
// Mark db as modified since existing data may differ from file or file was deleted
|
// Mark db as modified since existing data may differ from file or file was deleted
|
||||||
m_db->markAsModified();
|
m_db->markAsModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewatch the database file
|
|
||||||
m_fileWatcher->restart();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int DatabaseWidget::numberOfSelectedEntries() const
|
int DatabaseWidget::numberOfSelectedEntries() const
|
||||||
@ -1604,7 +1575,6 @@ bool DatabaseWidget::save()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent recursions and infinite save loops
|
// Prevent recursions and infinite save loops
|
||||||
blockAutoReload(true);
|
|
||||||
m_blockAutoSave = true;
|
m_blockAutoSave = true;
|
||||||
++m_saveAttempts;
|
++m_saveAttempts;
|
||||||
|
|
||||||
@ -1612,7 +1582,6 @@ bool DatabaseWidget::save()
|
|||||||
bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
|
bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
|
||||||
QString errorMessage;
|
QString errorMessage;
|
||||||
bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get("BackupBeforeSave").toBool());
|
bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get("BackupBeforeSave").toBool());
|
||||||
blockAutoReload(false);
|
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
m_saveAttempts = 0;
|
m_saveAttempts = 0;
|
||||||
|
@ -35,7 +35,7 @@ class KeePass1OpenWidget;
|
|||||||
class OpVaultOpenWidget;
|
class OpVaultOpenWidget;
|
||||||
class DatabaseSettingsDialog;
|
class DatabaseSettingsDialog;
|
||||||
class Database;
|
class Database;
|
||||||
class DelayingFileWatcher;
|
class FileWatcher;
|
||||||
class EditEntryWidget;
|
class EditEntryWidget;
|
||||||
class EditGroupWidget;
|
class EditGroupWidget;
|
||||||
class Entry;
|
class Entry;
|
||||||
@ -108,8 +108,6 @@ public:
|
|||||||
bool currentEntryHasNotes();
|
bool currentEntryHasNotes();
|
||||||
bool currentEntryHasTotp();
|
bool currentEntryHasTotp();
|
||||||
|
|
||||||
void blockAutoReload(bool block = true);
|
|
||||||
|
|
||||||
QByteArray entryViewState() const;
|
QByteArray entryViewState() const;
|
||||||
bool setEntryViewState(const QByteArray& state) const;
|
bool setEntryViewState(const QByteArray& state) const;
|
||||||
QList<int> mainSplitterSizes() const;
|
QList<int> mainSplitterSizes() const;
|
||||||
@ -210,7 +208,6 @@ protected:
|
|||||||
void showEvent(QShowEvent* event) override;
|
void showEvent(QShowEvent* event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateFilePath(const QString& filePath);
|
|
||||||
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
|
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
|
||||||
void switchBackToEntryEdit();
|
void switchBackToEntryEdit();
|
||||||
void switchToHistoryView(Entry* entry);
|
void switchToHistoryView(Entry* entry);
|
||||||
@ -273,7 +270,6 @@ private:
|
|||||||
bool m_searchLimitGroup;
|
bool m_searchLimitGroup;
|
||||||
|
|
||||||
// Autoreload
|
// Autoreload
|
||||||
QPointer<DelayingFileWatcher> m_fileWatcher;
|
|
||||||
bool m_blockAutoSave;
|
bool m_blockAutoSave;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ if(WITH_XC_KEESHARE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
|
add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS testsupport ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testtools SOURCES TestTools.cpp
|
add_unit_test(NAME testtools SOURCES TestTools.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
@ -20,13 +20,14 @@
|
|||||||
#include "TestGlobal.h"
|
#include "TestGlobal.h"
|
||||||
|
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QTemporaryFile>
|
|
||||||
|
|
||||||
#include "config-keepassx-tests.h"
|
#include "config-keepassx-tests.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
|
#include "core/Tools.h"
|
||||||
#include "crypto/Crypto.h"
|
#include "crypto/Crypto.h"
|
||||||
#include "format/KeePass2Writer.h"
|
#include "format/KeePass2Writer.h"
|
||||||
#include "keys/PasswordKey.h"
|
#include "keys/PasswordKey.h"
|
||||||
|
#include "util/TemporaryFile.h"
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestDatabase)
|
QTEST_GUILESS_MAIN(TestDatabase)
|
||||||
|
|
||||||
@ -35,6 +36,60 @@ void TestDatabase::initTestCase()
|
|||||||
QVERIFY(Crypto::init());
|
QVERIFY(Crypto::init());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDatabase::testOpen()
|
||||||
|
{
|
||||||
|
auto db = QSharedPointer<Database>::create();
|
||||||
|
QVERIFY(!db->isInitialized());
|
||||||
|
QVERIFY(!db->isModified());
|
||||||
|
|
||||||
|
auto key = QSharedPointer<CompositeKey>::create();
|
||||||
|
key->addKey(QSharedPointer<PasswordKey>::create("a"));
|
||||||
|
|
||||||
|
bool ok = db->open(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"), key);
|
||||||
|
QVERIFY(ok);
|
||||||
|
|
||||||
|
QVERIFY(db->isInitialized());
|
||||||
|
QVERIFY(!db->isModified());
|
||||||
|
|
||||||
|
db->metadata()->setName("test");
|
||||||
|
QVERIFY(db->isModified());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestDatabase::testSave()
|
||||||
|
{
|
||||||
|
QByteArray data;
|
||||||
|
QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
|
||||||
|
QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
|
||||||
|
QVERIFY(Tools::readAllFromDevice(&sourceDbFile, data));
|
||||||
|
sourceDbFile.close();
|
||||||
|
|
||||||
|
TemporaryFile tempFile;
|
||||||
|
QVERIFY(tempFile.open());
|
||||||
|
QCOMPARE(tempFile.write(data), static_cast<qint64>((data.size())));
|
||||||
|
tempFile.close();
|
||||||
|
|
||||||
|
auto db = QSharedPointer<Database>::create();
|
||||||
|
auto key = QSharedPointer<CompositeKey>::create();
|
||||||
|
key->addKey(QSharedPointer<PasswordKey>::create("a"));
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
bool ok = db->open(tempFile.fileName(), key, &error);
|
||||||
|
QVERIFY(ok);
|
||||||
|
|
||||||
|
// Test safe saves
|
||||||
|
db->metadata()->setName("test");
|
||||||
|
QVERIFY(db->isModified());
|
||||||
|
|
||||||
|
// Test unsafe saves
|
||||||
|
QVERIFY2(db->save(&error, false, false), error.toLatin1());
|
||||||
|
|
||||||
|
QVERIFY2(db->save(&error), error.toLatin1());
|
||||||
|
QVERIFY(!db->isModified());
|
||||||
|
|
||||||
|
// Test save backups
|
||||||
|
QVERIFY2(db->save(&error, true, true), error.toLatin1());
|
||||||
|
}
|
||||||
|
|
||||||
void TestDatabase::testEmptyRecycleBinOnDisabled()
|
void TestDatabase::testEmptyRecycleBinOnDisabled()
|
||||||
{
|
{
|
||||||
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/RecycleBinDisabled.kdbx");
|
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/RecycleBinDisabled.kdbx");
|
||||||
|
@ -27,6 +27,8 @@ class TestDatabase : public QObject
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
|
void testOpen();
|
||||||
|
void testSave();
|
||||||
void testEmptyRecycleBinOnDisabled();
|
void testEmptyRecycleBinOnDisabled();
|
||||||
void testEmptyRecycleBinOnNotCreated();
|
void testEmptyRecycleBinOnNotCreated();
|
||||||
void testEmptyRecycleBinOnEmpty();
|
void testEmptyRecycleBinOnEmpty();
|
||||||
|
Loading…
Reference in New Issue
Block a user