File indexing completed on 2025-02-02 05:17:48

0001 /*
0002  *   SPDX-FileCopyrightText: 2011, 2012 Ivan Cukic <ivan.cukic(at)kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 // Self
0008 #include "Database.h"
0009 #include <kactivities-features.h>
0010 
0011 // Qt
0012 #include <QDir>
0013 #include <QStandardPaths>
0014 
0015 // Utils
0016 #include <utils/d_ptr_implementation.h>
0017 #include <utils/qsqlquery_iterator.h>
0018 
0019 // System
0020 #include <array>
0021 #include <cmath>
0022 #include <memory>
0023 
0024 // Local
0025 #include "DebugResources.h"
0026 #include "Utils.h"
0027 
0028 #include <common/database/schema/ResourcesDatabaseSchema.h>
0029 
0030 class ResourcesDatabaseInitializer::Private
0031 {
0032 public:
0033     Common::Database::Ptr database;
0034 };
0035 
0036 Common::Database::Ptr resourcesDatabase()
0037 {
0038     static ResourcesDatabaseInitializer instance;
0039     return instance.d->database;
0040 }
0041 
0042 void ResourcesDatabaseInitializer::initDatabase(bool retryOnFail)
0043 {
0044     //
0045     // There are three situations we want to handle:
0046     // 1. The database can not be opened at all.
0047     //    This means that the current database files have
0048     //    been corrupted and that we need to replace them
0049     //    with the last working backup.
0050     // 2. The database was opened, but an error appeared
0051     //    somewhere at runtime.
0052     // 3. The database was successfully opened and no errors
0053     //    appeared during runtime.
0054     //
0055     // To achieve this, we will have three locations for
0056     // database files:
0057     //
0058     // 1. `resources` - the current database files
0059     // 2. `resources-test-backup` - at each KAMD start,
0060     //    we copy the current database files here.
0061     //    If an error appears during execution, the files
0062     //    will be removed and the error will be added to
0063     //    the log file `resources/errors.log`
0064     // 3. `resources-working-backup` - on each KAMD start,
0065     //    if there are files in `resources-test-backup`
0066     //    (meaning no error appeared at runtime), they
0067     //    will be copied to `resources-working-backup`.
0068     //
0069     // This means that the `working` backup will be a bit
0070     // older, but it will be the last database that produced
0071     // no errors at runtime.
0072     //
0073 
0074     const QString databaseDirectoryPath =
0075         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/");
0076 
0077     const QString databaseTestBackupDirectoryPath =
0078         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/test-backup/");
0079 
0080     const QString databaseWorkingBackupDirectoryPath =
0081         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/working-backup/");
0082 
0083     static const std::array<QString, 3> databaseFiles{"database", "database-wal", "database-shm"};
0084 
0085     {
0086         QDir dir;
0087         dir.mkpath(databaseDirectoryPath);
0088         dir.mkpath(databaseTestBackupDirectoryPath);
0089         dir.mkpath(databaseWorkingBackupDirectoryPath);
0090 
0091         if (!dir.exists(databaseDirectoryPath) || !dir.exists(databaseTestBackupDirectoryPath) || !dir.exists(databaseWorkingBackupDirectoryPath)) {
0092             qCWarning(KAMD_LOG_RESOURCES) << "Database directory can not be created!";
0093             return;
0094         }
0095     }
0096 
0097     const QDir databaseDirectory(databaseDirectoryPath);
0098     const QDir databaseTestBackupDirectory(databaseTestBackupDirectoryPath);
0099     const QDir databaseWorkingBackupDirectory(databaseWorkingBackupDirectoryPath);
0100 
0101     auto removeDatabaseFiles = [](const QDir &dir) {
0102         return std::all_of(databaseFiles.begin(), databaseFiles.cend(), [&dir](const QString &fileName) {
0103             const auto filePath = dir.filePath(fileName);
0104             return !QFile::exists(filePath) || QFile::remove(filePath);
0105         });
0106     };
0107 
0108     auto copyDatabaseFiles = [removeDatabaseFiles](const QDir &fromDir, const QDir &toDir) {
0109         return removeDatabaseFiles(toDir) && std::all_of(databaseFiles.begin(), databaseFiles.cend(), [&fromDir, &toDir](const QString &fileName) {
0110                    const auto fromFilePath = fromDir.filePath(fileName);
0111                    const auto toFilePath = toDir.filePath(fileName);
0112                    return QFile::copy(fromFilePath, toFilePath);
0113                });
0114     };
0115 
0116     auto databaseFilesExistIn = [](const QDir &dir) {
0117         return dir.exists() && std::all_of(databaseFiles.begin(), databaseFiles.cend(), [&dir](const QString &fileName) {
0118                    const auto filePath = dir.filePath(fileName);
0119                    return QFile::exists(filePath);
0120                });
0121     };
0122 
0123     // First, let's move the files from `resources-test-backup` to
0124     // `resources-working-backup` (if they exist)
0125     if (databaseFilesExistIn(databaseTestBackupDirectory)) {
0126         qCDebug(KAMD_LOG_RESOURCES) << "Marking the test backup as working...";
0127         if (copyDatabaseFiles(databaseTestBackupDirectory, databaseWorkingBackupDirectory)) {
0128             removeDatabaseFiles(databaseTestBackupDirectory);
0129         } else {
0130             qCWarning(KAMD_LOG_RESOURCES) << "Marking the test backup as working failed!";
0131             removeDatabaseFiles(databaseWorkingBackupDirectory);
0132         }
0133     }
0134 
0135     // Next, copy the current database files to `resources-test-backup`
0136     if (databaseFilesExistIn(databaseDirectory)) {
0137         qCDebug(KAMD_LOG_RESOURCES) << "Creating the backup of the current database...";
0138         if (!copyDatabaseFiles(databaseDirectory, databaseTestBackupDirectory)) {
0139             qCWarning(KAMD_LOG_RESOURCES) << "Creating the backup of the current database failed!";
0140             removeDatabaseFiles(databaseTestBackupDirectory);
0141         }
0142     }
0143 
0144     // Now we can try to open the database
0145     d->database = Common::Database::instance(Common::Database::ResourcesDatabase, Common::Database::ReadWrite);
0146 
0147     if (d->database) {
0148         qCDebug(KAMD_LOG_RESOURCES) << "Database opened successfully";
0149         QObject::connect(d->database.get(), &Common::Database::error, d->database.get(), [databaseTestBackupDirectory, removeDatabaseFiles](const QSqlError &error) {
0150             const QString errorLog =
0151                 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/errors.log");
0152             QFile file(errorLog);
0153             if (file.open(QIODevice::Append)) {
0154                 QTextStream out(&file);
0155                 out << QDateTime::currentDateTime().toString(Qt::ISODate) << " error: " << error.text() << "\n";
0156             } else {
0157                 qCWarning(KAMD_LOG_RESOURCES) << QDateTime::currentDateTime().toString(Qt::ISODate) << " error: " << error.text();
0158             }
0159 
0160             removeDatabaseFiles(databaseTestBackupDirectory);
0161         });
0162         Common::ResourcesDatabaseSchema::initSchema(*d->database);
0163 
0164     } else {
0165         // The current database can not be opened, delete the
0166         // backup we just created
0167         removeDatabaseFiles(databaseTestBackupDirectory);
0168 
0169         if (databaseFilesExistIn(databaseWorkingBackupDirectoryPath)) {
0170             qCWarning(KAMD_LOG_RESOURCES) << "The database seems to be corrupted, trying to load the latest working version";
0171 
0172             const auto success = copyDatabaseFiles(databaseWorkingBackupDirectory, databaseDirectory);
0173 
0174             if (success && retryOnFail) {
0175                 // Avoid infinite recursion
0176                 initDatabase(false);
0177             }
0178 
0179         } else {
0180             qCWarning(KAMD_LOG_RESOURCES) << "The database might be corrupted and there is no working backup";
0181         }
0182     }
0183 }
0184 
0185 ResourcesDatabaseInitializer::ResourcesDatabaseInitializer()
0186 {
0187     initDatabase(true);
0188 }
0189 
0190 ResourcesDatabaseInitializer::~ResourcesDatabaseInitializer()
0191 {
0192 }