Detect background changes to database file.

This gives the option to reload the database.

TODO:
 - Settings for reloadBehavior (ask, reloadUnchanged, ignore)
 - Improve notification, by using a header instead of dialog: nicer, less
intrusive, gives more options to user, and works better when multiple databases
are open.
 - Keep tab order on reload.
This commit is contained in:
Francois Ferrand 2013-04-24 22:38:34 +02:00
parent 850c7c7ecf
commit d5c8787451
10 changed files with 200 additions and 13 deletions

View File

@ -227,6 +227,11 @@ bool Database::verifyKey(const CompositeKey& key) const
return (m_key.rawKey() == key.rawKey());
}
CompositeKey Database::key() const
{
return m_key;
}
void Database::createRecycleBin()
{
Group* recycleBin = Group::createRecycleBin();

View File

@ -88,6 +88,7 @@ public:
void setKey(const CompositeKey& key);
bool hasKey() const;
bool verifyKey(const CompositeKey& key) const;
CompositeKey key() const;
void recycleEntry(Entry* entry);
void recycleGroup(Group* group);
void setEmitModified(bool value);

View File

@ -91,14 +91,26 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
openDatabase();
}
void DatabaseOpenWidget::enterKey(const CompositeKey& masterKey)
{
if (masterKey.isEmpty()) {
return;
}
openDatabase(masterKey);
}
void DatabaseOpenWidget::openDatabase()
{
KeePass2Reader reader;
CompositeKey masterKey = databaseKey();
if (masterKey.isEmpty()) {
return;
}
openDatabase(masterKey);
}
void DatabaseOpenWidget::openDatabase(const CompositeKey& masterKey)
{
KeePass2Reader reader;
QFile file(m_filename);
if (!file.open(QIODevice::ReadOnly)) {
// TODO: error message

View File

@ -39,6 +39,7 @@ public:
~DatabaseOpenWidget();
void load(const QString& filename);
void enterKey(const QString& pw, const QString& keyFile);
void enterKey(const CompositeKey& masterKey);
Database* database();
Q_SIGNALS:
@ -49,6 +50,7 @@ protected:
protected Q_SLOTS:
virtual void openDatabase();
void openDatabase(const CompositeKey& masterKey);
void reject();
private Q_SLOTS:

View File

@ -20,6 +20,9 @@
#include <QtCore/QFileInfo>
#include <QtGui/QTabWidget>
#include <QtGui/QMessageBox>
#include <QtCore/QFileSystemWatcher>
#include <QtCore/QTimer>
#include <QtCore/QDebug>
#include "autotype/AutoType.h"
#include "core/Config.h"
@ -45,7 +48,9 @@ DatabaseManagerStruct::DatabaseManagerStruct()
const int DatabaseTabWidget::LastDatabasesCount = 5;
DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
: QTabWidget(parent),
m_fileWatcher(new QFileSystemWatcher(this)),
m_reloadBehavior(ReloadUnmodified) //TODO: setting
{
DragTabBar* tabBar = new DragTabBar(this);
tabBar->setDrawBase(false);
@ -53,6 +58,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabase(int)));
connect(autoType(), SIGNAL(globalShortcutTriggered()), SLOT(performGlobalAutoType()));
connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
}
DatabaseTabWidget::~DatabaseTabWidget()
@ -92,7 +98,7 @@ void DatabaseTabWidget::openDatabase()
}
void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw,
const QString& keyFile)
const QString& keyFile, const CompositeKey& key)
{
QFileInfo fileInfo(fileName);
QString canonicalFilePath = fileInfo.canonicalFilePath();
@ -136,12 +142,17 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw,
dbStruct.filePath = fileInfo.absoluteFilePath();
dbStruct.canonicalFilePath = canonicalFilePath;
dbStruct.fileName = fileInfo.fileName();
dbStruct.lastModified = fileInfo.lastModified();
insertDatabase(db, dbStruct);
m_fileWatcher->addPath(dbStruct.filePath);
updateRecentDatabases(dbStruct.filePath);
if (!pw.isNull() || !keyFile.isEmpty()) {
if (!key.isEmpty()) {
dbStruct.dbWidget->switchToOpenDatabase(dbStruct.filePath, key);
}
else if (!pw.isNull() || !keyFile.isEmpty()) {
dbStruct.dbWidget->switchToOpenDatabase(dbStruct.filePath, pw, keyFile);
}
else {
@ -168,6 +179,111 @@ void DatabaseTabWidget::importKeePass1Database()
dbStruct.dbWidget->switchToImportKeepass1(fileName);
}
void DatabaseTabWidget::fileChanged(const QString &fileName)
{
const bool wasEmpty = m_changedFiles.isEmpty();
m_changedFiles.insert(fileName);
if (wasEmpty && !m_changedFiles.isEmpty())
QTimer::singleShot(200, this, SLOT(checkReloadDatabases()));
}
void DatabaseTabWidget::expectFileChange(const DatabaseManagerStruct& dbStruct)
{
if (dbStruct.filePath.isEmpty())
return;
m_expectedFileChanges.insert(dbStruct.filePath);
}
void DatabaseTabWidget::unexpectFileChange(DatabaseManagerStruct& dbStruct)
{
if (dbStruct.filePath.isEmpty())
return;
m_expectedFileChanges.remove(dbStruct.filePath);
dbStruct.lastModified = QFileInfo(dbStruct.filePath).lastModified();
}
void DatabaseTabWidget::checkReloadDatabases()
{
QSet<QString> changedFiles;
changedFiles = m_changedFiles.subtract(m_expectedFileChanges);
m_changedFiles.clear();
if (changedFiles.isEmpty())
return;
Q_FOREACH (DatabaseManagerStruct dbStruct, m_dbList) {
QString filePath = dbStruct.filePath;
Database * db = dbStruct.dbWidget->database();
if (!changedFiles.contains(filePath))
continue;
QFileInfo fi(filePath);
QDateTime lastModified = fi.lastModified();
if (dbStruct.lastModified == lastModified)
continue;
DatabaseWidget::Mode mode = dbStruct.dbWidget->currentMode();
if (mode == DatabaseWidget::None || mode == DatabaseWidget::LockedMode || !db->hasKey())
continue;
if ( (m_reloadBehavior == AlwaysAsk)
|| (m_reloadBehavior == ReloadUnmodified && mode == DatabaseWidget::EditMode)
|| (m_reloadBehavior == ReloadUnmodified && dbStruct.modified)) {
//TODO: display banner instead, to let user now file has changed and choose to Reload, Overwrite, and SaveAs
// --> less obstrubsive (esp. if multiple DB are open), cleaner UI
if (QMessageBox::warning(this, fi.exists() ? tr("Database file changed") : tr("Database file removed"),
tr("Do you want to discard your changes and reload?"),
QMessageBox::Yes|QMessageBox::No) == QMessageBox::No)
continue;
}
if (fi.exists()) {
//Ignore/cancel all edits
dbStruct.dbWidget->switchToView(false);
dbStruct.modified = false;
//Save current group/entry
Uuid currentGroup;
if (Group* group = dbStruct.dbWidget->groupView()->currentGroup())
currentGroup = group->uuid();
Uuid currentEntry;
if (Entry* entry = dbStruct.dbWidget->entryView()->currentEntry())
currentEntry = entry->uuid();
QString searchText = dbStruct.dbWidget->searchText();
//Reload updated db
CompositeKey key = db->key();
closeDatabase(db);
openDatabase(filePath, QString(), QString(), key);
//Restore current group/entry
dbStruct = indexDatabaseManagerStruct(count() - 1);
if (dbStruct.dbWidget) {
Database * db = dbStruct.dbWidget->database();
if (!searchText.isEmpty())
dbStruct.dbWidget->showSearch(searchText);
if (!currentGroup.isNull())
if (Group* group = db->resolveGroup(currentGroup))
dbStruct.dbWidget->groupView()->setCurrentGroup(group);
if (!currentEntry.isNull())
if (Entry* entry = db->resolveEntry(currentEntry))
dbStruct.dbWidget->entryView()->setCurrentEntry(entry);
}
//TODO: keep tab order...
} else {
//Ignore/cancel all edits
dbStruct.dbWidget->switchToView(false);
dbStruct.modified = false;
//Close database
closeDatabase(dbStruct.dbWidget->database());
}
}
}
bool DatabaseTabWidget::closeDatabase(Database* db)
{
Q_ASSERT(db);
@ -219,6 +335,7 @@ void DatabaseTabWidget::deleteDatabase(Database* db)
const DatabaseManagerStruct dbStruct = m_dbList.value(db);
int index = databaseIndex(db);
m_fileWatcher->removePath(dbStruct.filePath);
removeTab(index);
toggleTabbar();
m_dbList.remove(db);
@ -260,12 +377,16 @@ void DatabaseTabWidget::saveDatabase(Database* db)
if (dbStruct.saveToFilename) {
bool result = false;
expectFileChange(dbStruct);
QSaveFile saveFile(dbStruct.filePath);
if (saveFile.open(QIODevice::WriteOnly)) {
m_writer.writeDatabase(&saveFile, db);
result = saveFile.commit();
}
unexpectFileChange(dbStruct);
if (result) {
dbStruct.modified = false;
updateTabName(db);
@ -283,12 +404,12 @@ void DatabaseTabWidget::saveDatabase(Database* db)
void DatabaseTabWidget::saveDatabaseAs(Database* db)
{
DatabaseManagerStruct& dbStruct = m_dbList[db];
QString oldFileName;
QString oldFilePath;
if (dbStruct.saveToFilename) {
oldFileName = dbStruct.filePath;
oldFilePath = dbStruct.filePath;
}
QString fileName = fileDialog()->getSaveFileName(this, tr("Save database as"),
oldFileName, tr("KeePass 2 Database").append(" (*.kdbx)"));
oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"));
if (!fileName.isEmpty()) {
bool result = false;
@ -299,15 +420,18 @@ void DatabaseTabWidget::saveDatabaseAs(Database* db)
}
if (result) {
m_fileWatcher->removePath(oldFilePath);
dbStruct.modified = false;
dbStruct.saveToFilename = true;
QFileInfo fileInfo(fileName);
dbStruct.filePath = fileInfo.absoluteFilePath();
dbStruct.canonicalFilePath = fileInfo.canonicalFilePath();
dbStruct.fileName = fileInfo.fileName();
dbStruct.lastModified = fileInfo.lastModified();
dbStruct.dbWidget->updateFilename(dbStruct.filePath);
updateTabName(db);
updateRecentDatabases(dbStruct.filePath);
m_fileWatcher->addPath(dbStruct.filePath);
}
else {
QMessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n"

View File

@ -18,7 +18,9 @@
#ifndef KEEPASSX_DATABASETABWIDGET_H
#define KEEPASSX_DATABASETABWIDGET_H
#include <QtCore/QDateTime>
#include <QtCore/QHash>
#include <QtCore/QSet>
#include <QtGui/QTabWidget>
#include "format/KeePass2Writer.h"
@ -27,6 +29,7 @@
class DatabaseWidget;
class DatabaseOpenWidget;
class QFile;
class QFileSystemWatcher;
struct DatabaseManagerStruct
{
@ -39,6 +42,7 @@ struct DatabaseManagerStruct
bool saveToFilename;
bool modified;
bool readOnly;
QDateTime lastModified;
};
Q_DECLARE_TYPEINFO(DatabaseManagerStruct, Q_MOVABLE_TYPE);
@ -51,16 +55,24 @@ public:
explicit DatabaseTabWidget(QWidget* parent = Q_NULLPTR);
~DatabaseTabWidget();
void openDatabase(const QString& fileName, const QString& pw = QString(),
const QString& keyFile = QString());
const QString& keyFile = QString(), const CompositeKey& key = CompositeKey());
DatabaseWidget* currentDatabaseWidget();
bool hasLockableDatabases();
static const int LastDatabasesCount;
enum ReloadBehavior {
AlwaysAsk,
ReloadUnmodified,
IgnoreAll
};
public Q_SLOTS:
void newDatabase();
void openDatabase();
void importKeePass1Database();
void fileChanged(const QString& fileName);
void checkReloadDatabases();
void saveDatabase(int index = -1);
void saveDatabaseAs(int index = -1);
bool closeDatabase(int index = -1);
@ -96,9 +108,15 @@ private:
void insertDatabase(Database* db, const DatabaseManagerStruct& dbStruct);
void updateRecentDatabases(const QString& filename);
void connectDatabase(Database* newDb, Database* oldDb = Q_NULLPTR);
void expectFileChange(const DatabaseManagerStruct& dbStruct);
void unexpectFileChange(DatabaseManagerStruct& dbStruct);
KeePass2Writer m_writer;
QHash<Database*, DatabaseManagerStruct> m_dbList;
QSet<QString> m_changedFiles;
QSet<QString> m_expectedFileChanges;
QFileSystemWatcher* m_fileWatcher;
ReloadBehavior m_reloadBehavior;
};
#endif // KEEPASSX_DATABASETABWIDGET_H

View File

@ -533,6 +533,13 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString
m_databaseOpenWidget->enterKey(password, keyFile);
}
void DatabaseWidget::switchToOpenDatabase(const QString &fileName, const CompositeKey& masterKey)
{
updateFilename(fileName);
switchToOpenDatabase(fileName);
m_databaseOpenWidget->enterKey(masterKey);
}
void DatabaseWidget::switchToImportKeepass1(const QString& fileName)
{
updateFilename(fileName);
@ -556,10 +563,10 @@ void DatabaseWidget::closeSearch()
m_groupView->setCurrentGroup(m_lastGroup);
}
void DatabaseWidget::showSearch()
void DatabaseWidget::showSearch(const QString & searchString)
{
m_searchUi->searchEdit->blockSignals(true);
m_searchUi->searchEdit->clear();
m_searchUi->searchEdit->setText(searchString);
m_searchUi->searchEdit->blockSignals(false);
m_searchUi->searchCurrentRadioButton->blockSignals(true);
@ -665,6 +672,11 @@ bool DatabaseWidget::isInSearchMode()
return m_entryView->inEntryListMode();
}
QString DatabaseWidget::searchText()
{
return m_entryView->inEntryListMode() ? m_searchUi->searchEdit->text() : QString();
}
void DatabaseWidget::clearLastGroup(Group* group)
{
if (group) {

View File

@ -37,6 +37,7 @@ class KeePass1OpenWidget;
class QFile;
class QMenu;
class UnlockDatabaseWidget;
class CompositeKey;
namespace Ui {
class SearchWidget;
@ -63,6 +64,7 @@ public:
bool dbHasKey();
bool canDeleteCurrentGoup();
bool isInSearchMode();
QString searchText();
int addWidget(QWidget* w);
void setCurrentIndex(int index);
void setCurrentWidget(QWidget* widget);
@ -97,14 +99,16 @@ public Q_SLOTS:
void switchToDatabaseSettings();
void switchToOpenDatabase(const QString& fileName);
void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile);
void switchToOpenDatabase(const QString &fileName, const CompositeKey &masterKey);
void switchToImportKeepass1(const QString& fileName);
void switchToView(bool accepted);
void toggleSearch();
void showSearch(const QString & searchString = QString());
void emitGroupContextMenuRequested(const QPoint& pos);
void emitEntryContextMenuRequested(const QPoint& pos);
private Q_SLOTS:
void switchBackToEntryEdit();
void switchToView(bool accepted);
void switchToHistoryView(Entry* entry);
void switchToEntryEdit(Entry* entry);
void switchToEntryEdit(Entry* entry, bool create);
@ -117,7 +121,6 @@ private Q_SLOTS:
void search();
void startSearch();
void startSearchTimer();
void showSearch();
void closeSearch();
private:

View File

@ -284,6 +284,15 @@ void MainWindow::clearLastDatabases()
config()->set("LastDatabases", QVariant());
}
void MainWindow::changeEvent(QEvent *e)
{
QMainWindow::changeEvent(e);
if (e->type() == QEvent::ActivationChange) {
if (isActiveWindow())
m_ui->tabWidget->checkReloadDatabases();
}
}
void MainWindow::openDatabase(const QString& fileName, const QString& pw, const QString& keyFile)
{
m_ui->tabWidget->openDatabase(fileName, pw, keyFile);

View File

@ -41,6 +41,7 @@ public Q_SLOTS:
const QString& keyFile = QString());
protected:
void changeEvent(QEvent *e);
void closeEvent(QCloseEvent* event) Q_DECL_OVERRIDE;
private Q_SLOTS: