diff --git a/src/librssguard/database/databasedriver.cpp b/src/librssguard/database/databasedriver.cpp index 10a2d7404..b794615de 100755 --- a/src/librssguard/database/databasedriver.cpp +++ b/src/librssguard/database/databasedriver.cpp @@ -12,7 +12,9 @@ DatabaseDriver::DatabaseDriver(QObject* parent) : QObject(parent) {} -QStringList DatabaseDriver::prepareScript(const QString& base_sql_folder, const QString& sql_file, const QString& database_name) { +QStringList DatabaseDriver::prepareScript(const QString& base_sql_folder, + const QString& sql_file, + const QString& database_name) { QStringList statements; auto next_file = base_sql_folder + QDir::separator() + sql_file; QString sql_script = QString::fromUtf8(IOFactory::readFile(next_file)); @@ -30,11 +32,11 @@ QStringList DatabaseDriver::prepareScript(const QString& base_sql_folder, const auto included_file = base_sql_folder + QDir::separator() + included_file_name; QString included_sql_script = QString::fromUtf8(IOFactory::readFile(included_file)); - auto included_statements = sql_script.split(APP_DB_COMMENT_SPLIT, + auto included_statements = included_sql_script.split(APP_DB_COMMENT_SPLIT, #if QT_VERSION >= 0x050F00 // Qt >= 5.15.0 - Qt::SplitBehaviorFlags::SkipEmptyParts); + Qt::SplitBehaviorFlags::SkipEmptyParts); #else - QString::SplitBehavior::SkipEmptyParts); + QString::SplitBehavior::SkipEmptyParts); #endif statements << included_statements; diff --git a/src/librssguard/database/databasefactory.cpp b/src/librssguard/database/databasefactory.cpp index 53e80825a..df8c47890 100644 --- a/src/librssguard/database/databasefactory.cpp +++ b/src/librssguard/database/databasefactory.cpp @@ -3,6 +3,7 @@ #include "database/databasefactory.h" #include "3rd-party/boolinq/boolinq.h" +#include "database/mariadbdriver.h" #include "database/sqlitedriver.h" #include "gui/messagebox.h" #include "miscellaneous/application.h" @@ -19,28 +20,6 @@ DatabaseFactory::DatabaseFactory(QObject* parent) determineDriver(); } -/*else if (m_activeDatabaseDriver == DatabaseDriver::UsedDriver::MYSQL) { - QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className(), - DatabaseDriver::DesiredType::FromSettings); - QSqlQuery query(database); - - query.prepare("SELECT Round(Sum(data_length + index_length), 1) " - "FROM information_schema.tables " - "WHERE table_schema = :db " - "GROUP BY table_schema;"); - query.bindValue(QSL(":db"), database.databaseName()); - - if (query.exec() && query.next()) { - return query.value(0).value(); - } - else { - return 0; - } - } - else { - return 0; - }*/ - void DatabaseFactory::removeConnection(const QString& connection_name) { qDebugNN << LOGSEC_DB << "Removing database connection '" << connection_name << "'."; QSqlDatabase::removeDatabase(connection_name); @@ -52,7 +31,7 @@ void DatabaseFactory::determineDriver() { }; if (QSqlDatabase::isDriverAvailable(APP_DB_MYSQL_DRIVER)) { - //m_allDbDrivers.append(new MariaDbDriver(this)); + m_allDbDrivers.append(new MariaDbDriver(this)); } const QString db_driver = qApp->settings()->value(GROUP(Database), SETTING(Database::ActiveDriver)).toString(); diff --git a/src/librssguard/database/databasefactory.h b/src/librssguard/database/databasefactory.h index 3de83aab0..3c172e384 100644 --- a/src/librssguard/database/databasefactory.h +++ b/src/librssguard/database/databasefactory.h @@ -13,19 +13,6 @@ class DatabaseFactory : public QObject { Q_OBJECT public: - - // Describes possible MySQL-specific errors. - enum class MySQLError { - Ok = 0, - UnknownError = 1, - AccessDenied = 1045, - UnknownDatabase = 1049, - ConnectionError = 2002, - CantConnect = 2003, - UnknownHost = 2005 - }; - - // Constructor. explicit DatabaseFactory(QObject* parent = nullptr); // Removes connection. diff --git a/src/librssguard/database/mariadbdriver.cpp b/src/librssguard/database/mariadbdriver.cpp new file mode 100755 index 000000000..2aa955007 --- /dev/null +++ b/src/librssguard/database/mariadbdriver.cpp @@ -0,0 +1,298 @@ +// For license of this file, see /LICENSE.md. + +#include "database/mariadbdriver.h" + +#include "definitions/definitions.h" +#include "exceptions/applicationexception.h" +#include "miscellaneous/application.h" +#include "miscellaneous/settings.h" + +#include +#include +#include + +MariaDbDriver::MariaDbDriver(QObject* parent) : DatabaseDriver(parent), m_databaseInitialized(false) {} + +MariaDbDriver::MariaDbError MariaDbDriver::testConnection(const QString& hostname, int port, + const QString& w_database, const QString& username, + const QString& password) { + QSqlDatabase database = QSqlDatabase::addDatabase(APP_DB_MYSQL_DRIVER, APP_DB_MYSQL_TEST); + + database.setHostName(hostname); + database.setPort(port); + database.setUserName(username); + database.setPassword(password); + database.setDatabaseName(w_database); + + if (database.open() && !database.lastError().isValid()) { + QSqlQuery query(QSL("SELECT version();"), database); + + if (!query.lastError().isValid() && query.next()) { + qDebugNN << LOGSEC_DB + << "Checked MySQL database, version is" + << QUOTE_W_SPACE_DOT(query.value(0).toString()); + + // Connection succeeded, clean up the mess and return OK status. + database.close(); + return MariaDbError::Ok; + } + else { + database.close(); + return MariaDbError::UnknownError; + } + } + else if (database.lastError().isValid()) { + auto nat = database.lastError().nativeErrorCode(); + bool nat_converted = false; + auto nat_int = nat.toInt(&nat_converted); + + if (nat_converted) { + return static_cast(nat_int); + } + else { + qWarningNN << LOGSEC_DB + << "Failed to recognize MySQL error code:" + << QUOTE_W_SPACE_DOT(nat); + + return MariaDbError::UnknownError; + } + } + else { + return MariaDbError::UnknownError; + } +} + +QString MariaDbDriver::interpretErrorCode(MariaDbDriver::MariaDbError error_code) const { + switch (error_code) { + case MariaDbError::Ok: + return tr("MySQL server works as expected."); + + case MariaDbError::UnknownDatabase: + return tr("Selected database does not exist (yet). It will be created. It's okay."); + + case MariaDbError::CantConnect: + case MariaDbError::ConnectionError: + case MariaDbError::UnknownHost: + return tr("No MySQL server is running in the target destination."); + + case MariaDbError::AccessDenied: + return tr("Access denied. Invalid username or password used."); + + default: + return tr("Unknown error: '%1'.").arg(int(error_code)); + } +} + +QString MariaDbDriver::humanDriverType() const { + return tr("MariaDB"); +} + +QString MariaDbDriver::qtDriverCode() const { + return APP_DB_MYSQL_DRIVER; +} + +DatabaseDriver::DriverType MariaDbDriver::driverType() const { + return DatabaseDriver::DriverType::MySQL; +} + +bool MariaDbDriver::vacuumDatabase() { + QSqlDatabase database = connection(objectName()); + QSqlQuery query_vacuum(database); + + return query_vacuum.exec(QSL("OPTIMIZE TABLE Feeds;")) && + query_vacuum.exec(QSL("OPTIMIZE TABLE Messages;")); +} + +bool MariaDbDriver::saveDatabase() { + return true; +} + +void MariaDbDriver::backupDatabase(const QString& backup_folder, const QString& backup_name) { + Q_UNUSED(backup_folder) + Q_UNUSED(backup_name) +} + +bool MariaDbDriver::initiateRestoration(const QString& database_package_file) { + Q_UNUSED(database_package_file) + return true; +} + +bool MariaDbDriver::finishRestoration() { + return true; +} + +qint64 MariaDbDriver::databaseDataSize() { + QSqlDatabase database = connection(metaObject()->className()); + QSqlQuery query(database); + + query.prepare("SELECT Round(Sum(data_length + index_length), 1) " + "FROM information_schema.tables " + "WHERE table_schema = :db " + "GROUP BY table_schema;"); + query.bindValue(QSL(":db"), database.databaseName()); + + if (query.exec() && query.next()) { + return query.value(0).value(); + } + else { + return 0; + } +} + +QSqlDatabase MariaDbDriver::initializeDatabase(const QString& connection_name) { + // Folders are created. Create new QSqlDatabase object. + QSqlDatabase database = QSqlDatabase::addDatabase(APP_DB_MYSQL_DRIVER, connection_name); + const QString database_name = qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLDatabase)).toString(); + + database.setHostName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLHostname)).toString()); + database.setPort(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLPort)).toInt()); + database.setUserName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLUsername)).toString()); + database.setPassword(qApp->settings()->password(GROUP(Database), SETTING(Database::MySQLPassword)).toString()); + + if (!database.open()) { + qFatal("Cannot open MySQL database: %s.", qPrintable(database.lastError().text())); + } + else { + QSqlQuery query_db(database); + + query_db.setForwardOnly(true); + + if (!query_db.exec(QString("USE %1").arg(database_name)) || + !query_db.exec(QSL("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'"))) { + // If no "rssguard" database exists or schema version is wrong, then initialize it. + qWarningNN << LOGSEC_DB << "Error occurred. MySQL database is not initialized. Initializing now."; + + try { + const QStringList statements = prepareScript(APP_SQL_PATH, APP_DB_MYSQL_INIT, database_name); + + for (const QString& statement : statements) { + query_db.exec(statement); + + if (query_db.lastError().isValid()) { + throw ApplicationException(query_db.lastError().text()); + } + } + } + catch (const ApplicationException& ex) { + qFatal("Error when running SQL scripts: %s.", qPrintable(ex.message())); + } + + qDebugNN << LOGSEC_DB << "MySQL database backend should be ready now."; + } + else { + // Database was previously initialized. Now just check the schema version. + query_db.next(); + const QString installed_db_schema = query_db.value(0).toString(); + + if (installed_db_schema.toInt() < QString(APP_DB_SCHEMA_VERSION).toInt()) { + if (updateDatabaseSchema(database, installed_db_schema, database_name)) { + qDebugNN << LOGSEC_DB + << "Database schema was updated from '" + << installed_db_schema + << "' to '" + << APP_DB_SCHEMA_VERSION + << "' successully or it is already up to date."; + } + else { + qFatal("Database schema was not updated from '%s' to '%s' successully.", + qPrintable(installed_db_schema), + APP_DB_SCHEMA_VERSION); + } + } + } + + query_db.finish(); + } + + // Everything is initialized now. + m_databaseInitialized = true; + return database; +} + +bool MariaDbDriver::updateDatabaseSchema(const QSqlDatabase& database, + const QString& source_db_schema_version, + const QString& database_name) { + int working_version = QString(source_db_schema_version).remove('.').toInt(); + const int current_version = QString(APP_DB_SCHEMA_VERSION).remove('.').toInt(); + + while (working_version != current_version) { + try { + const QStringList statements = prepareScript(APP_SQL_PATH, + QString(APP_DB_UPDATE_FILE_PATTERN).arg(QSL("mysql"), + QString::number(working_version), + QString::number(working_version + 1)), + database_name); + + for (const QString& statement : statements) { + QSqlQuery query = database.exec(statement); + + if (!query.exec(statement) && query.lastError().isValid()) { + throw ApplicationException(query.lastError().text()); + } + } + } + catch (const ApplicationException& ex) { + qFatal("Error when running SQL scripts: %s.", qPrintable(ex.message())); + } + + // Increment the version. + qDebugNN << LOGSEC_DB + << "Updating database schema: '" + << working_version + << "' -> '" + << working_version + 1 + << "'."; + + working_version++; + } + + return true; +} + +QSqlDatabase MariaDbDriver::connection(const QString& connection_name, DatabaseDriver::DesiredStorageType desired_type) { + Q_UNUSED(desired_type) + + if (!m_databaseInitialized) { + // Return initialized database. + return initializeDatabase(connection_name); + } + else { + QSqlDatabase database; + + if (QSqlDatabase::contains(connection_name)) { + qDebugNN << LOGSEC_DB + << "MySQL connection '" + << connection_name + << "' is already active."; + + // 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_MYSQL_DRIVER, connection_name); + database.setHostName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLHostname)).toString()); + database.setPort(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLPort)).toInt()); + database.setUserName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLUsername)).toString()); + database.setPassword(qApp->settings()->password(GROUP(Database), SETTING(Database::MySQLPassword)).toString()); + database.setDatabaseName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLDatabase)).toString()); + } + + if (!database.isOpen() && !database.open()) { + qFatal("MySQL database was NOT opened. Delivered error message: '%s'.", + qPrintable(database.lastError().text())); + } + else { + qDebugNN << LOGSEC_DB + << "MySQL database connection" + << QUOTE_W_SPACE(connection_name) + << "to file" + << QUOTE_W_SPACE(QDir::toNativeSeparators(database.databaseName())) + << "seems to be established."; + } + + return database; + } +} diff --git a/src/librssguard/database/mariadbdriver.h b/src/librssguard/database/mariadbdriver.h new file mode 100755 index 000000000..e35b11845 --- /dev/null +++ b/src/librssguard/database/mariadbdriver.h @@ -0,0 +1,49 @@ +// For license of this file, see /LICENSE.md. + +#ifndef MARIADBDRIVER_H +#define MARIADBDRIVER_H + +#include "database/databasedriver.h" + +class MariaDbDriver : public DatabaseDriver { + public: + enum class MariaDbError { + Ok = 0, + UnknownError = 1, + AccessDenied = 1045, + UnknownDatabase = 1049, + ConnectionError = 2002, + CantConnect = 2003, + UnknownHost = 2005 + }; + + explicit MariaDbDriver(QObject* parent = nullptr); + + MariaDbError testConnection(const QString& hostname, int port, const QString& w_database, + const QString& username, const QString& password); + + virtual QString humanDriverType() const; + virtual QString qtDriverCode() const; + virtual DriverType driverType() const; + virtual bool vacuumDatabase(); + virtual bool saveDatabase(); + virtual void backupDatabase(const QString& backup_folder, const QString& backup_name); + virtual bool initiateRestoration(const QString& database_package_file); + virtual bool finishRestoration(); + virtual qint64 databaseDataSize(); + virtual QSqlDatabase connection(const QString& connection_name, + DatabaseDriver::DesiredStorageType desired_type = DatabaseDriver::DesiredStorageType::FromSettings); + + QString interpretErrorCode(MariaDbError error_code) const; + + private: + bool updateDatabaseSchema(const QSqlDatabase& database, + const QString& source_db_schema_version, + const QString& database_name); + QSqlDatabase initializeDatabase(const QString& connection_name); + + private: + bool m_databaseInitialized; +}; + +#endif // MARIADBDRIVER_H diff --git a/src/librssguard/gui/settings/settingsdatabase.cpp b/src/librssguard/gui/settings/settingsdatabase.cpp index 59141fe61..500386f07 100644 --- a/src/librssguard/gui/settings/settingsdatabase.cpp +++ b/src/librssguard/gui/settings/settingsdatabase.cpp @@ -3,6 +3,7 @@ #include "gui/settings/settingsdatabase.h" #include "database/databasefactory.h" +#include "database/mariadbdriver.h" #include "definitions/definitions.h" #include "gui/guiutilities.h" #include "miscellaneous/application.h" @@ -44,27 +45,24 @@ SettingsDatabase::~SettingsDatabase() { } void SettingsDatabase::mysqlTestConnection() { - // TODO: TODO + MariaDbDriver* driv = static_cast(qApp->database()->driver()); + const MariaDbDriver::MariaDbError error_code = driv->testConnection(m_ui->m_txtMysqlHostname->lineEdit()->text(), + m_ui->m_spinMysqlPort->value(), + m_ui->m_txtMysqlDatabase->lineEdit()->text(), + m_ui->m_txtMysqlUsername->lineEdit()->text(), + m_ui->m_txtMysqlPassword->lineEdit()->text()); + const QString interpretation = driv->interpretErrorCode(error_code); - /* - const DatabaseFactory::MySQLError error_code = qApp->database()->driver()->mysqlTestConnection(m_ui->m_txtMysqlHostname->lineEdit()->text(), - m_ui->m_spinMysqlPort->value(), - m_ui->m_txtMysqlDatabase->lineEdit()->text(), - m_ui->m_txtMysqlUsername->lineEdit()->text(), - m_ui->m_txtMysqlPassword->lineEdit()->text()); - const QString interpretation = qApp->database()->driver()->mysqlInterpretErrorCode(error_code); - - switch (error_code) { - case DatabaseFactory::MySQLError::Ok: - case DatabaseFactory::MySQLError::UnknownDatabase: + switch (error_code) { + case MariaDbDriver::MariaDbError::Ok: + case MariaDbDriver::MariaDbError::UnknownDatabase: m_ui->m_lblMysqlTestResult->setStatus(WidgetWithStatus::StatusType::Ok, interpretation, interpretation); break; - default: + default: m_ui->m_lblMysqlTestResult->setStatus(WidgetWithStatus::StatusType::Error, interpretation, interpretation); break; - } - */ + } } void SettingsDatabase::onMysqlHostnameChanged(const QString& new_hostname) { diff --git a/src/librssguard/librssguard.pro b/src/librssguard/librssguard.pro index 7419ebd47..150f02bcf 100644 --- a/src/librssguard/librssguard.pro +++ b/src/librssguard/librssguard.pro @@ -47,6 +47,7 @@ HEADERS += core/feeddownloader.h \ database/databasedriver.h \ database/databasefactory.h \ database/databasequeries.h \ + database/mariadbdriver.h \ database/sqlitedriver.h \ definitions/definitions.h \ definitions/typedefs.h \ @@ -228,6 +229,7 @@ SOURCES += core/feeddownloader.cpp \ database/databasedriver.cpp \ database/databasefactory.cpp \ database/databasequeries.cpp \ + database/mariadbdriver.cpp \ database/sqlitedriver.cpp \ dynamic-shortcuts/dynamicshortcuts.cpp \ dynamic-shortcuts/dynamicshortcutswidget.cpp \