From 992073d43d6a7d4e3de3141f7934e42b674e7f1d Mon Sep 17 00:00:00 2001 From: slava Date: Sat, 30 May 2026 12:29:23 +0500 Subject: [PATCH] Add support for database wipe on exit, including delayed deletion handling and user-triggered wipe option in GUI. Update `DatabaseManager` with sentinel logic and file path utilities. --- database/DatabaseManager.cpp | 30 +++++++++++++++++++++++++----- database/DatabaseManager.h | 13 +++++++++++++ main.cpp | 25 ++++++++++++++++++++++++- views/MainWindow.cpp | 16 ++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index a1d88f9..c4a2e50 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -33,6 +33,30 @@ DatabaseManager &DatabaseManager::instance() { return instance; } +QString DatabaseManager::dbFilePath() { + const QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) { + dbPath.replace(0, 1, QDir::homePath()); + } + return dbPath; +} + +void DatabaseManager::wipeDatabaseFiles() { + const QString dbPath = dbFilePath(); + QFile::remove(dbPath); + QFile::remove(dbPath + "-wal"); + QFile::remove(dbPath + "-shm"); + QFile::remove(dbPath + ".wipe"); +} + +void DatabaseManager::writeWipeSentinel() { + QFile marker(dbFilePath() + ".wipe"); + if (marker.open(QIODevice::WriteOnly)) { + marker.close(); + } +} + QSqlDatabase DatabaseManager::database() { QMutexLocker locker(&mutex); QString name = connectionName(); @@ -40,12 +64,8 @@ QSqlDatabase DatabaseManager::database() { // Создаем новое соединение, если его ещё нет if (!connectionPool.contains(name)) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", name); - const QSettings settings("config.ini", QSettings::IniFormat); - QString dbPath = settings.value("db/dbPath").toString(); - if (dbPath.startsWith("~")) { - dbPath.replace(0, 1, QDir::homePath()); - } + const QString dbPath = dbFilePath(); // Создаём директорию для БД, если её нет QDir().mkpath(QFileInfo(dbPath).absolutePath()); diff --git a/database/DatabaseManager.h b/database/DatabaseManager.h index ea809d6..2ea86d9 100644 --- a/database/DatabaseManager.h +++ b/database/DatabaseManager.h @@ -5,6 +5,7 @@ #include #include #include +#include class DatabaseManager { public: @@ -15,6 +16,17 @@ public: void cleanupOldData(int days = 5); void closeConnection(); + // Путь к файлу БД из config.ini (с раскрытием ~). Единая точка вычисления. + static QString dbFilePath(); + // Удалить файл БД вместе с WAL/SHM-сайдкарами и sentinel-файлом. + static void wipeDatabaseFiles(); + // Создать sentinel .wipe — отложенное удаление БД при следующем старте. + static void writeWipeSentinel(); + + // Флаг «удалить БД при выходе», выставляется из GUI-потока. + void setWipeOnExit(bool wipe) { m_wipeOnExit.store(wipe); } + bool wipeOnExit() const { return m_wipeOnExit.load(); } + private: DatabaseManager(); ~DatabaseManager(); @@ -24,4 +36,5 @@ private: QRecursiveMutex mutex; QSet activeConnections; QMap connectionPool; + std::atomic m_wipeOnExit{false}; }; \ No newline at end of file diff --git a/main.cpp b/main.cpp index dfc9ea2..2c9eaf1 100644 --- a/main.cpp +++ b/main.cpp @@ -350,6 +350,13 @@ int main(int argc, char *argv[]) { QDir().mkpath(QFileInfo(dbPath).absolutePath()); } + // Отложенное удаление БД (если файл был занят при прошлом выходе, напр. на Windows). + // Делаем здесь, до открытия любого соединения. + if (QFile::exists(DatabaseManager::dbFilePath() + ".wipe")) { + DatabaseManager::wipeDatabaseFiles(); + qInfo() << "[main] Локальная БД удалена по запросу пользователя"; + } + // Миграции БД DatabaseManager::instance().initSchema(); DatabaseManager::instance().cleanupOldData(30); @@ -444,5 +451,21 @@ int main(int argc, char *argv[]) { loader->close(); }, Qt::QueuedConnection); } - return QApplication::exec(); + const int rc = QApplication::exec(); + + // Пользователь выбрал «Выйти» с удалением БД. Потоки уже остановлены через + // aboutToQuit, их соединения сняты. Пробуем удалить файл сразу; если он занят + // (Windows: незавершившийся поток) — оставляем sentinel, удалим при старте. + if (DatabaseManager::instance().wipeOnExit()) { + dbReady.store(false); // прекращаем дозапись логов в БД + DatabaseManager::instance().closeConnection(); + if (QFile::remove(DatabaseManager::dbFilePath())) { + DatabaseManager::wipeDatabaseFiles(); // подчистить -wal/-shm/sentinel + qInfo() << "[main] Локальная БД удалена при выходе"; + } else { + DatabaseManager::writeWipeSentinel(); + qInfo() << "[main] Файл БД занят — удаление отложено до следующего старта"; + } + } + return rc; } diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index d20dba8..0bba6f2 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -22,6 +22,7 @@ #include "db/DeviceInfo.h" #include "device/DeviceWidget.h" #include "dao/BankProfileDAO.h" +#include "DatabaseManager.h" #include "dao/DeviceDAO.h" #include "dao/MaterialDAO.h" #include "dao/SettingsDAO.h" @@ -63,11 +64,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *tutorialPageAction = new QAction("Инструкция", this); auto *eventAction = new QAction("События", this); auto *fileLogAction = new QAction("Логи", this); + auto *exitAction = new QAction("Выйти", this); menu->addAction(devicesPageAction); // menu->addAction(accountPageAction); menu->addAction(eventAction); menu->addAction(tutorialPageAction); menu->addAction(fileLogAction); + menu->addAction(exitAction); connect(devicesPageAction, &QAction::triggered, this, [=]() { if (!m_devicesWindow) { @@ -114,6 +117,19 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { } }); + connect(exitAction, &QAction::triggered, this, [this]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle("Выход"); + msgBox.setText("Удалить локальную базу данных и выйти из приложения?"); + auto *yesBtn = msgBox.addButton("Выйти", QMessageBox::YesRole); + msgBox.addButton("Отмена", QMessageBox::NoRole); + msgBox.exec(); + if (msgBox.clickedButton() == yesBtn) { + DatabaseManager::instance().setWipeOnExit(true); + qApp->quit(); // штатное завершение фоновых потоков (aboutToQuit) + } + }); + if (SettingsDAO::get("tutorial_shown").isEmpty()) { QTimer::singleShot(0, this, [this]() { m_tutorialWindow = new TutorialWindow(this);