From 6bfc769cab731306012b86e8d962e255e199a9dd Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 22 Jun 2025 22:25:24 +0200 Subject: [PATCH] Added a quick integrity check during startup, fixes #679 --- conf/lms.conf | 4 + src/libs/database/impl/Db.cpp | 129 ++++++++++++++++++++++ src/libs/database/include/database/Db.hpp | 5 + 3 files changed, 138 insertions(+) diff --git a/conf/lms.conf b/conf/lms.conf index eaa1921f..1c2dfa12 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -11,7 +11,11 @@ ffmpeg-file = "/usr/bin/ffmpeg"; log-file = ""; access-log-file = ""; # Minimum severity, can be "debug", "info", "warning", "error" or "fatal" +# "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application log-min-severity = "info"; +# Database consistency check to run at startup. +# Can be "none", "quick", or "full" +db-integrity-check = "quick"; # Output db queries on stdout db-show-queries = false; diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index ff696b7a..0bb7e46a 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -19,6 +19,9 @@ #include "database/Db.hpp" +#include +#include + #include #include @@ -70,21 +73,98 @@ namespace lms::db std::filesystem::path _dbPath; }; + + enum class IntegrityCheckType + { + Quick, + Full + }; + bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function errorCallback) + { + bool integrityCheckPassed{}; + + auto statement = connection.prepareStatement(checkType == IntegrityCheckType::Full ? "PRAGMA integrity_check" : "PRAGMA quick_check"); + statement->execute(); + + std::string result; + result.reserve(32); + while (statement->nextRow()) + { + result.clear(); + statement->getResult(0, &result, result.capacity()); + + if (result == "ok") + { + integrityCheckPassed = true; + break; + } + + errorCallback(result); + } + + return integrityCheckPassed; + } + + bool checkDbForeignKeyConstraints(Wt::Dbo::SqlConnection& connection, std::function errorCallback) + { + bool foreignKeyConstraintsPassed{ true }; + + auto statement = connection.prepareStatement("PRAGMA foreign_key_check"); + statement->execute(); + + std::string table; + std::string foreignTable; + // see https://www.sqlite.org/pragma.html#pragma_foreign_key_check for exepcted result + while (statement->nextRow()) + { + foreignKeyConstraintsPassed = false; + + table.clear(); + foreignTable.clear(); + long long rowId{}; + + statement->getResult(0, &table, static_cast(table.capacity())); + statement->getResult(1, &rowId); + statement->getResult(2, &foreignTable, static_cast(foreignTable.capacity())); + + errorCallback(table, rowId, foreignTable); + } + + return foreignKeyConstraintsPassed; + } } // namespace // Session living class handling the database and the login Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount) { + std::string checkType{ "quick" }; LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath); auto connection{ std::make_unique(dbPath) }; if (core::IConfig * config{ core::Service::get() }) // may not be here on testU + { connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false"); + checkType = config->getString("db-integrity-check", "quick"); + } auto connectionPool{ std::make_unique(std::move(connection), connectionCount) }; connectionPool->setTimeout(std::chrono::seconds{ 10 }); _connectionPool = std::move(connectionPool); + + if (checkType == "quick") + { + performQuickCheck(); + } + else if (checkType == "full") + { + performIntegrityCheck(); + performForeignKeyConstraintsCheck(); + } + else if (checkType != "none") + { + throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'."); + } } void Db::executeSql(const std::string& sql) @@ -114,6 +194,55 @@ namespace lms::db return *tlsSession; } + void Db::performQuickCheck() + { + ScopedConnection connection{ *_connectionPool }; + + LMS_LOG(DB, INFO, "Performing quick database check..."); + + // Quick check is just a simple integrity check + bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) { + LMS_LOG(DB, ERROR, "Quick check error: " << error); + }) }; + + if (quickCheckPassed) + LMS_LOG(DB, INFO, "Quick database check passed!"); + else + LMS_LOG(DB, ERROR, "Quick database check done with errors!"); + } + + void Db::performIntegrityCheck() + { + ScopedConnection connection{ *_connectionPool }; + + LMS_LOG(DB, INFO, "Checking database integrity..."); + + bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) { + LMS_LOG(DB, ERROR, "Integrity check error: " << error); + }) }; + + if (integrityCheckPassed) + LMS_LOG(DB, INFO, "Database integrity check passed!"); + else + LMS_LOG(DB, ERROR, "Database integrity check done with errors!"); + } + + void Db::performForeignKeyConstraintsCheck() + { + ScopedConnection connection{ *_connectionPool }; + + LMS_LOG(DB, INFO, "Checking foreign key constraints..."); + + bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) { + LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'"); + }) }; + + if (!foreignKeyConstraintsPassed) + throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database."); + + LMS_LOG(DB, INFO, "Foreign key constraints check passed!"); + } + Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool) : _connectionPool{ pool } , _connection{ _connectionPool.getConnection() } diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/database/include/database/Db.hpp index a90f319a..352bdf03 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/database/include/database/Db.hpp @@ -46,6 +46,10 @@ namespace lms::db core::RecursiveSharedMutex& getMutex() { return _sharedMutex; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } + void performQuickCheck(); + void performIntegrityCheck(); + void performForeignKeyConstraintsCheck(); + class ScopedConnection { public: @@ -53,6 +57,7 @@ namespace lms::db ~ScopedConnection(); Wt::Dbo::SqlConnection* operator->() const; + Wt::Dbo::SqlConnection& operator*() const { return *_connection; } private: ScopedConnection(const ScopedConnection&) = delete;