File indexing completed on 2025-01-05 04:46:51

0001 /*
0002     SPDX-FileCopyrightText: 2024 g10 Code GmbH
0003     SPDX-FileContributor: Daniel Vrátil <dvratil@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "dbmigrator.h"
0009 #include "ControlManager.h"
0010 #include "akonadidbmigrator_debug.h"
0011 #include "akonadifull-version.h"
0012 #include "akonadischema.h"
0013 #include "akranges.h"
0014 #include "entities.h"
0015 #include "private/dbus_p.h"
0016 #include "private/standarddirs_p.h"
0017 #include "storage/countquerybuilder.h"
0018 #include "storage/datastore.h"
0019 #include "storage/dbconfig.h"
0020 #include "storage/dbconfigmysql.h"
0021 #include "storage/dbconfigpostgresql.h"
0022 #include "storage/dbconfigsqlite.h"
0023 #include "storage/dbintrospector.h"
0024 #include "storage/querybuilder.h"
0025 #include "storage/schematypes.h"
0026 #include "storage/transaction.h"
0027 #include "storagejanitor.h"
0028 #include "utils.h"
0029 
0030 #include <QCommandLineOption>
0031 #include <QCommandLineParser>
0032 #include <QCoreApplication>
0033 #include <QDBusConnection>
0034 #include <QDBusError>
0035 #include <QDBusInterface>
0036 #include <QDir>
0037 #include <QElapsedTimer>
0038 #include <QFile>
0039 #include <QFileInfo>
0040 #include <QScopeGuard>
0041 #include <QSettings>
0042 #include <QSqlDatabase>
0043 #include <QSqlError>
0044 #include <QSqlQuery>
0045 #include <QSqlRecord>
0046 #include <QThread>
0047 
0048 #include <KLocalizedString>
0049 
0050 #include <chrono>
0051 #include <memory>
0052 #include <qdbusconnection.h>
0053 
0054 using namespace Akonadi;
0055 using namespace Akonadi::Server;
0056 using namespace AkRanges;
0057 using namespace std::chrono_literals;
0058 
0059 Q_DECLARE_METATYPE(UIDelegate::Result);
0060 
0061 namespace
0062 {
0063 
0064 constexpr size_t maxTransactionSize = 1000; // Arbitary guess
0065 
0066 class MigratorDataStoreFactory : public DataStoreFactory
0067 {
0068 public:
0069     explicit MigratorDataStoreFactory(DbConfig *config)
0070         : m_dbConfig(config)
0071     {
0072     }
0073 
0074     DataStore *createStore() override
0075     {
0076         class MigratorDataStore : public DataStore
0077         {
0078         public:
0079             explicit MigratorDataStore(DbConfig *config)
0080                 : DataStore(config)
0081             {
0082             }
0083         };
0084         return new MigratorDataStore(m_dbConfig);
0085     }
0086 
0087 private:
0088     DbConfig *const m_dbConfig;
0089 };
0090 
0091 class Rollback
0092 {
0093     Q_DISABLE_COPY_MOVE(Rollback)
0094 public:
0095     Rollback() = default;
0096     ~Rollback()
0097     {
0098         run();
0099     }
0100 
0101     void reset()
0102     {
0103         mRollbacks.clear();
0104     }
0105 
0106     void run()
0107     {
0108         // Run rollbacks in reverse order!
0109         for (auto it = mRollbacks.rbegin(); it != mRollbacks.rend(); ++it) {
0110             (*it)();
0111         }
0112         mRollbacks.clear();
0113     }
0114 
0115     template<typename T>
0116     void add(T &&rollback)
0117     {
0118         mRollbacks.push_back(std::forward<T>(rollback));
0119     }
0120 
0121 private:
0122     std::vector<std::function<void()>> mRollbacks;
0123 };
0124 
0125 void stopDatabaseServer(DbConfig *config)
0126 {
0127     config->stopInternalServer();
0128 }
0129 
0130 bool createDatabase(DbConfig *config)
0131 {
0132     auto db = QSqlDatabase::addDatabase(config->driverName(), QStringLiteral("initConnection"));
0133     config->apply(db);
0134     db.setDatabaseName(config->databaseName());
0135     if (!db.isValid()) {
0136         qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid database object during initial database connection";
0137         return false;
0138     }
0139 
0140     const auto closeDb = qScopeGuard([&db]() {
0141         db.close();
0142     });
0143 
0144     // Try to connect to the database and select the Akonadi DB
0145     if (db.open()) {
0146         return true;
0147     }
0148 
0149     // That failed - the database might not exist yet..
0150     qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to use database" << config->databaseName();
0151     qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
0152     qCDebug(AKONADIDBMIGRATOR_LOG) << "Trying to create database now...";
0153 
0154     db.close();
0155     db.setDatabaseName(QString());
0156     // Try to just connect to the DB server without selecting a database
0157     if (!db.open()) {
0158         // Failed, DB server not running or broken
0159         qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to connect to database!";
0160         qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
0161         return false;
0162     }
0163 
0164     // Try to create the database
0165     QSqlQuery query(db);
0166     if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(config->databaseName()))) {
0167         qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to create database";
0168         qCCritical(AKONADIDBMIGRATOR_LOG) << "Query error:" << query.lastError().text();
0169         qCCritical(AKONADIDBMIGRATOR_LOG) << "Database error:" << db.lastError().text();
0170         return false;
0171     }
0172     return true;
0173 }
0174 
0175 std::unique_ptr<DataStore> prepareDatabase(DbConfig *config)
0176 {
0177     if (config->useInternalServer()) {
0178         if (!config->startInternalServer()) {
0179             qCCritical(AKONADIDBMIGRATOR_LOG) << "Failed to start the database server";
0180             return {};
0181         }
0182     } else {
0183         if (!createDatabase(config)) {
0184             return {};
0185         }
0186     }
0187 
0188     config->setup();
0189 
0190     auto factory = std::make_unique<MigratorDataStoreFactory>(config);
0191     std::unique_ptr<DataStore> store{factory->createStore()};
0192     if (!store->database().isOpen()) {
0193         return {};
0194     }
0195     if (!store->init()) {
0196         return {};
0197     }
0198 
0199     return store;
0200 }
0201 
0202 void cleanupDatabase(DataStore *store, DbConfig *config)
0203 {
0204     store->close();
0205 
0206     stopDatabaseServer(config);
0207 }
0208 
0209 bool syncAutoIncrementValue(DataStore *sourceStore, DataStore *destStore, const TableDescription &table)
0210 {
0211     const auto idCol = std::find_if(table.columns.begin(), table.columns.end(), [](const auto &col) {
0212         return col.isPrimaryKey;
0213     });
0214     if (idCol == table.columns.end()) {
0215         return false;
0216     }
0217 
0218     const auto getAutoIncrementValue = [](DataStore *store, const QString &table, const QString &idCol) -> std::optional<qint64> {
0219         const auto db = store->database();
0220         const auto introspector = DbIntrospector::createInstance(db);
0221 
0222         QSqlQuery query(db);
0223         if (!query.exec(introspector->getAutoIncrementValueQuery(table, idCol))) {
0224             qCCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
0225             return {};
0226         }
0227         if (!query.next()) {
0228             // SQLite returns an empty result set for empty tables, so we assume the table is empty and
0229             // the counter starts at 1
0230             return 1;
0231         }
0232 
0233         return query.value(0).toLongLong();
0234     };
0235 
0236     const auto setAutoIncrementValue = [](DataStore *store, const QString &table, const QString &idCol, qint64 seq) -> bool {
0237         const auto db = store->database();
0238         const auto introspector = DbIntrospector::createInstance(db);
0239 
0240         QSqlQuery query(db);
0241         if (!query.exec(introspector->updateAutoIncrementValueQuery(table, idCol, seq))) {
0242             qCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
0243             return false;
0244         }
0245 
0246         return true;
0247     };
0248 
0249     const auto seq = getAutoIncrementValue(sourceStore, table.name, idCol->name);
0250     if (!seq) {
0251         return false;
0252     }
0253 
0254     return setAutoIncrementValue(destStore, table.name, idCol->name, *seq);
0255 }
0256 
0257 bool analyzeTable(const QString &table, DataStore *store)
0258 {
0259     auto dbType = DbType::type(store->database());
0260     QString queryStr;
0261     switch (dbType) {
0262     case DbType::Sqlite:
0263     case DbType::PostgreSQL:
0264         queryStr = QLatin1StringView("ANALYZE ") + table;
0265         break;
0266     case DbType::MySQL:
0267         queryStr = QLatin1StringView("ANALYZE TABLE ") + table;
0268         break;
0269     case DbType::Unknown:
0270         qCCritical(AKONADIDBMIGRATOR_LOG) << "Unknown database type";
0271         return false;
0272     }
0273 
0274     QSqlQuery query(store->database());
0275     if (!query.exec(queryStr)) {
0276         qCCritical(AKONADIDBMIGRATOR_LOG) << query.lastError().text();
0277         return false;
0278     }
0279 
0280     return true;
0281 }
0282 
0283 QString createTmpAkonadiServerRc(const QString &targetEngine)
0284 {
0285     const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
0286     const auto newFileName = QStringLiteral("%1.new").arg(origFileName);
0287 
0288     QSettings settings(newFileName, QSettings::IniFormat);
0289     settings.setValue(QStringLiteral("General/Driver"), targetEngine);
0290 
0291     return newFileName;
0292 }
0293 
0294 QString driverFromEngineName(const QString &engine)
0295 {
0296     const auto enginelc = engine.toLower();
0297     if (enginelc == QLatin1StringView("sqlite")) {
0298         return QStringLiteral("QSQLITE");
0299     }
0300     if (enginelc == QLatin1StringView("mysql")) {
0301         return QStringLiteral("QMYSQL");
0302     }
0303     if (enginelc == QLatin1StringView("postgres")) {
0304         return QStringLiteral("QPSQL");
0305     }
0306 
0307     qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid engine:" << engine;
0308     return {};
0309 }
0310 
0311 std::unique_ptr<DbConfig> dbConfigFromServerRc(const QString &configFile, bool overrideDbPath = false)
0312 {
0313     QSettings settings(configFile, QSettings::IniFormat);
0314     const auto driver = settings.value(QStringLiteral("General/Driver")).toString();
0315     std::unique_ptr<DbConfig> config;
0316     QString dbPathSuffix;
0317     if (driver == QLatin1StringView("QSQLITE") || driver == QLatin1StringView("QSQLITE3")) {
0318         config = std::make_unique<DbConfigSqlite>(configFile);
0319     } else if (driver == QLatin1StringView("QMYSQL")) {
0320         config = std::make_unique<DbConfigMysql>(configFile);
0321         dbPathSuffix = QStringLiteral("/db_data");
0322     } else if (driver == QLatin1StringView("QPSQL")) {
0323         config = std::make_unique<DbConfigPostgresql>(configFile);
0324         dbPathSuffix = QStringLiteral("/db_data");
0325     } else {
0326         qCCritical(AKONADIDBMIGRATOR_LOG) << "Invalid driver: " << driver;
0327         return {};
0328     }
0329 
0330     const auto dbPath = overrideDbPath ? StandardDirs::saveDir("data", QStringLiteral("db_migration%1").arg(dbPathSuffix)) : QString{};
0331     config->init(settings, true, dbPath);
0332 
0333     return config;
0334 }
0335 
0336 bool isValidDbPath(const QString &path)
0337 {
0338     if (path.isEmpty()) {
0339         return false;
0340     }
0341 
0342     const QFileInfo fi(path);
0343     return fi.exists() && (fi.isFile() || fi.isDir());
0344 }
0345 
0346 std::error_code restoreDatabaseFromBackup(const QString &backupPath, const QString &originalPath)
0347 {
0348     std::error_code ec;
0349     if (QFileInfo(originalPath).exists()) {
0350         // Remove the original path, it could have been created by the new db
0351         std::filesystem::remove_all(originalPath.toStdString(), ec);
0352         return ec;
0353     }
0354 
0355     // std::filesystem doesn't care whether it's file or directory, unlike QFile vs QDir.
0356     std::filesystem::rename(backupPath.toStdString(), originalPath.toStdString(), ec);
0357     return ec;
0358 }
0359 
0360 bool akonadiIsRunning()
0361 {
0362     auto sessionIface = QDBusConnection::sessionBus().interface();
0363     return sessionIface->isServiceRegistered(DBus::serviceName(DBus::ControlLock)) || sessionIface->isServiceRegistered(DBus::serviceName(DBus::Server));
0364 }
0365 
0366 bool stopAkonadi()
0367 {
0368     static constexpr auto shutdownTimeout = 5s;
0369 
0370     org::freedesktop::Akonadi::ControlManager manager(DBus::serviceName(DBus::Control), QStringLiteral("/ControlManager"), QDBusConnection::sessionBus());
0371     if (!manager.isValid()) {
0372         return false;
0373     }
0374 
0375     manager.shutdown();
0376 
0377     QElapsedTimer timer;
0378     timer.start();
0379     while (akonadiIsRunning() && timer.durationElapsed() <= shutdownTimeout) {
0380         QThread::msleep(100);
0381     }
0382 
0383     return timer.durationElapsed() <= shutdownTimeout && !akonadiIsRunning();
0384 }
0385 
0386 bool startAkonadi()
0387 {
0388     QDBusConnection::sessionBus().interface()->startService(DBus::serviceName(DBus::Control));
0389     return true;
0390 }
0391 
0392 bool acquireAkonadiLock()
0393 {
0394     auto connIface = QDBusConnection::sessionBus().interface();
0395     auto reply = connIface->registerService(DBus::serviceName(DBus::ControlLock));
0396     if (!reply.isValid() || reply != QDBusConnectionInterface::ServiceRegistered) {
0397         return false;
0398     }
0399 
0400     reply = connIface->registerService(DBus::serviceName(DBus::UpgradeIndicator));
0401     if (!reply.isValid() || reply != QDBusConnectionInterface::ServiceRegistered) {
0402         return false;
0403     }
0404 
0405     return true;
0406 }
0407 
0408 bool releaseAkonadiLock()
0409 {
0410     auto connIface = QDBusConnection::sessionBus().interface();
0411     connIface->unregisterService(DBus::serviceName(DBus::ControlLock));
0412     connIface->unregisterService(DBus::serviceName(DBus::UpgradeIndicator));
0413     return true;
0414 }
0415 
0416 } // namespace
0417 
0418 DbMigrator::DbMigrator(const QString &targetEngine, UIDelegate *delegate, QObject *parent)
0419     : QObject(parent)
0420     , m_targetEngine(targetEngine)
0421     , m_uiDelegate(delegate)
0422 {
0423     qRegisterMetaType<UIDelegate::Result>();
0424 }
0425 
0426 DbMigrator::~DbMigrator()
0427 {
0428     if (m_thread) {
0429         m_thread->wait();
0430     }
0431 }
0432 
0433 void DbMigrator::startMigration()
0434 {
0435     m_thread.reset(QThread::create([this]() {
0436         bool restartAkonadi = false;
0437         if (akonadiIsRunning()) {
0438             emitInfo(i18nc("@info:status", "Stopping Akonadi service..."));
0439             restartAkonadi = true;
0440             if (!stopAkonadi()) {
0441                 emitError(i18nc("@info:status", "Error: timeout while waiting for Akonadi to stop."));
0442                 emitCompletion(false);
0443                 return;
0444             }
0445         }
0446 
0447         if (!acquireAkonadiLock()) {
0448             emitError(i18nc("@info:status", "Error: couldn't acquire DBus lock for Akonadi."));
0449             emitCompletion(false);
0450             return;
0451         }
0452 
0453         const bool result = runMigrationThread();
0454 
0455         releaseAkonadiLock();
0456         if (restartAkonadi) {
0457             emitInfo(i18nc("@info:status", "Starting Akonadi service..."));
0458             startAkonadi();
0459         }
0460 
0461         emitCompletion(result);
0462     }));
0463     m_thread->start();
0464 }
0465 
0466 bool DbMigrator::runStorageJanitor(DbConfig *dbConfig)
0467 {
0468     StorageJanitor janitor(dbConfig);
0469     connect(&janitor, &StorageJanitor::done, this, [this]() {
0470         emitInfo(i18nc("@info:status", "Database fsck completed"));
0471     });
0472     // Runs the janitor in the current thread
0473     janitor.check();
0474 
0475     return true;
0476 }
0477 
0478 bool DbMigrator::runMigrationThread()
0479 {
0480     Rollback rollback;
0481 
0482     const auto driver = driverFromEngineName(m_targetEngine);
0483     if (driver.isEmpty()) {
0484         emitError(i18nc("@info:status", "Invalid database engine \"%1\" - valid values are \"sqlite\", \"mysql\" and \"postgres\".", m_targetEngine));
0485         return false;
0486     }
0487 
0488     // Create backup akonadiserverrc
0489     const auto sourceServerCfgFile = backupAkonadiServerRc();
0490     if (!sourceServerCfgFile) {
0491         return false;
0492     }
0493     rollback.add([file = *sourceServerCfgFile]() {
0494         QFile::remove(file);
0495     });
0496 
0497     // Create new akonadiserverrc with the new engine configuration
0498     const QString destServerCfgFile = createTmpAkonadiServerRc(driver);
0499     rollback.add([destServerCfgFile]() {
0500         QFile::remove(destServerCfgFile);
0501     });
0502 
0503     // Create DbConfig for the source DB
0504     auto sourceConfig = dbConfigFromServerRc(*sourceServerCfgFile);
0505     if (!sourceConfig) {
0506         emitError(i18nc("@info:shell", "Error: failed to configure source database server."));
0507         return false;
0508     }
0509 
0510     if (sourceConfig->driverName() == driver) {
0511         emitError(i18nc("@info:shell", "Source and destination database engines are the same."));
0512         return false;
0513     }
0514 
0515     // Check that we actually have valid source datbase path
0516     const auto sourceDbPath = sourceConfig->databasePath();
0517     if (!isValidDbPath(sourceDbPath)) {
0518         emitError(i18nc("@info:shell", "Error: failed to obtain path to source database data file or directory."));
0519         return false;
0520     }
0521 
0522     // Configure the new DB server
0523     auto destConfig = dbConfigFromServerRc(destServerCfgFile, /* overrideDbPath=*/true);
0524     if (!destConfig) {
0525         emitError(i18nc("@info:shell", "Error: failed to configure the new database server."));
0526         return false;
0527     }
0528 
0529     auto sourceStore = prepareDatabase(sourceConfig.get());
0530     if (!sourceStore) {
0531         emitError(i18nc("@info:shell", "Error: failed to open existing database to migrate data from."));
0532         return false;
0533     }
0534     auto destStore = prepareDatabase(destConfig.get());
0535     if (!destStore) {
0536         emitError(i18nc("@info:shell", "Error: failed to open new database to migrate data to."));
0537         return false;
0538     }
0539 
0540     // Run StorageJanitor on the existing database to ensure it's in a consistent state
0541     emitInfo(i18nc("@info:status", "Running fsck on the source database"));
0542     runStorageJanitor(sourceConfig.get());
0543 
0544     const bool migrationSuccess = migrateTables(sourceStore.get(), destStore.get(), destConfig.get());
0545 
0546     // Stop database servers and close connections. Make sure we always reach here, even if the migration failed
0547     cleanupDatabase(sourceStore.get(), sourceConfig.get());
0548     cleanupDatabase(destStore.get(), destConfig.get());
0549 
0550     if (!migrationSuccess) {
0551         return false;
0552     }
0553 
0554     // Remove the migrated database if the migration or post-migration steps fail
0555     rollback.add([this, dbPath = destConfig->databasePath()]() {
0556         std::error_code ec;
0557         std::filesystem::remove_all(dbPath.toStdString(), ec);
0558         if (ec) {
0559             emitError(i18nc("@info:status %2 is error message",
0560                             "Error: failed to remove temporary database directory %1: %2",
0561                             dbPath,
0562                             QString::fromStdString(ec.message())));
0563         }
0564     });
0565 
0566     // Move the old database into backup location
0567     emitInfo(i18nc("@info:shell", "Backing up original database..."));
0568     const auto backupPath = moveDatabaseToBackupLocation(sourceConfig.get());
0569     if (!backupPath.has_value()) {
0570         return false;
0571     }
0572 
0573     if (!backupPath->isEmpty()) {
0574         rollback.add([this, backupPath = *backupPath, sourceDbPath]() {
0575             emitInfo(i18nc("@info:status", "Restoring database from backup %1 to %2", backupPath, sourceDbPath));
0576             if (const auto ec = restoreDatabaseFromBackup(backupPath, sourceDbPath); ec) {
0577                 emitError(i18nc("@info:status %1 is error message", "Error: failed to restore database from backup: %1", QString::fromStdString(ec.message())));
0578             }
0579         });
0580     }
0581 
0582     // Move the new database to the main location
0583     if (!moveDatabaseToMainLocation(destConfig.get(), destServerCfgFile)) {
0584         return false;
0585     }
0586 
0587     // Migration was success, nothing to roll back.
0588     rollback.reset();
0589 
0590     return true;
0591 }
0592 
0593 bool DbMigrator::migrateTables(DataStore *sourceStore, DataStore *destStore, DbConfig *destConfig)
0594 {
0595     // Disable foreign key constraint checks
0596     if (!destConfig->disableConstraintChecks(destStore->database())) {
0597         return false;
0598     }
0599 
0600     AkonadiSchema schema;
0601     const int totalTables = schema.tables().size() + schema.relations().size();
0602     int doneTables = 0;
0603 
0604     // Copy regular tables
0605     for (const auto &table : schema.tables()) {
0606         ++doneTables;
0607         emitProgress(table.name, doneTables, totalTables);
0608         if (!copyTable(sourceStore, destStore, table)) {
0609             emitError(i18nc("@info:shell", "Error has occurred while migrating table %1", table.name));
0610             return false;
0611         }
0612     }
0613 
0614     // Copy relational tables
0615     for (const auto &relation : schema.relations()) {
0616         const RelationTableDescription table{relation};
0617         ++doneTables;
0618         emitProgress(table.name, doneTables, totalTables);
0619         if (!copyTable(sourceStore, destStore, table)) {
0620             emitError(i18nc("@info:shell", "Error has occurred while migrating table %1", table.name));
0621             return false;
0622         }
0623     }
0624 
0625     // Re-enable foreign key constraint checks
0626     if (!destConfig->enableConstraintChecks(destStore->database())) {
0627         return false;
0628     }
0629 
0630     return true;
0631 }
0632 
0633 std::optional<QString> DbMigrator::moveDatabaseToBackupLocation(DbConfig *config)
0634 {
0635     const std::filesystem::path dbPath = config->databasePath().toStdString();
0636 
0637     QDir backupDir = StandardDirs::saveDir("data", QStringLiteral("migration_backup"));
0638     if (!backupDir.isEmpty()) {
0639         const auto result = questionYesNoSkip(i18nc("@label", "Backup directory already exists. Do you want to overwrite the previous backup?"));
0640         if (result == UIDelegate::Result::Skip) {
0641             return QString{};
0642         }
0643         if (result == UIDelegate::Result::No) {
0644             emitError(i18nc("@info:shell", "Cannot proceed without backup. Migration interrupted."));
0645             return {};
0646         }
0647 
0648         if (!backupDir.removeRecursively()) {
0649             emitError(i18nc("@info:shell", "Failed to remove previous backup directory."));
0650             return {};
0651         }
0652     }
0653 
0654     backupDir.mkpath(QStringLiteral("."));
0655 
0656     std::error_code ec;
0657     std::filesystem::path backupPath = backupDir.absolutePath().toStdString();
0658     // /path/to/akonadi.sql -> /path/to/backup/akonadi.sql
0659     // /path/to/db_data -> /path/to/backup/db_data
0660     std::filesystem::rename(dbPath, backupPath / *(--dbPath.end()), ec);
0661     if (ec) {
0662         emitError(i18nc("@info:shell", "Failed to move database to backup location: %1", QString::fromStdString(ec.message())));
0663         return {};
0664     }
0665 
0666     return backupDir.absolutePath();
0667 }
0668 
0669 std::optional<QString> DbMigrator::backupAkonadiServerRc()
0670 {
0671     const auto origFileName = StandardDirs::serverConfigFile(StandardDirs::ReadWrite);
0672     const auto bkpFileName = QStringLiteral("%1.bkp").arg(origFileName);
0673     std::error_code ec;
0674 
0675     if (QFile::exists(bkpFileName)) {
0676         const auto result = questionYesNo(i18nc("@label", "Backup file %1 already exists. Overwrite?", bkpFileName));
0677         if (result != UIDelegate::Result::Yes) {
0678             emitError(i18nc("@info:status", "Cannot proceed without backup. Migration interrupted."));
0679             return {};
0680         }
0681 
0682         std::filesystem::remove(bkpFileName.toStdString(), ec);
0683         if (ec) {
0684             emitError(i18nc("@info:status", "Error: Failed to remove existing backup file %1: %2", bkpFileName, QString::fromStdString(ec.message())));
0685             return {};
0686         }
0687     }
0688 
0689     std::filesystem::copy(origFileName.toStdString(), bkpFileName.toStdString(), ec);
0690     if (ec) {
0691         emitError(i18nc("@info:status", "Failed to back up Akonadi Server configuration: %1", QString::fromStdString(ec.message())));
0692         return {};
0693     }
0694 
0695     return bkpFileName;
0696 }
0697 
0698 bool DbMigrator::moveDatabaseToMainLocation(DbConfig *destConfig, const QString &destServerCfgFile)
0699 {
0700     std::error_code ec;
0701     // /path/to/db_migration/akonadi.db -> /path/to/akonadi.db
0702     // /path/to/db_migration/db_data -> /path/to/db_data
0703     const std::filesystem::path dbSrcPath = destConfig->databasePath().toStdString();
0704     const auto dbDestPath = dbSrcPath.parent_path().parent_path() / *(--dbSrcPath.end());
0705     std::filesystem::rename(dbSrcPath, dbDestPath, ec);
0706     if (ec) {
0707         emitError(i18nc("@info:status %1 is error message",
0708                         "Error: failed to move migrated database to the primary location: %1",
0709                         QString::fromStdString(ec.message())));
0710         return false;
0711     }
0712 
0713     // Adjust the db path stored in new akonadiserverrc to point to the primary location
0714     {
0715         QSettings settings(destServerCfgFile, QSettings::IniFormat);
0716         destConfig->setDatabasePath(QString::fromStdString(dbDestPath.string()), settings);
0717     }
0718 
0719     // Remove the - now empty - db_migration directory
0720     // We don't concern ourselves too much with this failing.
0721     std::filesystem::remove(dbSrcPath.parent_path(), ec);
0722 
0723     // Turn the new temporary akonadiserverrc int othe main one so that
0724     // Akonadi starts with the new DB configuration.
0725     std::filesystem::remove(StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
0726     if (ec) {
0727         emitError(i18nc("@info:status %1 is error message", "Error: failed to remove original akonadiserverrc: %1", QString::fromStdString(ec.message())));
0728         return false;
0729     }
0730     std::filesystem::rename(destServerCfgFile.toStdString(), StandardDirs::serverConfigFile(StandardDirs::ReadWrite).toStdString(), ec);
0731     if (ec) {
0732         emitError(i18nc("@info:status %1 is error message",
0733                         "Error: failed to move new akonadiserverrc to the primary location: %1",
0734                         QString::fromStdString(ec.message())));
0735         return false;
0736     }
0737 
0738     return true;
0739 }
0740 
0741 bool DbMigrator::copyTable(DataStore *sourceStore, DataStore *destStore, const TableDescription &table)
0742 {
0743     const auto columns = table.columns | Views::transform([](const auto &tbl) {
0744                              return tbl.name;
0745                          })
0746         | Actions::toQList;
0747 
0748     // Count number of items in the current table
0749     const auto totalRows = [](DataStore *store, const QString &table) {
0750         CountQueryBuilder countQb(store, table);
0751         countQb.exec();
0752         return countQb.result();
0753     }(sourceStore, table.name);
0754 
0755     // Fetch *everything* from the current able
0756     QueryBuilder sourceQb(sourceStore, table.name);
0757     sourceQb.addColumns(columns);
0758     sourceQb.exec();
0759     auto sourceQuery = sourceQb.query();
0760 
0761     // Clean the destination table (from data pre-inserted by DbInitializer)
0762     {
0763         QueryBuilder clearQb(destStore, table.name, QueryBuilder::Delete);
0764         clearQb.exec();
0765     }
0766 
0767     // Begin insertion transaction
0768     Transaction transaction(destStore, QStringLiteral("Migrator"));
0769     size_t trxSize = 0;
0770     size_t processed = 0;
0771 
0772     // Loop over source resluts
0773     while (sourceQuery.next()) {
0774         // Insert the current row into the new database
0775         QueryBuilder destQb(destStore, table.name, QueryBuilder::Insert);
0776         destQb.setIdentificationColumn({});
0777         for (int col = 0; col < table.columns.size(); ++col) {
0778             QVariant value;
0779             if (table.columns[col].type == QLatin1StringView("QDateTime")) {
0780                 value = Utils::variantToDateTime(sourceQuery.value(col), sourceStore);
0781             } else if (table.columns[col].type == QLatin1StringView("bool")) {
0782                 value = sourceQuery.value(col).toBool();
0783             } else if (table.columns[col].type == QLatin1StringView("QByteArray")) {
0784                 value = Utils::variantToByteArray(sourceQuery.value(col));
0785             } else if (table.columns[col].type == QLatin1StringView("QString")) {
0786                 value = Utils::variantToString(sourceQuery.value(col));
0787             } else {
0788                 value = sourceQuery.value(col);
0789             }
0790             destQb.setColumnValue(table.columns[col].name, value);
0791         }
0792         if (!destQb.exec()) {
0793             qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to insert row into table" << table.name << ":" << destQb.query().lastError().text();
0794             return false;
0795         }
0796 
0797         // Commit the transaction after every "maxTransactionSize" inserts to make it reasonably fast
0798         if (++trxSize > maxTransactionSize) {
0799             if (!transaction.commit()) {
0800                 qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to commit transaction:" << destStore->database().lastError().text();
0801                 return false;
0802             }
0803             trxSize = 0;
0804             transaction.begin();
0805         }
0806 
0807         emitTableProgress(table.name, ++processed, totalRows);
0808     }
0809 
0810     // Commit whatever is left in the transaction
0811     if (!transaction.commit()) {
0812         qCWarning(AKONADIDBMIGRATOR_LOG) << "Failed to commit transaction:" << destStore->database().lastError().text();
0813         return false;
0814     }
0815 
0816     // Synchronize next autoincrement value (if the table has one)
0817     if (const auto cnt = std::count_if(table.columns.begin(),
0818                                        table.columns.end(),
0819                                        [](const auto &col) {
0820                                            return col.isAutoIncrement;
0821                                        });
0822         cnt == 1) {
0823         if (!syncAutoIncrementValue(sourceStore, destStore, table)) {
0824             emitError(i18nc("@info:status", "Error: failed to update autoincrement value for table %1", table.name));
0825             return false;
0826         }
0827     }
0828 
0829     emitInfo(i18nc("@info:status", "Optimizing table %1...", table.name));
0830     // Optimize the new table
0831     if (!analyzeTable(table.name, destStore)) {
0832         emitError(i18nc("@info:status", "Error: failed to optimize table %1", table.name));
0833         return false;
0834     }
0835 
0836     return true;
0837 }
0838 
0839 void DbMigrator::emitInfo(const QString &message)
0840 {
0841     QMetaObject::invokeMethod(
0842         this,
0843         [this, message]() {
0844             Q_EMIT info(message);
0845         },
0846         Qt::QueuedConnection);
0847 }
0848 
0849 void DbMigrator::emitError(const QString &message)
0850 {
0851     QMetaObject::invokeMethod(
0852         this,
0853         [this, message]() {
0854             Q_EMIT error(message);
0855         },
0856         Qt::QueuedConnection);
0857 }
0858 
0859 void DbMigrator::emitProgress(const QString &table, int done, int total)
0860 {
0861     QMetaObject::invokeMethod(
0862         this,
0863         [this, table, done, total]() {
0864             Q_EMIT progress(table, done, total);
0865         },
0866         Qt::QueuedConnection);
0867 }
0868 
0869 void DbMigrator::emitTableProgress(const QString &table, int done, int total)
0870 {
0871     QMetaObject::invokeMethod(
0872         this,
0873         [this, table, done, total]() {
0874             Q_EMIT tableProgress(table, done, total);
0875         },
0876         Qt::QueuedConnection);
0877 }
0878 
0879 void DbMigrator::emitCompletion(bool success)
0880 {
0881     QMetaObject::invokeMethod(
0882         this,
0883         [this, success]() {
0884             Q_EMIT migrationCompleted(success);
0885         },
0886         Qt::QueuedConnection);
0887 }
0888 
0889 UIDelegate::Result DbMigrator::questionYesNo(const QString &question)
0890 {
0891     UIDelegate::Result answer;
0892     QMetaObject::invokeMethod(
0893         this,
0894         [this, question, &answer]() {
0895             if (m_uiDelegate) {
0896                 answer = m_uiDelegate->questionYesNo(question);
0897             }
0898         },
0899         Qt::BlockingQueuedConnection);
0900     return answer;
0901 }
0902 
0903 UIDelegate::Result DbMigrator::questionYesNoSkip(const QString &question)
0904 {
0905     UIDelegate::Result answer;
0906     QMetaObject::invokeMethod(
0907         this,
0908         [this, question, &answer]() {
0909             if (m_uiDelegate) {
0910                 answer = m_uiDelegate->questionYesNoSkip(question);
0911             }
0912         },
0913         Qt::BlockingQueuedConnection);
0914     return answer;
0915 }
0916 #include "moc_dbmigrator.cpp"