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"