550 lines
19 KiB
C++
550 lines
19 KiB
C++
#include "core/databasefactory.h"
|
|
|
|
#include "core/defs.h"
|
|
#include "core/settings.h"
|
|
|
|
#include <QApplication>
|
|
#include <QDir>
|
|
#include <QSqlQuery>
|
|
#include <QSqlError>
|
|
#include <QVariant>
|
|
|
|
|
|
QPointer<DatabaseFactory> DatabaseFactory::s_instance;
|
|
|
|
DatabaseFactory::DatabaseFactory(QObject *parent)
|
|
: QObject(parent),
|
|
m_mysqlDatabaseInitialized(false),
|
|
m_sqliteFileBasedDatabaseinitialized(false),
|
|
m_sqliteInMemoryDatabaseInitialized(false) {
|
|
setObjectName("DatabaseFactory");
|
|
determineDriver();
|
|
}
|
|
|
|
DatabaseFactory::~DatabaseFactory() {
|
|
qDebug("Destroying DatabaseFactory object.");
|
|
}
|
|
|
|
DatabaseFactory *DatabaseFactory::instance() {
|
|
if (s_instance.isNull()) {
|
|
s_instance = new DatabaseFactory(qApp);
|
|
}
|
|
|
|
return s_instance;
|
|
}
|
|
|
|
DatabaseFactory::MySQLError DatabaseFactory::mysqlTestConnection(const QString &hostname, int port,
|
|
const QString &username, const QString &password) {
|
|
QSqlDatabase database = QSqlDatabase::addDatabase(APP_DB_DRIVER_MYSQL,
|
|
APP_DB_TEST_MYSQL);
|
|
|
|
database.setHostName(hostname);
|
|
database.setPort(port);
|
|
database.setUserName(username);
|
|
database.setPassword(password);
|
|
|
|
if (database.open()) {
|
|
// Connection succeeded, clean up the mess and return 0.
|
|
database.close();
|
|
removeConnection(APP_DB_TEST_MYSQL);
|
|
return MySQLOk;
|
|
}
|
|
else {
|
|
// Connection failed, do cleanup and return specific
|
|
// error code.
|
|
MySQLError error_code = static_cast<MySQLError>(database.lastError().number());
|
|
|
|
removeConnection(APP_DB_TEST_MYSQL);
|
|
return error_code;
|
|
}
|
|
}
|
|
|
|
QString DatabaseFactory::mysqlInterpretErrorCode(MySQLError error_code) {
|
|
switch (error_code) {
|
|
case MySQLOk:
|
|
return QObject::tr("MySQL server works as expected.");
|
|
|
|
case MySQLCantConnect:
|
|
case MySQLConnectionError:
|
|
case MySQLUnknownHost:
|
|
return QObject::tr("No MySQL server is running in the target destination.");
|
|
|
|
case MySQLAccessDenied:
|
|
return QObject::tr("Access denied. Invalid username or password used.");
|
|
|
|
default:
|
|
return QObject::tr("Unknown error.");
|
|
}
|
|
}
|
|
|
|
void DatabaseFactory::sqliteAssemblyDatabaseFilePath() {
|
|
if (Settings::instance()->type() == Settings::Portable) {
|
|
m_sqliteDatabaseFilePath = qApp->applicationDirPath() +
|
|
QDir::separator() +
|
|
QString(APP_DB_PATH);
|
|
}
|
|
else {
|
|
m_sqliteDatabaseFilePath = QDir::homePath() + QDir::separator() +
|
|
QString(APP_LOW_H_NAME) + QDir::separator() +
|
|
QString(APP_DB_PATH);
|
|
}
|
|
}
|
|
|
|
QSqlDatabase DatabaseFactory::sqliteInitializeInMemoryDatabase() {
|
|
QSqlDatabase database = QSqlDatabase::addDatabase(APP_DB_DRIVER_SQLITE);
|
|
|
|
database.setDatabaseName(":memory:");
|
|
|
|
if (!database.open()) {
|
|
qFatal("In-memory SQLite database was NOT opened. Delivered error message: '%s'",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
QSqlQuery query_db(database);
|
|
|
|
query_db.setForwardOnly(true);
|
|
query_db.exec("PRAGMA encoding = \"UTF-8\"");
|
|
query_db.exec("PRAGMA synchronous = OFF");
|
|
query_db.exec("PRAGMA journal_mode = MEMORY");
|
|
query_db.exec("PRAGMA page_size = 4096");
|
|
query_db.exec("PRAGMA cache_size = 16384");
|
|
query_db.exec("PRAGMA count_changes = OFF");
|
|
query_db.exec("PRAGMA temp_store = MEMORY");
|
|
|
|
// Sample query which checks for existence of tables.
|
|
query_db.exec("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'");
|
|
|
|
if (query_db.lastError().isValid()) {
|
|
qWarning("Error occurred. In-memory SQLite database is not initialized. Initializing now.");
|
|
|
|
QFile file_init(APP_MISC_PATH + QDir::separator() + APP_DB_INIT_SQLITE_MEMORY);
|
|
|
|
if (!file_init.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
// Database initialization file not opened. HUGE problem.
|
|
qFatal("In-memory SQLite database initialization file '%s' from directory '%s' was not found. In-memory database is uninitialized.",
|
|
APP_DB_INIT_SQLITE,
|
|
qPrintable(APP_MISC_PATH));
|
|
}
|
|
|
|
QStringList statements = QString(file_init.readAll()).split(APP_DB_INIT_SPLIT,
|
|
QString::SkipEmptyParts);
|
|
database.transaction();
|
|
|
|
foreach(const QString &statement, statements) {
|
|
query_db.exec(statement);
|
|
|
|
if (query_db.lastError().isValid()) {
|
|
qFatal("In-memory SQLite database initialization failed. Initialization script '%s' is not correct.",
|
|
APP_DB_INIT_SQLITE);
|
|
}
|
|
}
|
|
|
|
database.commit();
|
|
qDebug("In-memory SQLite database backend should be ready now.");
|
|
}
|
|
else {
|
|
query_db.next();
|
|
|
|
qDebug("In-memory SQLite database connection seems to be established.");
|
|
qDebug("In-memory SQLite database has version '%s'.", qPrintable(query_db.value(0).toString()));
|
|
}
|
|
|
|
// Loading messages from file-based database.
|
|
QSqlDatabase file_database = sqliteConnection(objectName(), StrictlyFileBased);
|
|
QSqlQuery copy_contents(database);
|
|
|
|
// Attach database.
|
|
copy_contents.exec(QString("ATTACH DATABASE '%1' AS 'storage';").arg(file_database.databaseName()));
|
|
|
|
// Copy all stuff.
|
|
QStringList tables; tables << "Information" << "Categories" <<
|
|
"Feeds" << "FeedsData" <<
|
|
"Messages";
|
|
|
|
foreach (const QString &table, tables) {
|
|
copy_contents.exec(QString("INSERT INTO main.%1 SELECT * FROM storage.%1;").arg(table));
|
|
}
|
|
|
|
// Detach database and finish.
|
|
copy_contents.exec("DETACH 'storage'");
|
|
copy_contents.finish();
|
|
|
|
query_db.finish();
|
|
}
|
|
|
|
// Everything is initialized now.
|
|
m_sqliteInMemoryDatabaseInitialized = true;
|
|
|
|
return database;
|
|
}
|
|
|
|
QSqlDatabase DatabaseFactory::sqliteInitializeFileBasedDatabase(const QString &connection_name) {
|
|
// Prepare file paths.
|
|
QDir db_path(m_sqliteDatabaseFilePath);
|
|
QFile db_file(db_path.absoluteFilePath(APP_DB_FILE));
|
|
|
|
// Check if database directory exists.
|
|
if (!db_path.exists()) {
|
|
if (!db_path.mkpath(db_path.absolutePath())) {
|
|
// Failure when create database file path.
|
|
qFatal("Directory '%s' for SQLite database file '%s' was NOT created."
|
|
"This is HUGE problem.",
|
|
qPrintable(db_path.absolutePath()),
|
|
qPrintable(db_file.symLinkTarget()));
|
|
}
|
|
}
|
|
|
|
// Folders are created. Create new QSQLDatabase object.
|
|
QSqlDatabase database;
|
|
|
|
database = QSqlDatabase::addDatabase(APP_DB_DRIVER_SQLITE,
|
|
connection_name);
|
|
database.setDatabaseName(db_file.fileName());
|
|
|
|
if (!database.open()) {
|
|
qFatal("File-based SQLite database was NOT opened. Delivered error message: '%s'",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
QSqlQuery query_db(database);
|
|
|
|
query_db.setForwardOnly(true);
|
|
query_db.exec("PRAGMA encoding = \"UTF-8\"");
|
|
query_db.exec("PRAGMA synchronous = OFF");
|
|
query_db.exec("PRAGMA journal_mode = MEMORY");
|
|
query_db.exec("PRAGMA page_size = 4096");
|
|
query_db.exec("PRAGMA cache_size = 16384");
|
|
query_db.exec("PRAGMA count_changes = OFF");
|
|
query_db.exec("PRAGMA temp_store = MEMORY");
|
|
|
|
// Sample query which checks for existence of tables.
|
|
query_db.exec("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'");
|
|
|
|
if (query_db.lastError().isValid()) {
|
|
qWarning("Error occurred. File-based SQLite database is not initialized. Initializing now.");
|
|
|
|
QFile file_init(APP_MISC_PATH + QDir::separator() + APP_DB_INIT_SQLITE);
|
|
|
|
if (!file_init.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
// Database initialization file not opened. HUGE problem.
|
|
qFatal("SQLite database initialization file '%s' from directory '%s' was not found. File-based database is uninitialized.",
|
|
APP_DB_INIT_SQLITE,
|
|
qPrintable(APP_MISC_PATH));
|
|
}
|
|
|
|
QStringList statements = QString(file_init.readAll()).split(APP_DB_INIT_SPLIT,
|
|
QString::SkipEmptyParts);
|
|
database.transaction();
|
|
|
|
foreach(const QString &statement, statements) {
|
|
query_db.exec(statement);
|
|
|
|
if (query_db.lastError().isValid()) {
|
|
qFatal("File-based SQLite database initialization failed. Initialization script '%s' is not correct.",
|
|
APP_DB_INIT_SQLITE);
|
|
}
|
|
}
|
|
|
|
database.commit();
|
|
qDebug("File-based SQLite database backend should be ready now.");
|
|
}
|
|
else {
|
|
query_db.next();
|
|
|
|
qDebug("File-based SQLite database connection '%s' to file '%s' seems to be established.",
|
|
qPrintable(connection_name),
|
|
qPrintable(QDir::toNativeSeparators(database.databaseName())));
|
|
qDebug("File-based SQLite database has version '%s'.", qPrintable(query_db.value(0).toString()));
|
|
}
|
|
|
|
query_db.finish();
|
|
}
|
|
|
|
// Everything is initialized now.
|
|
m_sqliteFileBasedDatabaseinitialized = true;
|
|
|
|
return database;
|
|
}
|
|
|
|
|
|
QSqlDatabase DatabaseFactory::connection(const QString &connection_name,
|
|
DesiredType desired_type) {
|
|
switch (m_activeDatabaseDriver) {
|
|
case MYSQL:
|
|
return mysqlConnection(connection_name);
|
|
|
|
case SQLITE:
|
|
case SQLITE_MEMORY:
|
|
default:
|
|
return sqliteConnection(connection_name, desired_type);
|
|
}
|
|
}
|
|
|
|
void DatabaseFactory::removeConnection(const QString &connection_name) {
|
|
qDebug("Removing database connection '%s'.", qPrintable(connection_name));
|
|
|
|
QSqlDatabase::removeDatabase(connection_name);
|
|
}
|
|
|
|
void DatabaseFactory::sqliteSaveMemoryDatabase() {
|
|
qDebug("Saving in-memory working database back to persistent file-based storage.");
|
|
|
|
QSqlDatabase database = sqliteConnection(objectName(), StrictlyInMemory);
|
|
QSqlDatabase file_database = sqliteConnection(objectName(), StrictlyFileBased);
|
|
QSqlQuery copy_contents(database);
|
|
|
|
// Attach database.
|
|
copy_contents.exec(QString("ATTACH DATABASE '%1' AS 'storage';").arg(file_database.databaseName()));
|
|
|
|
// Copy all stuff.
|
|
QStringList tables; tables << "Categories" << "Feeds" << "FeedsData" <<
|
|
"Messages";
|
|
|
|
foreach (const QString &table, tables) {
|
|
copy_contents.exec(QString("DELETE FROM storage.%1;").arg(table));
|
|
copy_contents.exec(QString("INSERT INTO storage.%1 SELECT * FROM main.%1;").arg(table));
|
|
}
|
|
|
|
// Detach database and finish.
|
|
copy_contents.exec("DETACH 'storage'");
|
|
copy_contents.finish();
|
|
}
|
|
|
|
void DatabaseFactory::determineDriver() {
|
|
QString db_driver = Settings::instance()->value(APP_CFG_DB, "database_driver", APP_DB_DRIVER_SQLITE).toString();
|
|
|
|
if (db_driver == APP_DB_DRIVER_MYSQL && QSqlDatabase::isDriverAvailable(APP_DB_DRIVER_MYSQL)) {
|
|
// User wants to use MySQL and MySQL is actually available. Use it.
|
|
m_activeDatabaseDriver = MYSQL;
|
|
|
|
qDebug("Working database source was as MySQL database.");
|
|
}
|
|
else {
|
|
// User wants to use SQLite, which is always available. Check if file-based
|
|
// or in-memory database will be used.
|
|
if (Settings::instance()->value(APP_CFG_DB, "use_in_memory_db", false).toBool()) {
|
|
// Use in-memory SQLite database.
|
|
m_activeDatabaseDriver = SQLITE_MEMORY;
|
|
|
|
qDebug("Working database source was determined as SQLite in-memory database.");
|
|
}
|
|
else {
|
|
// Use strictly file-base SQLite database.
|
|
m_activeDatabaseDriver = SQLITE;
|
|
|
|
qDebug("Working database source was determined as SQLite file-based database.");
|
|
}
|
|
|
|
sqliteAssemblyDatabaseFilePath();
|
|
}
|
|
}
|
|
|
|
QSqlDatabase DatabaseFactory::mysqlConnection(const QString &connection_name) {
|
|
if (!m_mysqlDatabaseInitialized) {
|
|
// Return initialized database.
|
|
return mysqlInitializeDatabase(connection_name);
|
|
}
|
|
else {
|
|
QSqlDatabase database;
|
|
|
|
if (QSqlDatabase::contains(connection_name)) {
|
|
qDebug("MySQL connection '%s' is already active.",
|
|
qPrintable(connection_name));
|
|
|
|
// This database connection was added previously, no need to
|
|
// setup its properties.
|
|
database = QSqlDatabase::database(connection_name);
|
|
}
|
|
else {
|
|
// Database connection with this name does not exist
|
|
// yet, add it and set it up.
|
|
database = QSqlDatabase::addDatabase(APP_DB_DRIVER_MYSQL, connection_name);
|
|
|
|
database.setHostName(Settings::instance()->value(APP_CFG_DB, "mysql_hostname").toString());
|
|
database.setPort(Settings::instance()->value(APP_CFG_DB, "mysql_port", APP_DB_MYSQL_PORT).toInt());
|
|
database.setUserName(Settings::instance()->value(APP_CFG_DB, "mysql_username").toString());
|
|
database.setPassword(Settings::instance()->value(APP_CFG_DB, "mysql_password").toString());
|
|
database.setDatabaseName(APP_LOW_NAME);
|
|
}
|
|
|
|
if (!database.isOpen() && !database.open()) {
|
|
qFatal("MySQL database was NOT opened. Delivered error message: '%s'.",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
qDebug("MySQL database connection '%s' to file '%s' seems to be established.",
|
|
qPrintable(connection_name),
|
|
qPrintable(QDir::toNativeSeparators(database.databaseName())));
|
|
}
|
|
|
|
return database;
|
|
}
|
|
}
|
|
|
|
QSqlDatabase DatabaseFactory::mysqlInitializeDatabase(const QString &connection_name) {
|
|
// Folders are created. Create new QSQLDatabase object.
|
|
QSqlDatabase database = QSqlDatabase::addDatabase(APP_DB_DRIVER_MYSQL,
|
|
connection_name);
|
|
|
|
database.setHostName(Settings::instance()->value(APP_CFG_DB, "mysql_hostname").toString());
|
|
database.setPort(Settings::instance()->value(APP_CFG_DB, "mysql_port", APP_DB_MYSQL_PORT).toInt());
|
|
database.setUserName(Settings::instance()->value(APP_CFG_DB, "mysql_username").toString());
|
|
database.setPassword(Settings::instance()->value(APP_CFG_DB, "mysql_password").toString());
|
|
|
|
if (!database.open()) {
|
|
qFatal("MySQL database was NOT opened. Delivered error message: '%s'",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
QSqlQuery query_db(database);
|
|
|
|
query_db.setForwardOnly(true);
|
|
|
|
if (!query_db.exec("USE rssguard") ||
|
|
!query_db.exec("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'")) {
|
|
// If no "rssguard" database exists
|
|
// or schema version is wrong, then initialize it.
|
|
qWarning("Error occurred. MySQL database is not initialized. Initializing now.");
|
|
|
|
QFile file_init(APP_MISC_PATH + QDir::separator() + APP_DB_INIT_MYSQL);
|
|
|
|
if (!file_init.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
// Database initialization file not opened. HUGE problem.
|
|
qFatal("MySQL database initialization file '%s' from directory '%s' was not found. File-based database is uninitialized.",
|
|
APP_DB_INIT_MYSQL,
|
|
qPrintable(APP_MISC_PATH));
|
|
}
|
|
|
|
QStringList statements = QString(file_init.readAll()).split(APP_DB_INIT_SPLIT,
|
|
QString::SkipEmptyParts);
|
|
database.transaction();
|
|
|
|
foreach(const QString &statement, statements) {
|
|
query_db.exec(statement);
|
|
|
|
if (query_db.lastError().isValid()) {
|
|
qFatal("MySQL database initialization failed. Initialization script '%s' is not correct.",
|
|
APP_DB_INIT_MYSQL);
|
|
}
|
|
}
|
|
|
|
database.commit();
|
|
qDebug("MySQL database backend should be ready now.");
|
|
}
|
|
else {
|
|
query_db.next();
|
|
|
|
qDebug("MySQL database connection '%s' seems to be established.",
|
|
qPrintable(connection_name));
|
|
qDebug("MySQL database has version '%s'.", qPrintable(query_db.value(0).toString()));
|
|
}
|
|
|
|
query_db.finish();
|
|
}
|
|
|
|
// Everything is initialized now.
|
|
m_mysqlDatabaseInitialized = true;
|
|
|
|
return database;
|
|
}
|
|
|
|
QSqlDatabase DatabaseFactory::sqliteConnection(const QString &connection_name,
|
|
DatabaseFactory::DesiredType desired_type) {
|
|
if (desired_type == DatabaseFactory::StrictlyInMemory ||
|
|
(desired_type == DatabaseFactory::FromSettings && m_activeDatabaseDriver == SQLITE_MEMORY)) {
|
|
// We request in-memory database (either user explicitly
|
|
// needs in-memory database or it was enabled in the settings).
|
|
if (!m_sqliteInMemoryDatabaseInitialized) {
|
|
// It is not initialized yet.
|
|
return sqliteInitializeInMemoryDatabase();
|
|
}
|
|
else {
|
|
QSqlDatabase database = QSqlDatabase::database();
|
|
|
|
database.setDatabaseName(":memory:");
|
|
|
|
if (!database.isOpen() && !database.open()) {
|
|
qFatal("In-memory SQLite database was NOT opened. Delivered error message: '%s'.",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
qDebug("In-memory SQLite database connection seems to be established.");
|
|
}
|
|
|
|
return database;
|
|
}
|
|
}
|
|
else {
|
|
// We request file-based database.
|
|
if (!m_sqliteFileBasedDatabaseinitialized) {
|
|
// File-based database is not yet initialised.
|
|
return sqliteInitializeFileBasedDatabase(connection_name);
|
|
}
|
|
else {
|
|
QSqlDatabase database;
|
|
|
|
if (QSqlDatabase::contains(connection_name)) {
|
|
qDebug("SQLite connection '%s' is already active.",
|
|
qPrintable(connection_name));
|
|
|
|
// This database connection was added previously, no need to
|
|
// setup its properties.
|
|
database = QSqlDatabase::database(connection_name);
|
|
}
|
|
else {
|
|
// Database connection with this name does not exist
|
|
// yet, add it and set it up.
|
|
database = QSqlDatabase::addDatabase(APP_DB_DRIVER_SQLITE, connection_name);
|
|
|
|
QDir db_path(m_sqliteDatabaseFilePath);
|
|
QFile db_file(db_path.absoluteFilePath(APP_DB_FILE));
|
|
|
|
// Setup database file path.
|
|
database.setDatabaseName(db_file.fileName());
|
|
}
|
|
|
|
if (!database.isOpen() && !database.open()) {
|
|
qFatal("File-based SQLite database was NOT opened. Delivered error message: '%s'.",
|
|
qPrintable(database.lastError().text()));
|
|
}
|
|
else {
|
|
qDebug("File-based SQLite database connection '%s' to file '%s' seems to be established.",
|
|
qPrintable(connection_name),
|
|
qPrintable(QDir::toNativeSeparators(database.databaseName())));
|
|
}
|
|
|
|
return database;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DatabaseFactory::sqliteVacuumDatabase() {
|
|
QSqlDatabase database = sqliteConnection(objectName(), FromSettings);
|
|
QSqlQuery query_vacuum(database);
|
|
|
|
return query_vacuum.exec("VACUUM");
|
|
}
|
|
|
|
void DatabaseFactory::saveDatabase() {
|
|
switch (m_activeDatabaseDriver) {
|
|
case SQLITE_MEMORY:
|
|
sqliteSaveMemoryDatabase();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool DatabaseFactory::vacuumDatabase() {
|
|
switch (m_activeDatabaseDriver) {
|
|
case SQLITE_MEMORY:
|
|
case SQLITE:
|
|
return sqliteVacuumDatabase();
|
|
|
|
case MYSQL:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|