Added a quick integrity check during startup, fixes #679
This commit is contained in:
@@ -11,7 +11,11 @@ ffmpeg-file = "/usr/bin/ffmpeg";
|
|||||||
log-file = "";
|
log-file = "";
|
||||||
access-log-file = "";
|
access-log-file = "";
|
||||||
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
|
# 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";
|
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
|
# Output db queries on stdout
|
||||||
db-show-queries = false;
|
db-show-queries = false;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
||||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||||
|
|
||||||
@@ -70,21 +73,98 @@ namespace lms::db
|
|||||||
|
|
||||||
std::filesystem::path _dbPath;
|
std::filesystem::path _dbPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class IntegrityCheckType
|
||||||
|
{
|
||||||
|
Quick,
|
||||||
|
Full
|
||||||
|
};
|
||||||
|
bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function<void(std::string_view error)> 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<void(std::string_view table, long long rowId, std::string_view referredTable)> 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<int>(table.capacity()));
|
||||||
|
statement->getResult(1, &rowId);
|
||||||
|
statement->getResult(2, &foreignTable, static_cast<int>(foreignTable.capacity()));
|
||||||
|
|
||||||
|
errorCallback(table, rowId, foreignTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foreignKeyConstraintsPassed;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Session living class handling the database and the login
|
// Session living class handling the database and the login
|
||||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
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);
|
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath);
|
||||||
|
|
||||||
auto connection{ std::make_unique<Connection>(dbPath) };
|
auto connection{ std::make_unique<Connection>(dbPath) };
|
||||||
if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU
|
if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU
|
||||||
|
{
|
||||||
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
|
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
|
||||||
|
checkType = config->getString("db-integrity-check", "quick");
|
||||||
|
}
|
||||||
|
|
||||||
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
||||||
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||||
|
|
||||||
_connectionPool = std::move(connectionPool);
|
_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)
|
void Db::executeSql(const std::string& sql)
|
||||||
@@ -114,6 +194,55 @@ namespace lms::db
|
|||||||
return *tlsSession;
|
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)
|
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
|
||||||
: _connectionPool{ pool }
|
: _connectionPool{ pool }
|
||||||
, _connection{ _connectionPool.getConnection() }
|
, _connection{ _connectionPool.getConnection() }
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ namespace lms::db
|
|||||||
core::RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
core::RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
||||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||||
|
|
||||||
|
void performQuickCheck();
|
||||||
|
void performIntegrityCheck();
|
||||||
|
void performForeignKeyConstraintsCheck();
|
||||||
|
|
||||||
class ScopedConnection
|
class ScopedConnection
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -53,6 +57,7 @@ namespace lms::db
|
|||||||
~ScopedConnection();
|
~ScopedConnection();
|
||||||
|
|
||||||
Wt::Dbo::SqlConnection* operator->() const;
|
Wt::Dbo::SqlConnection* operator->() const;
|
||||||
|
Wt::Dbo::SqlConnection& operator*() const { return *_connection; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ScopedConnection(const ScopedConnection&) = delete;
|
ScopedConnection(const ScopedConnection&) = delete;
|
||||||
|
|||||||
Reference in New Issue
Block a user