Database.cpp 8.17 KB
Newer Older
Ivan Čukić's avatar
Ivan Čukić committed
1
/*
2
 *   SPDX-FileCopyrightText: 2011, 2012 Ivan Cukic <ivan.cukic(at)kde.org>
Ivan Čukić's avatar
Ivan Čukić committed
3
 *
4
 *   SPDX-License-Identifier: GPL-2.0-or-later
Ivan Čukić's avatar
Ivan Čukić committed
5 6
 */

Ivan Čukić's avatar
Ivan Čukić committed
7 8
// Self
#include <kactivities-features.h>
Ivan Čukić's avatar
Ivan Čukić committed
9
#include "Database.h"
Ivan Čukić's avatar
Ivan Čukić committed
10

Ivan Čukić's avatar
Ivan Čukić committed
11
// Qt
Ivan Čukić's avatar
Ivan Čukić committed
12
#include <QDir>
Ivan Čukić's avatar
Ivan Čukić committed
13 14
#include <QSqlDatabase>
#include <QSqlError>
Ivan Čukić's avatar
Ivan Čukić committed
15
#include <QSqlQuery>
Ivan Čukić's avatar
Ivan Čukić committed
16
#include <QStandardPaths>
Ivan Čukić's avatar
Ivan Čukić committed
17
#include <QVariant>
Ivan Čukić's avatar
Ivan Čukić committed
18

Ivan Čukić's avatar
Ivan Čukić committed
19 20 21
// KDE
#include <kdelibs4migration.h>

Ivan Čukić's avatar
Ivan Čukić committed
22
// Utils
23
#include <utils/d_ptr_implementation.h>
24
#include <utils/qsqlquery_iterator.h>
25

Ivan Čukić's avatar
Ivan Čukić committed
26 27
// System
#include <cmath>
28
#include <memory>
29
#include <array>
Ivan Čukić's avatar
Ivan Čukić committed
30 31

// Local
32
#include "DebugResources.h"
33
#include "Utils.h"
Ivan Čukić's avatar
Ivan Čukić committed
34

Ivan Čukić's avatar
Ivan Čukić committed
35 36
#include <common/database/Database.h>
#include <common/database/schema/ResourcesDatabaseSchema.h>
Ivan Čukić's avatar
Ivan Čukić committed
37

Ivan Čukić's avatar
Ivan Čukić committed
38
class ResourcesDatabaseInitializer::Private {
Ivan Čukić's avatar
Ivan Čukić committed
39 40
public:
    Common::Database::Ptr database;
Ivan Čukić's avatar
Ivan Čukić committed
41 42 43

};

44
Common::Database::Ptr resourcesDatabase()
Ivan Čukić's avatar
Ivan Čukić committed
45
{
Ivan Čukić's avatar
Ivan Čukić committed
46
    static ResourcesDatabaseInitializer instance;
47
    return instance.d->database;
Ivan Čukić's avatar
Ivan Čukić committed
48 49
}

