diff --git a/src/core/Config.cpp b/src/core/Config.cpp index e39f6e43b..03b5e4755 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -92,6 +92,7 @@ void Config::init(const QString& fileName) m_defaults.insert("RememberLastKeyFiles", true); m_defaults.insert("OpenPreviousDatabasesOnStartup", true); m_defaults.insert("AutoSaveAfterEveryChange", false); + m_defaults.insert("AutoReloadOnChange", true); m_defaults.insert("AutoSaveOnExit", false); m_defaults.insert("ShowToolbar", true); m_defaults.insert("MinimizeOnCopy", false); diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 6dc971b33..336820381 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -324,3 +324,9 @@ void Database::startModifiedTimer() } m_timer->start(150); } + +const CompositeKey & Database::key() const +{ + return m_data.key; +} + diff --git a/src/core/Database.h b/src/core/Database.h index 6d2237d4c..3cd5ed1b1 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -88,6 +88,7 @@ public: QByteArray transformSeed() const; quint64 transformRounds() const; QByteArray transformedMasterKey() const; + const CompositeKey & key() const; void setCipher(const Uuid& cipher); void setCompressionAlgo(Database::CompressionAlgorithm algo); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index eb293a935..2028c7828 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -547,7 +547,7 @@ void Group::merge(const Group* other) if (!findEntry(entry->uuid())) { entry->clone(Entry::CloneNoFlags)->setGroup(this); } else { - resolveConflict(this->findEntry(entry->uuid()), entry); + resolveConflict(findEntry(entry->uuid()), entry); } } @@ -555,8 +555,8 @@ void Group::merge(const Group* other) const QList dbChildren = other->children(); for (Group* group : dbChildren) { // groups are searched by name instead of uuid - if (this->findChildByName(group->name())) { - this->findChildByName(group->name())->merge(group); + if (findChildByName(group->name())) { + findChildByName(group->name())->merge(group); } else { group->setParent(this); } @@ -765,24 +765,24 @@ void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) Entry* clonedEntry; - switch(this->mergeMode()) { + switch(mergeMode()) { case KeepBoth: // if one entry is newer, create a clone and add it to the group if (timeExisting > timeOther) { clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry->setGroup(this); - this->markOlderEntry(clonedEntry); + markOlderEntry(clonedEntry); } else if (timeExisting < timeOther) { clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry->setGroup(this); - this->markOlderEntry(existingEntry); + markOlderEntry(existingEntry); } break; case KeepNewer: if (timeExisting < timeOther) { // only if other entry is newer, replace existing one - this->removeEntry(existingEntry); - this->addEntry(otherEntry); + removeEntry(existingEntry); + addEntry(otherEntry->clone(Entry::CloneNoFlags)); } break; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index d4001501d..af6907001 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -221,6 +221,7 @@ void DatabaseTabWidget::importKeePass1Database() Database* db = new Database(); DatabaseManagerStruct dbStruct; dbStruct.dbWidget = new DatabaseWidget(db, this); + dbStruct.dbWidget->databaseModified(); dbStruct.modified = true; insertDatabase(db, dbStruct); @@ -312,17 +313,28 @@ bool DatabaseTabWidget::closeAllDatabases() bool DatabaseTabWidget::saveDatabase(Database* db) { DatabaseManagerStruct& dbStruct = m_dbList[db]; + // temporarily disable autoreload + dbStruct.dbWidget->ignoreNextAutoreload(); if (dbStruct.saveToFilename) { QSaveFile saveFile(dbStruct.canonicalFilePath); if (saveFile.open(QIODevice::WriteOnly)) { + // write the database to the file m_writer.writeDatabase(&saveFile, db); if (m_writer.hasError()) { MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" + m_writer.errorString()); return false; } - if (!saveFile.commit()) { + + if (saveFile.commit()) { + // successfully saved database file + dbStruct.modified = false; + dbStruct.dbWidget->databaseSaved(); + updateTabName(db); + return true; + } + else { MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" + saveFile.errorString()); return false; @@ -333,10 +345,6 @@ bool DatabaseTabWidget::saveDatabase(Database* db) + saveFile.errorString()); return false; } - - dbStruct.modified = false; - updateTabName(db); - return true; } else { return saveDatabaseAs(db); @@ -390,22 +398,14 @@ bool DatabaseTabWidget::saveDatabaseAs(Database* db) } } - QSaveFile saveFile(fileName); - if (!saveFile.open(QIODevice::WriteOnly)) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + saveFile.errorString()); - return false; - } + // setup variables so saveDatabase succeeds + dbStruct.saveToFilename = true; + dbStruct.canonicalFilePath = fileName; - m_writer.writeDatabase(&saveFile, db); - if (m_writer.hasError()) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + m_writer.errorString()); - return false; - } - if (!saveFile.commit()) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + saveFile.errorString()); + if (!saveDatabase(db)) { + // failed to save, revert back + dbStruct.saveToFilename = false; + dbStruct.canonicalFilePath = oldFileName; return false; } @@ -626,7 +626,7 @@ void DatabaseTabWidget::insertDatabase(Database* db, const DatabaseManagerStruct setCurrentIndex(index); connectDatabase(db); connect(dbStruct.dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseFromSender())); - connect(dbStruct.dbWidget, SIGNAL(databaseChanged(Database*)), SLOT(changeDatabase(Database*))); + connect(dbStruct.dbWidget, SIGNAL(databaseChanged(Database*, bool)), SLOT(changeDatabase(Database*, bool))); connect(dbStruct.dbWidget, SIGNAL(unlockedDatabase()), SLOT(updateTabNameFromDbWidgetSender())); connect(dbStruct.dbWidget, SIGNAL(unlockedDatabase()), SLOT(emitDatabaseUnlockedFromDbWidgetSender())); } @@ -744,6 +744,7 @@ void DatabaseTabWidget::modified() if (!dbStruct.modified) { dbStruct.modified = true; + dbStruct.dbWidget->databaseModified(); updateTabName(db); } } @@ -765,7 +766,7 @@ void DatabaseTabWidget::updateLastDatabases(const QString& filename) } } -void DatabaseTabWidget::changeDatabase(Database* newDb) +void DatabaseTabWidget::changeDatabase(Database* newDb, bool unsavedChanges) { Q_ASSERT(sender()); Q_ASSERT(!m_dbList.contains(newDb)); @@ -773,6 +774,7 @@ void DatabaseTabWidget::changeDatabase(Database* newDb) DatabaseWidget* dbWidget = static_cast(sender()); Database* oldDb = databaseFromDatabaseWidget(dbWidget); DatabaseManagerStruct dbStruct = m_dbList[oldDb]; + dbStruct.modified = unsavedChanges; m_dbList.remove(oldDb); m_dbList.insert(newDb, dbStruct); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 7d095b560..24bdbde2f 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -91,7 +91,7 @@ private Q_SLOTS: void updateTabNameFromDbWidgetSender(); void modified(); void toggleTabbar(); - void changeDatabase(Database* newDb); + void changeDatabase(Database* newDb, bool unsavedChanges); void emitActivateDatabaseChanged(); void emitDatabaseUnlockedFromDbWidgetSender(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 7c899d3ac..89e7bf689 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "core/Group.h" #include "core/Metadata.h" #include "core/Tools.h" +#include "format/KeePass2Reader.h" #include "gui/ChangeMasterKeyWidget.h" #include "gui/Clipboard.h" #include "gui/DatabaseOpenWidget.h" @@ -156,9 +158,18 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_databaseOpenMergeWidget, SIGNAL(editFinished(bool)), SLOT(mergeDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); - connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); + connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); + connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); + connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile())); + connect(&m_ignoreWatchTimer, SIGNAL(timeout()), this, SLOT(onWatchedFileChanged())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); + m_databaseModified = false; + + m_fileWatchTimer.setSingleShot(true); + m_ignoreWatchTimer.setSingleShot(true); + m_ignoreNextAutoreload = false; + m_searchCaseSensitive = false; m_searchCurrentGroup = false; @@ -291,7 +302,7 @@ void DatabaseWidget::replaceDatabase(Database* db) Database* oldDb = m_db; m_db = db; m_groupView->changeDatabase(m_db); - Q_EMIT databaseChanged(m_db); + Q_EMIT databaseChanged(m_db, m_databaseModified); delete oldDb; } @@ -663,8 +674,10 @@ void DatabaseWidget::openDatabase(bool accepted) m_databaseOpenWidget = nullptr; delete m_keepass1OpenWidget; m_keepass1OpenWidget = nullptr; + m_fileWatcher.addPath(m_filename); } else { + m_fileWatcher.removePath(m_filename); if (m_databaseOpenWidget->database()) { delete m_databaseOpenWidget->database(); } @@ -809,6 +822,16 @@ void DatabaseWidget::switchToImportKeepass1(const QString& fileName) setCurrentWidget(m_keepass1OpenWidget); } +void DatabaseWidget::databaseModified() +{ + m_databaseModified = true; +} + +void DatabaseWidget::databaseSaved() +{ + m_databaseModified = false; +} + void DatabaseWidget::search(const QString& searchtext) { if (searchtext.isEmpty()) @@ -947,9 +970,99 @@ void DatabaseWidget::lock() void DatabaseWidget::updateFilename(const QString& fileName) { + if (! m_filename.isEmpty()) { + m_fileWatcher.removePath(m_filename); + } + + m_fileWatcher.addPath(fileName); m_filename = fileName; } +void DatabaseWidget::ignoreNextAutoreload() +{ + m_ignoreNextAutoreload = true; + m_ignoreWatchTimer.start(100); +} + +void DatabaseWidget::onWatchedFileChanged() +{ + if (m_ignoreNextAutoreload) { + // Reset the watch + m_ignoreNextAutoreload = false; + m_ignoreWatchTimer.stop(); + m_fileWatcher.addPath(m_filename); + } + else { + if (m_fileWatchTimer.isActive()) + return; + + m_fileWatchTimer.start(500); + } +} + +void DatabaseWidget::reloadDatabaseFile() +{ + if (m_db == nullptr) + return; + + if (! config()->get("AutoReloadOnChange").toBool()) { + // Ask if we want to reload the db + QMessageBox::StandardButton mb = MessageBox::question(this, tr("Autoreload Request"), + tr("The database file has changed. Do you want to load the changes?"), + QMessageBox::Yes | QMessageBox::No); + + if (mb == QMessageBox::No) { + // Notify everyone the database does not match the file + emit m_db->modified(); + m_databaseModified = true; + // Rewatch the database file + m_fileWatcher.addPath(m_filename); + return; + } + } + + KeePass2Reader reader; + QFile file(m_filename); + if (file.open(QIODevice::ReadOnly)) { + Database* db = reader.readDatabase(&file, database()->key()); + if (db != nullptr) { + if (m_databaseModified) { + // Ask if we want to merge changes into new database + QMessageBox::StandardButton mb = MessageBox::question(this, tr("Merge Request"), + tr("The database file has changed and you have unsaved changes." + "Do you want to merge your changes?"), + QMessageBox::Yes | QMessageBox::No); + + if (mb == QMessageBox::Yes) { + // Merge the old database into the new one + m_db->setEmitModified(false); + db->merge(m_db); + } + else { + // Since we are accepting the new file as-is, internally mark as unmodified + // TODO: when saving is moved out of DatabaseTabWidget, this should be replaced + m_databaseModified = false; + } + } + + replaceDatabase(db); + } + else { + MessageBox::critical(this, tr("Autoreload Failed"), + tr("Could not parse or unlock the new database file while attempting" + "to autoreload this database.")); + } + } + else { + MessageBox::critical(this, tr("Autoreload Failed"), + tr("Could not open the new database file while attempting to autoreload" + "this database.")); + } + + // Rewatch the database file + m_fileWatcher.addPath(m_filename); +} + int DatabaseWidget::numberOfSelectedEntries() const { return m_entryView->numberOfSelectedEntries(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 7e0c7013f..1031def44 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -20,6 +20,8 @@ #include #include +#include +#include #include "core/Uuid.h" @@ -42,6 +44,7 @@ class QSplitter; class QLabel; class UnlockDatabaseWidget; class UnlockDatabaseDialog; +class QFileSystemWatcher; class DatabaseWidget : public QStackedWidget { @@ -89,13 +92,14 @@ public: EntryView* entryView(); void showUnlockDialog(); void closeUnlockDialog(); + void ignoreNextAutoreload(); Q_SIGNALS: void closeRequest(); void currentModeChanged(DatabaseWidget::Mode mode); void groupChanged(); void entrySelectionChanged(); - void databaseChanged(Database* newDb); + void databaseChanged(Database* newDb, bool unsavedChanges); void databaseMerged(Database* mergedDb); void groupContextMenuRequested(const QPoint& globalPos); void entryContextMenuRequested(const QPoint& globalPos); @@ -133,6 +137,8 @@ public Q_SLOTS: void switchToOpenMergeDatabase(const QString& fileName); void switchToOpenMergeDatabase(const QString& fileName, const QString& password, const QString& keyFile); void switchToImportKeepass1(const QString& fileName); + void databaseModified(); + void databaseSaved(); // Search related slots void search(const QString& searchtext); void setSearchCaseSensitive(bool state); @@ -154,6 +160,9 @@ private Q_SLOTS: void unlockDatabase(bool accepted); void emitCurrentModeChanged(); void clearLastGroup(Group* group); + // Database autoreload slots + void onWatchedFileChanged(); + void reloadDatabaseFile(); private: void setClipboardTextAndMinimize(const QString& text); @@ -187,6 +196,13 @@ private: QString m_lastSearchText; bool m_searchCaseSensitive; bool m_searchCurrentGroup; + + // Autoreload + QFileSystemWatcher m_fileWatcher; + QTimer m_fileWatchTimer; + bool m_ignoreNextAutoreload; + QTimer m_ignoreWatchTimer; + bool m_databaseModified; }; #endif // KEEPASSX_DATABASEWIDGET_H diff --git a/src/gui/SettingsWidget.cpp b/src/gui/SettingsWidget.cpp index d4c4497a9..49d9a6968 100644 --- a/src/gui/SettingsWidget.cpp +++ b/src/gui/SettingsWidget.cpp @@ -102,6 +102,7 @@ void SettingsWidget::loadSettings() config()->get("OpenPreviousDatabasesOnStartup").toBool()); m_generalUi->autoSaveAfterEveryChangeCheckBox->setChecked(config()->get("AutoSaveAfterEveryChange").toBool()); m_generalUi->autoSaveOnExitCheckBox->setChecked(config()->get("AutoSaveOnExit").toBool()); + m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get("AutoReloadOnChange").toBool()); m_generalUi->minimizeOnCopyCheckBox->setChecked(config()->get("MinimizeOnCopy").toBool()); m_generalUi->useGroupIconOnEntryCreationCheckBox->setChecked(config()->get("UseGroupIconOnEntryCreation").toBool()); m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get("AutoTypeEntryTitleMatch").toBool()); @@ -156,6 +157,7 @@ void SettingsWidget::saveSettings() config()->set("AutoSaveAfterEveryChange", m_generalUi->autoSaveAfterEveryChangeCheckBox->isChecked()); config()->set("AutoSaveOnExit", m_generalUi->autoSaveOnExitCheckBox->isChecked()); + config()->set("AutoReloadOnChange", m_generalUi->autoReloadOnChangeCheckBox->isChecked()); config()->set("MinimizeOnCopy", m_generalUi->minimizeOnCopyCheckBox->isChecked()); config()->set("UseGroupIconOnEntryCreation", m_generalUi->useGroupIconOnEntryCreationCheckBox->isChecked()); diff --git a/src/gui/SettingsWidgetGeneral.ui b/src/gui/SettingsWidgetGeneral.ui index 4969c59f1..eb9209777 100644 --- a/src/gui/SettingsWidgetGeneral.ui +++ b/src/gui/SettingsWidgetGeneral.ui @@ -56,54 +56,54 @@ + + + Automatically reload the database when modified externally + + + + Minimize when copying to clipboard - + Use group icon on entry creation - + Global Auto-Type shortcut - + - + Use entry title to match windows for global auto-type - + Language - + - - - - Show a system tray icon - - - - + QLayout::SetMaximumSize @@ -139,7 +139,7 @@ - + QLayout::SetMaximumSize @@ -169,7 +169,7 @@ - + QLayout::SetMaximumSize @@ -183,6 +183,13 @@ + + + + Show a system tray icon + + + diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index f19c38163..169b8ef4f 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -66,20 +66,20 @@ void TestGui::initTestCase() QByteArray tmpData; QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); - QVERIFY(Tools::readAllFromDevice(&sourceDbFile, tmpData)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); sourceDbFile.close(); - - // Write the temp storage to a temp database file for use in our tests - QVERIFY(m_dbFile.open()); - QCOMPARE(m_dbFile.write(tmpData), static_cast((tmpData.size()))); - m_dbFile.close(); - - m_dbFileName = QFileInfo(m_dbFile).fileName(); } // Every test starts with opening the temp database void TestGui::init() { + // Write the temp storage to a temp database file for use in our tests + QVERIFY(m_dbFile.open()); + QCOMPARE(m_dbFile.write(m_dbData), static_cast((m_dbData.size()))); + m_dbFile.close(); + + m_dbFileName = QFileInfo(m_dbFile).fileName(); + fileDialog()->setNextFileName(m_dbFile.fileName()); triggerAction("actionDatabaseOpen"); @@ -111,8 +111,8 @@ void TestGui::cleanup() void TestGui::testMergeDatabase() { - // this triggers a warning. Perhaps similar to https://bugreports.qt.io/browse/QTBUG-49623 ? - QSignalSpy dbMergeSpy(m_tabWidget->currentWidget(), SIGNAL(databaseMerged(Database*))); + // It is safe to ignore the warning this line produces + QSignalSpy dbMergeSpy(m_dbWidget, SIGNAL(databaseMerged(Database*))); // set file to merge from fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); @@ -140,6 +140,74 @@ void TestGui::testMergeDatabase() QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); } +void TestGui::testAutoreloadDatabase() +{ + config()->set("AutoReloadOnChange", false); + + // Load the MergeDatabase.kdbx file into temporary storage + QByteArray tmpData; + QFile mergeDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); + QVERIFY(mergeDbFile.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&mergeDbFile, tmpData)); + mergeDbFile.close(); + + // Test accepting new file in autoreload + MessageBox::setNextAnswer(QMessageBox::Yes); + // Overwrite the current database with the temp data + QVERIFY(m_dbFile.open()); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + // the General group contains one entry from the new db data + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); + QVERIFY(! m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); + + // Reset the state + cleanup(); + init(); + + // Test rejecting new file in autoreload + MessageBox::setNextAnswer(QMessageBox::No); + // Overwrite the current temp database with a new file + m_dbFile.open(); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + // Ensure the merge did not take place + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 0); + QVERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); + + // Reset the state + cleanup(); + init(); + + // Test accepting a merge of edits into autoreload + // Turn on autoload so we only get one messagebox (for the merge) + config()->set("AutoReloadOnChange", true); + + // Modify some entries + testEditEntry(); + + // This is saying yes to merging the entries + MessageBox::setNextAnswer(QMessageBox::Yes); + // Overwrite the current database with the temp data + QVERIFY(m_dbFile.open()); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); + QVERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); +} + void TestGui::testTabs() { QCOMPARE(m_tabWidget->count(), 1); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index ff9a775e5..ab5bf5f2f 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -39,6 +39,7 @@ private Q_SLOTS: void cleanupTestCase(); void testMergeDatabase(); + void testAutoreloadDatabase(); void testTabs(); void testEditEntry(); void testAddEntry(); @@ -65,6 +66,7 @@ private: MainWindow* m_mainWindow; DatabaseTabWidget* m_tabWidget; DatabaseWidget* m_dbWidget; + QByteArray m_dbData; QTemporaryFile m_dbFile; QString m_dbFileName; Database* m_db;