Ivan Čukić's avatar
Ivan Čukić committed
50
void ResourcesDatabaseInitializer::initDatabase(bool retryOnFail)
Ivan Čukić's avatar
Ivan Čukić committed
51
{
Ivan Čukić's avatar
Ivan Čukić committed
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
    //
    // There are three situations we want to handle:
    // 1. The database can not be opened at all.
    //    This means that the current database files have
    //    been corrupted and that we need to replace them
    //    with the last working backup.
    // 2. The database was opened, but an error appeared
    //    somewhere at runtime.
    // 3. The database was successfully opened and no errors
    //    appeared during runtime.
    //
    // To achieve this, we will have three locations for
    // database files:
    //
    // 1. `resources` - the current database files
    // 2. `resources-test-backup` - at each KAMD start,
    //    we copy the current database files here.
    //    If an error appears during execution, the files
    //    will be removed and the error will be added to
    //    the log file `resources/errors.log`
    // 3. `resources-working-backup` - on each KAMD start,
    //    if there are files in `resources-test-backup`
    //    (meaning no error appeared at runtime), they
    //    will be copied to `resources-working-backup`.
    //
    // This means that the `working` backup will be a bit
    // older, but it will be the last database that produced
    // no errors at runtime.
    //


    const QString databaseDirectoryPath
Ivan Čukić's avatar
Ivan Čukić committed
84
        = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
85
          + QStringLiteral("/kactivitymanagerd/resources/");
Ivan Čukić's avatar
Ivan Čukić committed
86

Ivan Čukić's avatar
Ivan Čukić committed
87 88 89 90 91 92 93 94
    const QString databaseTestBackupDirectoryPath
        = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
          + QStringLiteral("/kactivitymanagerd/resources/test-backup/");

    const QString databaseWorkingBackupDirectoryPath
        = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
          + QStringLiteral("/kactivitymanagerd/resources/working-backup/");

95
    static const std::array<QString, 3> databaseFiles{"database", "database-wal", "database-shm"};
Ivan Čukić's avatar
Ivan Čukić committed
96 97 98 99 100 101 102 103 104 105 106 107 108 109

    {
        QDir dir;
        dir.mkpath(databaseDirectoryPath);
        dir.mkpath(databaseTestBackupDirectoryPath);
        dir.mkpath(databaseWorkingBackupDirectoryPath);

        if (!dir.exists(databaseDirectoryPath) ||
            !dir.exists(databaseTestBackupDirectoryPath) ||
            !dir.exists(databaseWorkingBackupDirectoryPath)) {
            qCWarning(KAMD_LOG_RESOURCES) << "Database directory can not be created!";
            return;
        }
    }
110

Ivan Čukić's avatar
Ivan Čukić committed
111 112 113 114
    const QDir databaseDirectory(databaseDirectoryPath);
    const QDir databaseTestBackupDirectory(databaseTestBackupDirectoryPath);
    const QDir databaseWorkingBackupDirectory(databaseWorkingBackupDirectoryPath);

115
    auto removeDatabaseFiles = [] (const QDir &dir) {
Ivan Čukić's avatar
Ivan Čukić committed
116
        return std::all_of(databaseFiles.begin(), databaseFiles.cend(),
117
                           [&dir] (const QString &fileName) {
Ivan Čukić's avatar
Ivan Čukić committed
118 119 120 121 122
                               const auto filePath = dir.filePath(fileName);
                               return !QFile::exists(filePath) || QFile::remove(filePath);
                            });
    };

123
    auto copyDatabaseFiles = [removeDatabaseFiles] (const QDir &fromDir, const QDir& toDir) {
Ivan Čukić's avatar
Ivan Čukić committed
124 125
        return removeDatabaseFiles(toDir) &&
               std::all_of(databaseFiles.begin(), databaseFiles.cend(),
126
                           [&fromDir, &toDir] (const QString &fileName) {
Ivan Čukić's avatar
Ivan Čukić committed
127 128 129 130 131 132
                               const auto fromFilePath = fromDir.filePath(fileName);
                               const auto toFilePath = toDir.filePath(fileName);
                               return QFile::copy(fromFilePath, toFilePath);
                            });
    };

133
    auto databaseFilesExistIn = [] (const QDir &dir) {
Ivan Čukić's avatar
Ivan Čukić committed
134 135
        return dir.exists() &&
               std::all_of(databaseFiles.begin(), databaseFiles.cend(),
136
                           [&dir] (const QString &fileName) {
Ivan Čukić's avatar
Ivan Čukić committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
                               const auto filePath = dir.filePath(fileName);
                               return QFile::exists(filePath);
                           });
    };

    // First, let's move the files from `resources-test-backup` to
    // `resources-working-backup` (if they exist)
    if (databaseFilesExistIn(databaseTestBackupDirectory)) {
        qCDebug(KAMD_LOG_RESOURCES) << "Marking the test backup as working...";
        if (copyDatabaseFiles(databaseTestBackupDirectory, databaseWorkingBackupDirectory)) {
            removeDatabaseFiles(databaseTestBackupDirectory);
        } else {
            qCWarning(KAMD_LOG_RESOURCES) << "Marking the test backup as working failed!";
            removeDatabaseFiles(databaseWorkingBackupDirectory);
        }
152
    }
Ivan Čukić's avatar
Ivan Čukić committed
153

Ivan Čukić's avatar
Ivan Čukić committed
154 155 156 157 158 159 160 161 162 163
    // Next, copy the current database files to `resources-test-backup`
    if (databaseFilesExistIn(databaseDirectory)) {
        qCDebug(KAMD_LOG_RESOURCES) << "Creating the backup of the current database...";
        if (!copyDatabaseFiles(databaseDirectory, databaseTestBackupDirectory)) {
            qCWarning(KAMD_LOG_RESOURCES) << "Creating the backup of the current database failed!";
            removeDatabaseFiles(databaseTestBackupDirectory);
        }
    }

    // Now we can try to open the database
Ivan Čukić's avatar
Ivan Čukić committed
164 165 166
    d->database = Common::Database::instance(
            Common::Database::ResourcesDatabase,
            Common::Database::ReadWrite);
Ivan Čukić's avatar
Ivan Čukić committed
167

168
    if (d->database) {
Ivan Čukić's avatar
Ivan Čukić committed
169 170
        qCDebug(KAMD_LOG_RESOURCES) << "Database opened successfully";
        QObject::connect(d->database.get(), &Common::Database::error,
171
                         [databaseTestBackupDirectory, removeDatabaseFiles] (const QSqlError &error) {
Ivan Čukić's avatar
Ivan Čukić committed
172 173 174 175 176 177 178 179 180 181 182 183 184
                             const QString errorLog =
                                 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
                                     + QStringLiteral("/kactivitymanagerd/resources/errors.log");
                             QFile file(errorLog);
                             if (file.open(QIODevice::Append)) {
                                 QTextStream out(&file);
                                 out << QDateTime::currentDateTime().toString(Qt::ISODate) << " error: " << error.text() << "\n";
                             } else {
                                 qCWarning(KAMD_LOG_RESOURCES) << QDateTime::currentDateTime().toString(Qt::ISODate) << " error: " << error.text();
                             }

                             removeDatabaseFiles(databaseTestBackupDirectory);
                         });
185
        Common::ResourcesDatabaseSchema::initSchema(*d->database);
Ivan Čukić's avatar
Ivan Čukić committed
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205

    } else {
        // The current database can not be opened, delete the
        // backup we just created
        removeDatabaseFiles(databaseTestBackupDirectory);

        if (databaseFilesExistIn(databaseWorkingBackupDirectoryPath)) {
            qCWarning(KAMD_LOG_RESOURCES) << "The database seems to be corrupted, trying to load the latest working version";

            const auto success = copyDatabaseFiles(databaseWorkingBackupDirectory, databaseDirectory);

            if (success && retryOnFail) {
                // Avoid infinite recursion
                initDatabase(false);
            }

        } else {
            qCWarning(KAMD_LOG_RESOURCES) << "The database might be corrupted and there is no working backup";

        }
206
    }
Ivan Čukić's avatar
Ivan Čukić committed
207 208
}

Ivan Čukić's avatar
Ivan Čukić committed
209 210 211 212 213 214
ResourcesDatabaseInitializer::ResourcesDatabaseInitializer()
{
    initDatabase(true);
}

ResourcesDatabaseInitializer::~ResourcesDatabaseInitializer()
Ivan Čukić's avatar
Ivan Čukić committed
215 216 217
{
}