diff --git a/src/common/database/Database.cpp b/src/common/database/Database.cpp index 5b2aebd..3f9f89d 100644 --- a/src/common/database/Database.cpp +++ b/src/common/database/Database.cpp @@ -1,295 +1,314 @@ /* * Copyright (C) 2014 - 2016 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Database.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "DebugResources.h" namespace Common { namespace { #ifdef QT_DEBUG QString lastExecutedQuery; #endif std::mutex databases_mutex; struct DatabaseInfo { Qt::HANDLE thread; Database::OpenMode openMode; }; bool operator<(const DatabaseInfo &left, const DatabaseInfo &right) { return left.thread < right.thread ? true : left.thread > right.thread ? false : left.openMode < right.openMode; } std::map> databases; } class QSqlDatabaseWrapper { private: QSqlDatabase m_database; bool m_open; QString m_connectionName; public: QSqlDatabaseWrapper(const DatabaseInfo &info) : m_open(false) { m_connectionName = "kactivities_db_resources_" // Adding the thread number to the database name + QString::number((quintptr)info.thread) // And whether it is read-only or read-write + (info.openMode == Database::ReadOnly ? "_readonly" : "_readwrite"); m_database = QSqlDatabase::contains(m_connectionName) ? QSqlDatabase::database(m_connectionName) : QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName); if (info.openMode == Database::ReadOnly) { m_database.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY")); } // We are allowing the database file to be overridden mostly for testing purposes m_database.setDatabaseName(ResourcesDatabaseSchema::path()); m_open = m_database.open(); if (!m_open) { qCWarning(KAMD_LOG_RESOURCES) << "KActivities: Database is not open: " << m_database.connectionName() << m_database.databaseName() << m_database.lastError(); if (info.openMode == Database::ReadWrite) { qFatal("KActivities: Opening the database in RW mode should always succeed"); } } } ~QSqlDatabaseWrapper() { qCDebug(KAMD_LOG_RESOURCES) << "Closing SQL connection: " << m_connectionName; } + void close() + { + m_database.close(); + } + QSqlDatabase &get() { return m_database; } bool isOpen() const { return m_open; } QString connectionName() const { return m_connectionName; } }; class Database::Private { public: Private() { } QSqlQuery query(const QString &query) { return database ? QSqlQuery(query, database->get()) : QSqlQuery(); } QSqlQuery query() { return database ? QSqlQuery(database->get()) : QSqlQuery(); } QScopedPointer database; }; Database::Locker::Locker(Database &database) : m_database(database.d->database->get()) { m_database.transaction(); } Database::Locker::~Locker() { m_database.commit(); } Database::Ptr Database::instance(Source source, OpenMode openMode) { Q_UNUSED(source) // for the time being std::lock_guard lock(databases_mutex); // We are saving instances per thread and per read/write mode DatabaseInfo info; info.thread = QThread::currentThreadId(); info.openMode = openMode; // Do we have an instance matching the request? auto search = databases.find(info); if (search != databases.end()) { auto ptr = search->second.lock(); if (ptr) { return ptr; } } // Creating a new database instance auto ptr = std::make_shared(); ptr->d->database.reset(new QSqlDatabaseWrapper(info)); if (!ptr->d->database->isOpen()) { return nullptr; } databases[info] = ptr; if (info.openMode == ReadOnly) { // From now on, only SELECT queries will work ptr->setPragma(QStringLiteral("query_only = 1")); // These should not make any difference ptr->setPragma(QStringLiteral("synchronous = 0")); } else { // Using the write-ahead log and sync = NORMAL for faster writes ptr->setPragma(QStringLiteral("synchronous = 1")); } // Maybe we should use the write-ahead log auto walResult = ptr->pragma(QStringLiteral("journal_mode = WAL")); if (walResult != "wal") { qCWarning(KAMD_LOG_RESOURCES) << "KActivities: Database can not be opened in WAL mode. Check the " "SQLite version (required >3.7.0). And whether your filesystem " "supports shared memory"; + + ptr->d->database->close(); + return nullptr; } // We don't have a big database, lets flush the WAL when // it reaches 400k, not 4M as is default ptr->setPragma(QStringLiteral("wal_autocheckpoint = 100")); qCDebug(KAMD_LOG_RESOURCES) << "KActivities: Database connection: " << ptr->d->database->connectionName() << "\n query_only: " << ptr->pragma(QStringLiteral("query_only")) << "\n journal_mode: " << ptr->pragma(QStringLiteral("journal_mode")) << "\n wal_autocheckpoint: " << ptr->pragma(QStringLiteral("wal_autocheckpoint")) << "\n synchronous: " << ptr->pragma(QStringLiteral("synchronous")) ; return ptr; } Database::Database() { } Database::~Database() { } QSqlQuery Database::createQuery() const { return d->query(); } +void Database::reportError(const QSqlError &error_) +{ + Q_EMIT error(error_); +} + QString Database::lastQuery() const { #ifdef QT_DEBUG return lastExecutedQuery; #endif return QString(); } QSqlQuery Database::execQuery(const QString &query, bool ignoreErrors) const { Q_UNUSED(ignoreErrors); #ifdef QT_NO_DEBUG - return d->query(query); + auto result = d->query(query); + + if (!ignoreErrors && result.lastError().isValid()) { + Q_EMIT error(result.lastError()); + } + + return result; #else auto result = d->query(query); lastExecutedQuery = query; if (!ignoreErrors && result.lastError().isValid()) { qCWarning(KAMD_LOG_RESOURCES) << "SQL: " << "\n error: " << result.lastError() << "\n query: " << query; } return result; #endif } QSqlQuery Database::execQueries(const QStringList &queries) const { QSqlQuery result; for (const auto &query: queries) { result = execQuery(query); } return result; } void Database::setPragma(const QString &pragma) { execQuery(QStringLiteral("PRAGMA ") + pragma); } QVariant Database::pragma(const QString &pragma) const { return value("PRAGMA " + pragma); } QVariant Database::value(const QString &query) const { auto result = execQuery(query); return result.next() ? result.value(0) : QVariant(); } } // namespace Common diff --git a/src/common/database/Database.h b/src/common/database/Database.h index 38e6d09..cf55e36 100644 --- a/src/common/database/Database.h +++ b/src/common/database/Database.h @@ -1,152 +1,160 @@ /* * Copyright 2014 Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef COMMON_DATABASE_H #define COMMON_DATABASE_H #include #include #include #include +#include namespace Common { -class Database { +class Database: public QObject { + Q_OBJECT + public: typedef std::shared_ptr Ptr; enum Source { ResourcesDatabase }; enum OpenMode { ReadWrite, ReadOnly }; static Ptr instance(Source source, OpenMode openMode); QSqlQuery execQueries(const QStringList &queries) const; QSqlQuery execQuery(const QString &query, bool ignoreErrors = false) const; QSqlQuery createQuery() const; void setPragma(const QString &pragma); QVariant pragma(const QString &pragma) const; QVariant value(const QString &query) const; // For debugging purposes only QString lastQuery() const; ~Database(); Database(); friend class Locker; class Locker { public: explicit Locker(Database &database); ~Locker(); private: QSqlDatabase &m_database; }; + void reportError(const QSqlError &error); + #define DATABASE_TRANSACTION(A) \ /* enable this for debugging only: qCDebug(KAMD_LOG_RESOURCES) << "Location:" << __FILE__ << __LINE__; */ \ Common::Database::Locker lock(A) +Q_SIGNALS: + void error(const QSqlError &error) const; + private: D_PTR; }; template QString parseStarPattern(const QString &pattern, const QString &joker, EscapeFunction escape) { const auto begin = pattern.constBegin(); const auto end = pattern.constEnd(); auto currentStart = pattern.constBegin(); auto currentPosition = pattern.constBegin(); bool isEscaped = false; // This should be available in the QString class... auto stringFromIterators = [&](const QString::const_iterator ¤tStart, const QString::const_iterator ¤tPosition) { return pattern.mid( std::distance(begin, currentStart), std::distance(currentStart, currentPosition)); }; // Escaping % and _ for sql like // auto escape = [] (QString str) { // return str.replace("%", "\\%").replace("_", "\\_"); // }; QString resultPattern; resultPattern.reserve(pattern.size() * 1.5); for (; currentPosition != end; ++currentPosition) { if (isEscaped) { // Just skip the current character isEscaped = false; } else if (*currentPosition == QLatin1Char('\\')) { // Skip two characters isEscaped = true; } else if (*currentPosition == QLatin1Char('*')) { // Replacing the star with the sql like joker - % resultPattern.append(escape(stringFromIterators( currentStart, currentPosition)) + joker); currentStart = currentPosition + 1; } else { // This one is boring, nothing to do } } if (currentStart != currentPosition) { resultPattern.append(escape(stringFromIterators( currentStart, currentPosition))); } return resultPattern; } inline QString escapeSqliteLikePattern(QString pattern) { return pattern.replace(QLatin1String("%"), QLatin1String("\\%")).replace(QLatin1String("_"), QLatin1String("\\_")); } inline QString starPatternToLike(const QString &pattern) { return parseStarPattern(pattern, QStringLiteral("%"), escapeSqliteLikePattern); } inline QRegExp starPatternToRegex(const QString &pattern) { return QRegExp(parseStarPattern(pattern, QStringLiteral(".*"), QRegExp::escape)); } } // namespace Common #endif // COMMON_DATABASE_H diff --git a/src/common/database/schema/ResourcesDatabaseSchema.cpp b/src/common/database/schema/ResourcesDatabaseSchema.cpp index c1d1be2..38aec84 100644 --- a/src/common/database/schema/ResourcesDatabaseSchema.cpp +++ b/src/common/database/schema/ResourcesDatabaseSchema.cpp @@ -1,205 +1,217 @@ /* * Copyright (C) 2015 - 2016 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourcesDatabaseSchema.h" #include #include #include namespace Common { namespace ResourcesDatabaseSchema { const QString name = QStringLiteral("Resources"); QString version() { return QStringLiteral("2015.02.09"); } QStringList schema() { // If only we could use initializer lists here ... return QStringList() << // Schema information table, used for versioning QStringLiteral("CREATE TABLE IF NOT EXISTS SchemaInfo (" "key text PRIMARY KEY, value text" ")") << QStringLiteral("INSERT OR IGNORE INTO schemaInfo VALUES ('version', '%1')").arg(version()) << QStringLiteral("UPDATE schemaInfo SET value = '%1' WHERE key = 'version'").arg(version()) << // The ResourceEvent table saves the Opened/Closed event pairs for // a resource. The Accessed event is mapped to those. // Focusing events are not stored in order not to get a // huge database file and to lessen writes to the disk. QStringLiteral("CREATE TABLE IF NOT EXISTS ResourceEvent (" "usedActivity TEXT, " "initiatingAgent TEXT, " "targettedResource TEXT, " "start INTEGER, " "end INTEGER " ")") << // The ResourceScoreCache table stores the calculated scores // for resources based on the recorded events. QStringLiteral("CREATE TABLE IF NOT EXISTS ResourceScoreCache (" "usedActivity TEXT, " "initiatingAgent TEXT, " "targettedResource TEXT, " "scoreType INTEGER, " "cachedScore FLOAT, " "firstUpdate INTEGER, " "lastUpdate INTEGER, " "PRIMARY KEY(usedActivity, initiatingAgent, targettedResource)" ")") << // @since 2014.05.05 // The ResourceLink table stores the information, formerly kept by // Nepomuk, of which resources are linked to which activities. // The additional features compared to the old days are // the ability to limit the link to specific applications, and // to create global links. QStringLiteral("CREATE TABLE IF NOT EXISTS ResourceLink (" "usedActivity TEXT, " "initiatingAgent TEXT, " "targettedResource TEXT, " "PRIMARY KEY(usedActivity, initiatingAgent, targettedResource)" ")") << // @since 2015.01.18 // The ResourceInfo table stores the collected information about a // resource that is not agent nor activity related like the // title and the mime type. // If these are automatically retrieved (works for files), the // flag is set to true. This is done for the agents to be able to // override these. QStringLiteral("CREATE TABLE IF NOT EXISTS ResourceInfo (" "targettedResource TEXT, " "title TEXT, " "mimetype TEXT, " "autoTitle INTEGER, " "autoMimetype INTEGER, " "PRIMARY KEY(targettedResource)" ")") ; } // TODO: This will require some refactoring after we introduce more databases QString defaultPath() { return QStandardPaths::writableLocation( QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/database"); } const char *overrideFlagProperty = "org.kde.KActivities.ResourcesDatabase.overrideDatabase"; const char *overrideFileProperty = "org.kde.KActivities.ResourcesDatabase.overrideDatabaseFile"; QString path() { auto app = QCoreApplication::instance(); return (app->property(overrideFlagProperty).toBool()) ? app->property(overrideFileProperty).toString() : defaultPath(); } void overridePath(const QString &path) { auto app = QCoreApplication::instance(); app->setProperty(overrideFlagProperty, true); app->setProperty(overrideFileProperty, path); } void initSchema(Database &database) { QString dbSchemaVersion; auto query = database.execQuery( QStringLiteral("SELECT value FROM SchemaInfo WHERE key = 'version'"), /* ignore error */ true); if (query.next()) { dbSchemaVersion = query.value(0).toString(); } // Early bail-out if the schema is up-to-date if (dbSchemaVersion == version()) { return; } // Transition to KF5: // We left the world of Nepomuk, and all the ontologies went // across the sea to the Undying Lands. // This needs to be done before executing the schema() queries // so that we do not create new (empty) tables and block these // queries from being executed. if (dbSchemaVersion < QStringLiteral("2014.04.14")) { database.execQuery( QStringLiteral("ALTER TABLE nuao_DesktopEvent RENAME TO ResourceEvent"), /* ignore error */ true); database.execQuery( QStringLiteral("ALTER TABLE kext_ResourceScoreCache RENAME TO ResourceScoreCache"), /* ignore error */ true); } database.execQueries(ResourcesDatabaseSchema::schema()); + // We are asking for trouble. If the database is corrupt, + // some of these should fail. + // WARNING: Sqlite specific! + database.execQueries(QStringList{ + ".tables", + "SELECT count(*) FROM SchemaInfo", + "SELECT count(*) FROM ResourceEvent", + "SELECT count(*) FROM ResourceScoreCache", + "SELECT count(*) FROM ResourceLink", + "SELECT count(*) FROM ResourceInfo" + }); + // We can not allow empty fields for activity and agent, they need to // be at least magic values. These do not change the structure // of the database, but the old data. if (dbSchemaVersion < QStringLiteral("2015.02.09")) { const QString updateActivity = QStringLiteral("SET usedActivity=':global' " "WHERE usedActivity IS NULL OR usedActivity = ''"); const QString updateAgent = QStringLiteral("SET initiatingAgent=':global' " "WHERE initiatingAgent IS NULL OR initiatingAgent = ''"); // When the activity field was empty, it meant the file was // linked to all activities (aka :global) database.execQuery("UPDATE ResourceLink " + updateActivity); // When the agent field was empty, it meant the file was not // linked to a specified agent (aka :global) database.execQuery("UPDATE ResourceLink " + updateAgent); // These were not supposed to be empty, but in the case they were, // deal with them as well database.execQuery("UPDATE ResourceEvent " + updateActivity); database.execQuery("UPDATE ResourceEvent " + updateAgent); database.execQuery("UPDATE ResourceScoreCache " + updateActivity); database.execQuery("UPDATE ResourceScoreCache " + updateAgent); } } } // namespace Common } // namespace ResourcesDatabaseSchema diff --git a/src/service/plugins/sqlite/Database.cpp b/src/service/plugins/sqlite/Database.cpp index 58a730d..9dfea35 100644 --- a/src/service/plugins/sqlite/Database.cpp +++ b/src/service/plugins/sqlite/Database.cpp @@ -1,87 +1,229 @@ /* * Copyright (C) 2011, 2012 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include #include "Database.h" // Qt #include #include #include #include #include #include // KDE #include // Utils #include #include // System #include #include // Local #include "DebugResources.h" #include "Utils.h" #include #include -class ResourcesDatabaseMigrator::Private { +class ResourcesDatabaseInitializer::Private { public: Common::Database::Ptr database; }; Common::Database::Ptr resourcesDatabase() { - static ResourcesDatabaseMigrator instance; + static ResourcesDatabaseInitializer instance; return instance.d->database; } -ResourcesDatabaseMigrator::ResourcesDatabaseMigrator() +void ResourcesDatabaseInitializer::initDatabase(bool retryOnFail) { - const QString databaseDir + // + // 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 = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/"); - qCDebug(KAMD_LOG_RESOURCES) << "Creating directory: " << databaseDir; - auto created = QDir().mkpath(databaseDir); + const QString databaseTestBackupDirectoryPath + = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + + QStringLiteral("/kactivitymanagerd/resources/test-backup/"); + + const QString databaseWorkingBackupDirectoryPath + = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + + QStringLiteral("/kactivitymanagerd/resources/working-backup/"); + + const QStringList databaseFiles{"database", "database-wal", "database-shm"}; + + { + 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; + } + } - if (!created || !QDir(databaseDir).exists()) { - qCWarning(KAMD_LOG_RESOURCES) << "Database folder can not be created!"; + const QDir databaseDirectory(databaseDirectoryPath); + const QDir databaseTestBackupDirectory(databaseTestBackupDirectoryPath); + const QDir databaseWorkingBackupDirectory(databaseWorkingBackupDirectoryPath); + + auto removeDatabaseFiles = [&] (const QDir &dir) { + return std::all_of(databaseFiles.begin(), databaseFiles.cend(), + [&] (const QString &fileName) { + const auto filePath = dir.filePath(fileName); + return !QFile::exists(filePath) || QFile::remove(filePath); + }); + }; + + auto copyDatabaseFiles = [&] (const QDir &fromDir, const QDir& toDir) { + return removeDatabaseFiles(toDir) && + std::all_of(databaseFiles.begin(), databaseFiles.cend(), + [&] (const QString &fileName) { + const auto fromFilePath = fromDir.filePath(fileName); + const auto toFilePath = toDir.filePath(fileName); + return QFile::copy(fromFilePath, toFilePath); + }); + }; + + auto databaseFilesExistIn = [&] (const QDir &dir) { + return dir.exists() && + std::all_of(databaseFiles.begin(), databaseFiles.cend(), + [&] (const QString &fileName) { + 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); + } } + // 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 d->database = Common::Database::instance( Common::Database::ResourcesDatabase, Common::Database::ReadWrite); if (d->database) { + qCDebug(KAMD_LOG_RESOURCES) << "Database opened successfully"; + QObject::connect(d->database.get(), &Common::Database::error, + [=] (const QSqlError &error) { + 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); + }); Common::ResourcesDatabaseSchema::initSchema(*d->database); + + } 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"; + + } } } -ResourcesDatabaseMigrator::~ResourcesDatabaseMigrator() +ResourcesDatabaseInitializer::ResourcesDatabaseInitializer() +{ + initDatabase(true); +} + +ResourcesDatabaseInitializer::~ResourcesDatabaseInitializer() { } diff --git a/src/service/plugins/sqlite/Database.h b/src/service/plugins/sqlite/Database.h index c026c73..6e548d4 100644 --- a/src/service/plugins/sqlite/Database.h +++ b/src/service/plugins/sqlite/Database.h @@ -1,63 +1,61 @@ /* * Copyright (C) 2011, 2012 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PLUGINS_SQLITE_RESOURCESDATABASE_H #define PLUGINS_SQLITE_RESOURCESDATABASE_H // Qt #include #include #include #include #include #include // Utils #include // Local #include class QDateTime; class QSqlError; namespace Common { class Database; } // namespace Common -class ResourcesDatabaseMigrator : public QObject { - Q_OBJECT - +class ResourcesDatabaseInitializer { public: // static Database *self(); private: - ResourcesDatabaseMigrator(); - ~ResourcesDatabaseMigrator() override; + ResourcesDatabaseInitializer(); + ~ResourcesDatabaseInitializer(); - void migrateDatabase(const QString &newDatabaseFile) const; + void initDatabase(bool retryOnFail = true); D_PTR; friend Common::Database::Ptr resourcesDatabase(); }; Common::Database::Ptr resourcesDatabase(); #endif // PLUGINS_SQLITE_RESOURCESDATABASE_H diff --git a/src/service/plugins/sqlite/ResourceLinking.cpp b/src/service/plugins/sqlite/ResourceLinking.cpp index 345df7f..aa573b2 100644 --- a/src/service/plugins/sqlite/ResourceLinking.cpp +++ b/src/service/plugins/sqlite/ResourceLinking.cpp @@ -1,316 +1,316 @@ /* * Copyright (C) 2011, 2012, 2013, 2014, 2015 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include #include "ResourceLinking.h" // Qt #include #include #include // KDE #include #include // Boost #include #include // Local #include "DebugResources.h" #include "Database.h" #include "Utils.h" #include "StatsPlugin.h" #include "resourcelinkingadaptor.h" ResourceLinking::ResourceLinking(QObject *parent) : QObject(parent) { new ResourcesLinkingAdaptor(this); QDBusConnection::sessionBus().registerObject( QStringLiteral("/ActivityManager/Resources/Linking"), this); } void ResourceLinking::init() { auto activities = StatsPlugin::self()->activitiesInterface(); connect(activities, SIGNAL(CurrentActivityChanged(QString)), this, SLOT(onCurrentActivityChanged(QString))); connect(activities, SIGNAL(ActivityAdded(QString)), this, SLOT(onActivityAdded(QString))); connect(activities, SIGNAL(ActivityRemoved(QString)), this, SLOT(onActivityRemoved(QString))); } void ResourceLinking::LinkResourceToActivity(QString initiatingAgent, QString targettedResource, QString usedActivity) { qCDebug(KAMD_LOG_RESOURCES) << "Linking " << targettedResource << " to " << usedActivity << " from " << initiatingAgent; if (!validateArguments(initiatingAgent, targettedResource, usedActivity)) { qCWarning(KAMD_LOG_RESOURCES) << "Invalid arguments" << initiatingAgent << targettedResource << usedActivity; return; } if (usedActivity == ":any") { usedActivity = ":global"; } Q_ASSERT_X(!initiatingAgent.isEmpty(), "ResourceLinking::LinkResourceToActivity", "Agent should not be empty"); Q_ASSERT_X(!usedActivity.isEmpty(), "ResourceLinking::LinkResourceToActivity", "Activity should not be empty"); Q_ASSERT_X(!targettedResource.isEmpty(), "ResourceLinking::LinkResourceToActivity", "Resource should not be empty"); Utils::prepare(*resourcesDatabase(), linkResourceToActivityQuery, QStringLiteral( "INSERT OR REPLACE INTO ResourceLink" " (usedActivity, initiatingAgent, targettedResource) " "VALUES ( " "COALESCE(:usedActivity,'')," "COALESCE(:initiatingAgent,'')," "COALESCE(:targettedResource,'')" ")" )); DATABASE_TRANSACTION(*resourcesDatabase()); - Utils::exec(Utils::FailOnError, *linkResourceToActivityQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *linkResourceToActivityQuery, ":usedActivity" , usedActivity, ":initiatingAgent" , initiatingAgent, ":targettedResource" , targettedResource ); if (!usedActivity.isEmpty()) { // qCDebug(KAMD_LOG_RESOURCES) << "Sending link event added: activities:/" << usedActivity; org::kde::KDirNotify::emitFilesAdded(QUrl(QStringLiteral("activities:/") + usedActivity)); if (usedActivity == StatsPlugin::self()->currentActivity()) { // qCDebug(KAMD_LOG_RESOURCES) << "Sending link event added: activities:/current"; org::kde::KDirNotify::emitFilesAdded( QUrl(QStringLiteral("activities:/current"))); } } emit ResourceLinkedToActivity(initiatingAgent, targettedResource, usedActivity); } void ResourceLinking::UnlinkResourceFromActivity(QString initiatingAgent, QString targettedResource, QString usedActivity) { // qCDebug(KAMD_LOG_RESOURCES) << "Unlinking " << targettedResource << " from " << usedActivity << " from " << initiatingAgent; if (!validateArguments(initiatingAgent, targettedResource, usedActivity)) { qCWarning(KAMD_LOG_RESOURCES) << "Invalid arguments" << initiatingAgent << targettedResource << usedActivity; return; } Q_ASSERT_X(!initiatingAgent.isEmpty(), "ResourceLinking::UnlinkResourceFromActivity", "Agent should not be empty"); Q_ASSERT_X(!usedActivity.isEmpty(), "ResourceLinking::UnlinkResourceFromActivity", "Activity should not be empty"); Q_ASSERT_X(!targettedResource.isEmpty(), "ResourceLinking::UnlinkResourceFromActivity", "Resource should not be empty"); QSqlQuery *query = nullptr; if (usedActivity == ":any") { Utils::prepare(*resourcesDatabase(), unlinkResourceFromAllActivitiesQuery, QStringLiteral( "DELETE FROM ResourceLink " "WHERE " "initiatingAgent = COALESCE(:initiatingAgent , '') AND " "targettedResource = COALESCE(:targettedResource, '') " )); query = unlinkResourceFromAllActivitiesQuery.get(); } else { Utils::prepare(*resourcesDatabase(), unlinkResourceFromActivityQuery, QStringLiteral( "DELETE FROM ResourceLink " "WHERE " "usedActivity = COALESCE(:usedActivity , '') AND " "initiatingAgent = COALESCE(:initiatingAgent , '') AND " "targettedResource = COALESCE(:targettedResource, '') " )); query = unlinkResourceFromActivityQuery.get(); } DATABASE_TRANSACTION(*resourcesDatabase()); - Utils::exec(Utils::FailOnError, *query, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *query, ":usedActivity" , usedActivity, ":initiatingAgent" , initiatingAgent, ":targettedResource" , targettedResource ); if (!usedActivity.isEmpty()) { // auto mangled = QString::fromUtf8(QUrl::toPercentEncoding(targettedResource)); auto mangled = QString::fromLatin1(targettedResource.toUtf8().toBase64( QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); // qCDebug(KAMD_LOG_RESOURCES) << "Sending link event removed: activities:/" << usedActivity << '/' << mangled; org::kde::KDirNotify::emitFilesRemoved( { QUrl(QStringLiteral("activities:/") + usedActivity + '/' + mangled) }); if (usedActivity == StatsPlugin::self()->currentActivity()) { // qCDebug(KAMD_LOG_RESOURCES) << "Sending link event removed: activities:/current/" << mangled; org::kde::KDirNotify::emitFilesRemoved({ QUrl(QStringLiteral("activities:/current/") + mangled) }); } } emit ResourceUnlinkedFromActivity(initiatingAgent, targettedResource, usedActivity); } bool ResourceLinking::IsResourceLinkedToActivity(QString initiatingAgent, QString targettedResource, QString usedActivity) { if (!validateArguments(initiatingAgent, targettedResource, usedActivity)) { return false; } Q_ASSERT_X(!initiatingAgent.isEmpty(), "ResourceLinking::IsResourceLinkedToActivity", "Agent should not be empty"); Q_ASSERT_X(!usedActivity.isEmpty(), "ResourceLinking::IsResourceLinkedToActivity", "Activity should not be empty"); Q_ASSERT_X(!targettedResource.isEmpty(), "ResourceLinking::IsResourceLinkedToActivity", "Resource should not be empty"); Utils::prepare(*resourcesDatabase(), isResourceLinkedToActivityQuery, QStringLiteral( "SELECT * FROM ResourceLink " "WHERE " "usedActivity = COALESCE(:usedActivity , '') AND " "initiatingAgent = COALESCE(:initiatingAgent , '') AND " "targettedResource = COALESCE(:targettedResource, '') " )); - Utils::exec(Utils::FailOnError, *isResourceLinkedToActivityQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *isResourceLinkedToActivityQuery, ":usedActivity" , usedActivity, ":initiatingAgent" , initiatingAgent, ":targettedResource" , targettedResource ); return isResourceLinkedToActivityQuery->next(); } bool ResourceLinking::validateArguments(QString &initiatingAgent, QString &targettedResource, QString &usedActivity) { // Validating targetted resource if (targettedResource.isEmpty()) { qCDebug(KAMD_LOG_RESOURCES) << "Resource is invalid -- empty"; return false; } if (targettedResource.startsWith(QStringLiteral("file://"))) { targettedResource = QUrl(targettedResource).toLocalFile(); } if (targettedResource.startsWith(QStringLiteral("/"))) { QFileInfo file(targettedResource); if (!file.exists()) { qCDebug(KAMD_LOG_RESOURCES) << "Resource is invalid -- the file does not exist"; return false; } targettedResource = file.canonicalFilePath(); } // Handling special values for the agent if (initiatingAgent.isEmpty()) { initiatingAgent = ":global"; } // Handling special values for activities if (usedActivity == ":current") { usedActivity = StatsPlugin::self()->currentActivity(); } else if (usedActivity.isEmpty()) { usedActivity = ":global"; } // If the activity is not empty and the passed activity // does not exist, cancel the request if (!usedActivity.isEmpty() && usedActivity != ":global" && usedActivity != ":any" && !StatsPlugin::self()->listActivities().contains(usedActivity)) { qCDebug(KAMD_LOG_RESOURCES) << "Activity is invalid, it does not exist"; return false; } // qCDebug(KAMD_LOG_RESOURCES) << "agent" << initiatingAgent // << "resource" << targettedResource // << "activity" << usedActivity; return true; } void ResourceLinking::onActivityAdded(const QString &activity) { Q_UNUSED(activity); // Notify KIO // qCDebug(KAMD_LOG_RESOURCES) << "Added: activities:/ (" << activity << ")"; org::kde::KDirNotify::emitFilesAdded(QUrl(QStringLiteral("activities:/"))); } void ResourceLinking::onActivityRemoved(const QString &activity) { // Notify KIO // qCDebug(KAMD_LOG_RESOURCES) << "Removed: activities:/" << activity; org::kde::KDirNotify::emitFilesRemoved( { QUrl(QStringLiteral("activities:/") + activity) }); // Remove statistics for the activity } void ResourceLinking::onCurrentActivityChanged(const QString &activity) { Q_UNUSED(activity); // Notify KIO // qCDebug(KAMD_LOG_RESOURCES) << "Changed: activities:/current -> " << activity; org::kde::KDirNotify::emitFilesAdded( { QUrl(QStringLiteral("activities:/current")) }); } diff --git a/src/service/plugins/sqlite/ResourceScoreCache.cpp b/src/service/plugins/sqlite/ResourceScoreCache.cpp index 5f4c3ee..81d29cd 100644 --- a/src/service/plugins/sqlite/ResourceScoreCache.cpp +++ b/src/service/plugins/sqlite/ResourceScoreCache.cpp @@ -1,263 +1,263 @@ /* * Copyright (C) 2011, 2012 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include #include "ResourceScoreCache.h" // STD #include // Utils #include #include // Local #include "DebugResources.h" #include "StatsPlugin.h" #include "Database.h" #include "Utils.h" class ResourceScoreCache::Queries { private: Queries() : createResourceScoreCacheQuery(resourcesDatabase()->createQuery()) , getResourceScoreCacheQuery(resourcesDatabase()->createQuery()) , updateResourceScoreCacheQuery(resourcesDatabase()->createQuery()) , getScoreAdditionQuery(resourcesDatabase()->createQuery()) { Utils::prepare(*resourcesDatabase(), createResourceScoreCacheQuery, QStringLiteral( "INSERT INTO ResourceScoreCache " "VALUES (:usedActivity, :initiatingAgent, :targettedResource, " "0, 0, " // type, score ":firstUpdate, " // lastUpdate ":firstUpdate)" )); Utils::prepare(*resourcesDatabase(), getResourceScoreCacheQuery, QStringLiteral( "SELECT cachedScore, lastUpdate, firstUpdate FROM ResourceScoreCache " "WHERE " ":usedActivity = usedActivity AND " ":initiatingAgent = initiatingAgent AND " ":targettedResource = targettedResource " )); Utils::prepare(*resourcesDatabase(), updateResourceScoreCacheQuery, QStringLiteral( "UPDATE ResourceScoreCache SET " "cachedScore = :cachedScore, " "lastUpdate = :lastUpdate " "WHERE " ":usedActivity = usedActivity AND " ":initiatingAgent = initiatingAgent AND " ":targettedResource = targettedResource " )); Utils::prepare(*resourcesDatabase(), getScoreAdditionQuery, QStringLiteral( "SELECT start, end " "FROM ResourceEvent " "WHERE " ":usedActivity = usedActivity AND " ":initiatingAgent = initiatingAgent AND " ":targettedResource = targettedResource AND " "start > :start " "ORDER BY " "start ASC" )); } public: QSqlQuery createResourceScoreCacheQuery; QSqlQuery getResourceScoreCacheQuery; QSqlQuery updateResourceScoreCacheQuery; QSqlQuery getScoreAdditionQuery; static Queries &self(); }; ResourceScoreCache::Queries &ResourceScoreCache::Queries::self() { static Queries queries; return queries; } class ResourceScoreCache::Private { public: QString activity; QString application; QString resource; inline qreal timeFactor(int days) const { // Exp is falling rather quickly, we are slowing it 32 times return std::exp(-days / 32.0); } inline qreal timeFactor(const QDateTime &fromTime, const QDateTime &toTime) const { return timeFactor(fromTime.daysTo(toTime)); } }; ResourceScoreCache::ResourceScoreCache(const QString &activity, const QString &application, const QString &resource) { d->activity = activity; d->application = application; d->resource = resource; Q_ASSERT_X(!d->application.isEmpty(), "ResourceScoreCache::constructor", "Agent should not be empty"); Q_ASSERT_X(!d->activity.isEmpty(), "ResourceScoreCache::constructor", "Activity should not be empty"); Q_ASSERT_X(!d->resource.isEmpty(), "ResourceScoreCache::constructor", "Resource should not be empty"); } ResourceScoreCache::~ResourceScoreCache() { } void ResourceScoreCache::update() { QDateTime lastUpdate; QDateTime firstUpdate; QDateTime currentTime = QDateTime::currentDateTime(); qreal score = 0; DATABASE_TRANSACTION(*resourcesDatabase()); qCDebug(KAMD_LOG_RESOURCES) << "Creating the cache for: " << d->resource; // This can fail if we have the cache already made - auto isCacheNew = Utils::exec( + auto isCacheNew = Utils::exec(*resourcesDatabase(), Utils::IgnoreError, Queries::self().createResourceScoreCacheQuery, ":usedActivity", d->activity, ":initiatingAgent", d->application, ":targettedResource", d->resource, ":firstUpdate", currentTime.toSecsSinceEpoch() ); // Getting the old score - Utils::exec( + Utils::exec(*resourcesDatabase(), Utils::FailOnError, Queries::self().getResourceScoreCacheQuery, ":usedActivity", d->activity, ":initiatingAgent", d->application, ":targettedResource", d->resource ); // Only and always one result for (const auto &result: Queries::self().getResourceScoreCacheQuery) { lastUpdate.setSecsSinceEpoch(result["lastUpdate"].toUInt()); firstUpdate.setSecsSinceEpoch(result["firstUpdate"].toUInt()); qCDebug(KAMD_LOG_RESOURCES) << "Already in database? " << (!isCacheNew); qCDebug(KAMD_LOG_RESOURCES) << " First update : " << firstUpdate; qCDebug(KAMD_LOG_RESOURCES) << " Last update : " << lastUpdate; if (isCacheNew) { // If we haven't had the cache before, set the score to 0 firstUpdate = currentTime; score = 0; } else { // Adjusting the score depending on the time that passed since the // last update score = result["cachedScore"].toReal(); score *= d->timeFactor(lastUpdate, currentTime); } } // Calculating the updated score // We are processing all events since the last cache update qCDebug(KAMD_LOG_RESOURCES) << "After the adjustment"; qCDebug(KAMD_LOG_RESOURCES) << " Current score : " << score; qCDebug(KAMD_LOG_RESOURCES) << " First update : " << firstUpdate; qCDebug(KAMD_LOG_RESOURCES) << " Last update : " << lastUpdate; - Utils::exec(Utils::FailOnError, Queries::self().getScoreAdditionQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, Queries::self().getScoreAdditionQuery, ":usedActivity", d->activity, ":initiatingAgent", d->application, ":targettedResource", d->resource, ":start", lastUpdate.toSecsSinceEpoch() ); uint lastEventStart = currentTime.toSecsSinceEpoch(); for (const auto &result: Queries::self().getScoreAdditionQuery) { lastEventStart = result["start"].toUInt(); const auto end = result["end"].toUInt(); const auto intervalLength = end - lastEventStart; qCDebug(KAMD_LOG_RESOURCES) << "Interval length is " << intervalLength; if (intervalLength == 0) { // We have an Accessed event - otherwise, this wouldn't be 0 score += d->timeFactor(QDateTime::fromSecsSinceEpoch(end), currentTime); // like it is open for 1 minute } else { score += d->timeFactor(QDateTime::fromSecsSinceEpoch(end), currentTime) * intervalLength / 60.0; } } qCDebug(KAMD_LOG_RESOURCES) << " New score : " << score; // Updating the score - Utils::exec(Utils::FailOnError, Queries::self().updateResourceScoreCacheQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, Queries::self().updateResourceScoreCacheQuery, ":usedActivity", d->activity, ":initiatingAgent", d->application, ":targettedResource", d->resource, ":cachedScore", score, ":lastUpdate", lastEventStart ); // Notifying the world qCDebug(KAMD_LOG_RESOURCES) << "ResourceScoreUpdated:" << d->activity << d->application << d->resource ; emit QMetaObject::invokeMethod(StatsPlugin::self(), "ResourceScoreUpdated", Qt::QueuedConnection, Q_ARG(QString, d->activity), Q_ARG(QString, d->application), Q_ARG(QString, d->resource), Q_ARG(double, score), Q_ARG(uint, lastEventStart), Q_ARG(uint, firstUpdate.toSecsSinceEpoch()) ); } diff --git a/src/service/plugins/sqlite/StatsPlugin.cpp b/src/service/plugins/sqlite/StatsPlugin.cpp index a65f2dd..1c4766a 100644 --- a/src/service/plugins/sqlite/StatsPlugin.cpp +++ b/src/service/plugins/sqlite/StatsPlugin.cpp @@ -1,723 +1,723 @@ /* * Copyright (C) 2011, 2012, 2013, 2014 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include #include "StatsPlugin.h" // Qt #include #include #include #include // KDE #include #include // Boost #include #include // Local #include "Database.h" #include "ResourceScoreMaintainer.h" #include "ResourceLinking.h" #include "Utils.h" #include "../../Event.h" #include "resourcescoringadaptor.h" #include "common/specialvalues.h" KAMD_EXPORT_PLUGIN(sqliteplugin, StatsPlugin, "kactivitymanagerd-plugin-sqlite.json") StatsPlugin *StatsPlugin::s_instance = nullptr; StatsPlugin::StatsPlugin(QObject *parent, const QVariantList &args) : Plugin(parent) , m_activities(nullptr) , m_resources(nullptr) , m_resourceLinking(new ResourceLinking(this)) { Q_UNUSED(args); s_instance = this; new ResourcesScoringAdaptor(this); QDBusConnection::sessionBus().registerObject( QStringLiteral("/ActivityManager/Resources/Scoring"), this); setName(QStringLiteral("org.kde.ActivityManager.Resources.Scoring")); } bool StatsPlugin::init(QHash &modules) { Plugin::init(modules); if (!resourcesDatabase()) { return false; } m_activities = modules[QStringLiteral("activities")]; m_resources = modules[QStringLiteral("resources")]; m_resourceLinking->init(); connect(m_resources, SIGNAL(ProcessedResourceEvents(EventList)), this, SLOT(addEvents(EventList))); connect(m_resources, SIGNAL(RegisteredResourceMimetype(QString, QString)), this, SLOT(saveResourceMimetype(QString, QString))); connect(m_resources, SIGNAL(RegisteredResourceTitle(QString, QString)), this, SLOT(saveResourceTitle(QString, QString))); connect(modules[QStringLiteral("config")], SIGNAL(pluginConfigChanged()), this, SLOT(loadConfiguration())); loadConfiguration(); return true; } void StatsPlugin::loadConfiguration() { auto conf = config(); conf.config()->reparseConfiguration(); const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("kactivitymanagerd-pluginsrc"); m_blockedByDefault = conf.readEntry("blocked-by-default", false); m_blockAll = false; m_whatToRemember = (WhatToRemember)conf.readEntry("what-to-remember", (int)AllApplications); m_apps.clear(); if (m_whatToRemember == SpecificApplications) { auto apps = conf.readEntry( m_blockedByDefault ? "allowed-applications" : "blocked-applications", QStringList()); m_apps.insert(apps.cbegin(), apps.cend()); } // Delete old events, as per configuration. // For people who do not restart their computers, we should do this from // time to time. Doing this twice a day should be more than enough. deleteOldEvents(); m_deleteOldEventsTimer.setInterval(12 * 60 * 60 * 1000); connect(&m_deleteOldEventsTimer, &QTimer::timeout, this, &StatsPlugin::deleteOldEvents); // Loading URL filters m_urlFilters.clear(); auto filters = conf.readEntry("url-filters", QStringList() << "about:*" // Ignore about: stuff << "*/.*" // Ignore hidden files << "/" // Ignore root << "/tmp/*" // Ignore everything in /tmp ); for (const auto& filter: filters) { m_urlFilters << Common::starPatternToRegex(filter); } // Loading the private activities m_otrActivities = conf.readEntry("off-the-record-activities", QStringList()); } void StatsPlugin::deleteOldEvents() { DeleteEarlierStats(QString(), config().readEntry("keep-history-for", 0)); } void StatsPlugin::openResourceEvent(const QString &usedActivity, const QString &initiatingAgent, const QString &targettedResource, const QDateTime &start, const QDateTime &end) { Q_ASSERT_X(!initiatingAgent.isEmpty(), "StatsPlugin::openResourceEvent", "Agent should not be empty"); Q_ASSERT_X(!usedActivity.isEmpty(), "StatsPlugin::openResourceEvent", "Activity should not be empty"); Q_ASSERT_X(!targettedResource.isEmpty(), "StatsPlugin::openResourceEvent", "Resource should not be empty"); detectResourceInfo(targettedResource); Utils::prepare(*resourcesDatabase(), openResourceEventQuery, QStringLiteral( "INSERT INTO ResourceEvent" " (usedActivity, initiatingAgent, targettedResource, start, end) " "VALUES (:usedActivity, :initiatingAgent, :targettedResource, :start, :end)" )); - Utils::exec(Utils::FailOnError, *openResourceEventQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *openResourceEventQuery, ":usedActivity" , usedActivity , ":initiatingAgent" , initiatingAgent , ":targettedResource" , targettedResource , ":start" , start.toSecsSinceEpoch() , ":end" , (end.isNull()) ? QVariant() : end.toSecsSinceEpoch() ); } void StatsPlugin::closeResourceEvent(const QString &usedActivity, const QString &initiatingAgent, const QString &targettedResource, const QDateTime &end) { Q_ASSERT_X(!initiatingAgent.isEmpty(), "StatsPlugin::closeResourceEvent", "Agent should not be empty"); Q_ASSERT_X(!usedActivity.isEmpty(), "StatsPlugin::closeResourceEvent", "Activity should not be empty"); Q_ASSERT_X(!targettedResource.isEmpty(), "StatsPlugin::closeResourceEvent", "Resource should not be empty"); Utils::prepare(*resourcesDatabase(), closeResourceEventQuery, QStringLiteral( "UPDATE ResourceEvent " "SET end = :end " "WHERE " ":usedActivity = usedActivity AND " ":initiatingAgent = initiatingAgent AND " ":targettedResource = targettedResource AND " "end IS NULL" )); - Utils::exec(Utils::FailOnError, *closeResourceEventQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *closeResourceEventQuery, ":usedActivity" , usedActivity , ":initiatingAgent" , initiatingAgent , ":targettedResource" , targettedResource , ":end" , end.toSecsSinceEpoch() ); } void StatsPlugin::detectResourceInfo(const QString &_uri) { const QUrl uri = QUrl::fromUserInput(_uri); if (!uri.isLocalFile()) return; const QString file = uri.toLocalFile(); if (!QFile::exists(file)) return; KFileItem item(uri); if (insertResourceInfo(file)) { saveResourceMimetype(file, item.mimetype(), true); const auto text = item.text(); saveResourceTitle(file, text.isEmpty() ? _uri : text, true); } } bool StatsPlugin::insertResourceInfo(const QString &uri) { Utils::prepare(*resourcesDatabase(), getResourceInfoQuery, QStringLiteral( "SELECT targettedResource FROM ResourceInfo WHERE " " targettedResource = :targettedResource " )); getResourceInfoQuery->bindValue(":targettedResource", uri); - Utils::exec(Utils::FailOnError, *getResourceInfoQuery); + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *getResourceInfoQuery); if (getResourceInfoQuery->next()) { return false; } Utils::prepare(*resourcesDatabase(), insertResourceInfoQuery, QStringLiteral( "INSERT INTO ResourceInfo( " " targettedResource" ", title" ", autoTitle" ", mimetype" ", autoMimetype" ") VALUES (" " :targettedResource" ", '' " ", 1 " ", '' " ", 1 " ")" )); - Utils::exec(Utils::FailOnError, *insertResourceInfoQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *insertResourceInfoQuery, ":targettedResource", uri ); return true; } void StatsPlugin::saveResourceTitle(const QString &uri, const QString &title, bool autoTitle) { insertResourceInfo(uri); DATABASE_TRANSACTION(*resourcesDatabase()); Utils::prepare(*resourcesDatabase(), saveResourceTitleQuery, QStringLiteral( "UPDATE ResourceInfo SET " " title = :title" ", autoTitle = :autoTitle " "WHERE " "targettedResource = :targettedResource " )); - Utils::exec(Utils::FailOnError, *saveResourceTitleQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *saveResourceTitleQuery, ":targettedResource" , uri , ":title" , title , ":autoTitle" , (autoTitle ? "1" : "0") ); } void StatsPlugin::saveResourceMimetype(const QString &uri, const QString &mimetype, bool autoMimetype) { insertResourceInfo(uri); DATABASE_TRANSACTION(*resourcesDatabase()); Utils::prepare(*resourcesDatabase(), saveResourceMimetypeQuery, QStringLiteral( "UPDATE ResourceInfo SET " " mimetype = :mimetype" ", autoMimetype = :autoMimetype " "WHERE " "targettedResource = :targettedResource " )); - Utils::exec(Utils::FailOnError, *saveResourceMimetypeQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, *saveResourceMimetypeQuery, ":targettedResource" , uri , ":mimetype" , mimetype , ":autoMimetype" , (autoMimetype ? "1" : "0") ); } StatsPlugin *StatsPlugin::self() { return s_instance; } bool StatsPlugin::acceptedEvent(const Event &event) { using std::bind; using std::any_of; using namespace std::placeholders; return !( // If the URI is empty, we do not want to process it event.uri.isEmpty() || // Skip if the current activity is OTR m_otrActivities.contains(currentActivity()) || // Exclude URIs that match the ignored patterns any_of(m_urlFilters.cbegin(), m_urlFilters.cend(), bind(&QRegExp::exactMatch, _1, event.uri)) || // if blocked by default, the list contains allowed applications // ignore event if the list doesn't contain the application // if not blocked by default, the list contains blocked applications // ignore event if the list contains the application (m_whatToRemember == SpecificApplications && m_blockedByDefault != boost::binary_search(m_apps, event.application)) ); } Event StatsPlugin::validateEvent(Event event) { if (event.uri.startsWith(QStringLiteral("file://"))) { event.uri = QUrl(event.uri).toLocalFile(); } if (event.uri.startsWith(QStringLiteral("/"))) { QFileInfo file(event.uri); event.uri = file.exists() ? file.canonicalFilePath() : QString(); } return event; } QStringList StatsPlugin::listActivities() const { return Plugin::retrieve( m_activities, "ListActivities", "QStringList"); } QString StatsPlugin::currentActivity() const { return Plugin::retrieve( m_activities, "CurrentActivity", "QString"); } void StatsPlugin::addEvents(const EventList &events) { using namespace kamd::utils; if (m_blockAll || m_whatToRemember == NoApplications) { return; } const auto &eventsToProcess = events | transformed(&StatsPlugin::validateEvent, this) | filtered(&StatsPlugin::acceptedEvent, this); if (eventsToProcess.begin() == eventsToProcess.end()) return; DATABASE_TRANSACTION(*resourcesDatabase()); for (auto event : eventsToProcess) { switch (event.type) { case Event::Accessed: openResourceEvent( currentActivity(), event.application, event.uri, event.timestamp, event.timestamp); ResourceScoreMaintainer::self()->processResource( event.uri, event.application); break; case Event::Opened: openResourceEvent( currentActivity(), event.application, event.uri, event.timestamp); break; case Event::Closed: closeResourceEvent( currentActivity(), event.application, event.uri, event.timestamp); ResourceScoreMaintainer::self()->processResource( event.uri, event.application); break; case Event::UserEventType: ResourceScoreMaintainer::self()->processResource( event.uri, event.application); break; default: // Nothing yet // TODO: Add focus and modification break; } } } void StatsPlugin::DeleteRecentStats(const QString &activity, int count, const QString &what) { const auto usedActivity = activity.isEmpty() ? QVariant() : QVariant(activity); // If we need to delete everything, // no need to bother with the count and the date DATABASE_TRANSACTION(*resourcesDatabase()); if (what == QStringLiteral("everything")) { // Instantiating these every time is not a big overhead // since this method is rarely executed. auto removeEventsQuery = resourcesDatabase()->createQuery(); removeEventsQuery.prepare( "DELETE FROM ResourceEvent " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity)" ); auto removeScoreCachesQuery = resourcesDatabase()->createQuery(); removeScoreCachesQuery.prepare( "DELETE FROM ResourceScoreCache " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity)"); - Utils::exec(Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity); - Utils::exec(Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity); + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity); + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity); } else { // Deleting a specified length of time auto since = QDateTime::currentDateTime(); since = (what[0] == QLatin1Char('h')) ? since.addSecs(-count * 60 * 60) : (what[0] == QLatin1Char('d')) ? since.addDays(-count) : (what[0] == QLatin1Char('m')) ? since.addMonths(-count) : since; // Maybe we should decrease the scores for the previously // cached items. Thinking it is not that important - // if something was accessed before, and the user did not // remove the history, it is not really a secret. auto removeEventsQuery = resourcesDatabase()->createQuery(); removeEventsQuery.prepare( "DELETE FROM ResourceEvent " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) " "AND end > :since" ); auto removeScoreCachesQuery = resourcesDatabase()->createQuery(); removeScoreCachesQuery.prepare( "DELETE FROM ResourceScoreCache " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) " "AND firstUpdate > :since"); - Utils::exec(Utils::FailOnError, removeEventsQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity, ":since", since.toSecsSinceEpoch() ); - Utils::exec(Utils::FailOnError, removeScoreCachesQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity, ":since", since.toSecsSinceEpoch() ); } emit RecentStatsDeleted(activity, count, what); } void StatsPlugin::DeleteEarlierStats(const QString &activity, int months) { if (months == 0) { return; } // Deleting a specified length of time DATABASE_TRANSACTION(*resourcesDatabase()); const auto time = QDateTime::currentDateTime().addMonths(-months); const auto usedActivity = activity.isEmpty() ? QVariant() : QVariant(activity); auto removeEventsQuery = resourcesDatabase()->createQuery(); removeEventsQuery.prepare( "DELETE FROM ResourceEvent " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) " "AND start < :time" ); auto removeScoreCachesQuery = resourcesDatabase()->createQuery(); removeScoreCachesQuery.prepare( "DELETE FROM ResourceScoreCache " "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) " "AND lastUpdate < :time"); - Utils::exec(Utils::FailOnError, removeEventsQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity, ":time", time.toSecsSinceEpoch() ); - Utils::exec(Utils::FailOnError, removeScoreCachesQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity, ":time", time.toSecsSinceEpoch() ); emit EarlierStatsDeleted(activity, months); } void StatsPlugin::DeleteStatsForResource(const QString &activity, const QString &client, const QString &resource) { Q_ASSERT_X(!client.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Agent should not be empty"); Q_ASSERT_X(!activity.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Activity should not be empty"); Q_ASSERT_X(!resource.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Resource should not be empty"); Q_ASSERT_X(client != CURRENT_AGENT_TAG, "StatsPlugin::DeleteStatsForResource", "We can not handle CURRENT_AGENT_TAG here"); DATABASE_TRANSACTION(*resourcesDatabase()); // Check against sql injection if (activity.contains('\'') || client.contains('\'')) return; const auto activityFilter = activity == ANY_ACTIVITY_TAG ? " 1 " : QStringLiteral(" usedActivity = '%1' ").arg( activity == CURRENT_ACTIVITY_TAG ? currentActivity() : activity ); const auto clientFilter = client == ANY_AGENT_TAG ? " 1 " : QStringLiteral(" initiatingAgent = '%1' ").arg(client); auto removeEventsQuery = resourcesDatabase()->createQuery(); removeEventsQuery.prepare( "DELETE FROM ResourceEvent " "WHERE " + activityFilter + " AND " + clientFilter + " AND " + "targettedResource LIKE :targettedResource ESCAPE '\\'" ); auto removeScoreCachesQuery = resourcesDatabase()->createQuery(); removeScoreCachesQuery.prepare( "DELETE FROM ResourceScoreCache " "WHERE " + activityFilter + " AND " + clientFilter + " AND " + "targettedResource LIKE :targettedResource ESCAPE '\\'" ); const auto pattern = Common::starPatternToLike(resource); - Utils::exec(Utils::FailOnError, removeEventsQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":targettedResource", pattern); - Utils::exec(Utils::FailOnError, removeScoreCachesQuery, + Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":targettedResource", pattern); emit ResourceScoreDeleted(activity, client, resource); } bool StatsPlugin::isFeatureOperational(const QStringList &feature) const { if (feature[0] == "isOTR") { if (feature.size() != 2) return true; const auto activity = feature[1]; return activity == "activity" || activity == "current" || listActivities().contains(activity); return true; } return false; } // bool StatsPlugin::isFeatureEnabled(const QStringList &feature) const // { // if (feature[0] == "isOTR") { // if (feature.size() != 2) return false; // // auto activity = feature[1]; // // if (activity == "activity" || activity == "current") { // activity = currentActivity(); // } // // return m_otrActivities.contains(activity); // } // // return false; // } // // void StatsPlugin::setFeatureEnabled(const QStringList &feature, bool value) // { // if (feature[0] == "isOTR") { // if (feature.size() != 2) return; // // auto activity = feature[1]; // // if (activity == "activity" || activity == "current") { // activity = currentActivity(); // } // // if (!m_otrActivities.contains(activity)) { // m_otrActivities << activity; // config().writeEntry("off-the-record-activities", m_otrActivities); // config().sync(); // } // } // } QDBusVariant StatsPlugin::featureValue(const QStringList &feature) const { if (feature[0] == "isOTR") { if (feature.size() != 2) return QDBusVariant(false); auto activity = feature[1]; if (activity == "activity" || activity == "current") { activity = currentActivity(); } return QDBusVariant(m_otrActivities.contains(activity)); } return QDBusVariant(false); } void StatsPlugin::setFeatureValue(const QStringList &feature, const QDBusVariant &value) { if (feature[0] == "isOTR") { if (feature.size() != 2) return; auto activity = feature[1]; if (activity == "activity" || activity == "current") { activity = currentActivity(); } bool isOTR = value.variant().toBool(); if (isOTR && !m_otrActivities.contains(activity)) { m_otrActivities << activity; } else if (!isOTR && m_otrActivities.contains(activity)) { m_otrActivities.removeAll(activity); } config().writeEntry("off-the-record-activities", m_otrActivities); config().sync(); } } QStringList StatsPlugin::listFeatures(const QStringList &feature) const { if (feature.isEmpty() || feature[0].isEmpty()) { return { "isOTR/" }; } else if (feature[0] == "isOTR") { return listActivities(); } return QStringList(); } #include "StatsPlugin.moc" diff --git a/src/service/plugins/sqlite/StatsPlugin.h b/src/service/plugins/sqlite/StatsPlugin.h index c5476e9..36020ad 100644 --- a/src/service/plugins/sqlite/StatsPlugin.h +++ b/src/service/plugins/sqlite/StatsPlugin.h @@ -1,158 +1,158 @@ /* * Copyright (C) 2011, 2012, 2013, 2014 Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef PLUGINS_SQLITE_STATS_PLUGIN_H #define PLUGINS_SQLITE_STATS_PLUGIN_H // Qt #include #include +#include // Boost and STL #include #include // Local #include -class QSqlQuery; class ResourceLinking; /** * Communication with the outer world. * * - Handles configuration * - Filters the events based on the user's configuration. */ class StatsPlugin : public Plugin { Q_OBJECT // Q_CLASSINFO("D-Bus Interface", "org.kde.ActivityManager.Resources.Scoring") // Q_PLUGIN_METADATA(IID "org.kde.ActivityManager.plugins.sqlite") public: explicit StatsPlugin(QObject *parent = nullptr, const QVariantList &args = QVariantList()); static StatsPlugin *self(); bool init(QHash &modules) override; QString currentActivity() const; QStringList listActivities() const; inline QObject *activitiesInterface() const { return m_activities; } bool isFeatureOperational(const QStringList &feature) const override; QStringList listFeatures(const QStringList &feature) const override; QDBusVariant featureValue(const QStringList &property) const override; void setFeatureValue(const QStringList &property, const QDBusVariant &value) override; // // D-BUS Interface methods // public Q_SLOTS: void DeleteRecentStats(const QString &activity, int count, const QString &what); void DeleteEarlierStats(const QString &activity, int months); void DeleteStatsForResource(const QString &activity, const QString &client, const QString &resource); Q_SIGNALS: void ResourceScoreUpdated(const QString &activity, const QString &client, const QString &resource, double score, uint lastUpdate, uint firstUpdate); void ResourceScoreDeleted(const QString &activity, const QString &client, const QString &resource); void RecentStatsDeleted(const QString &activity, int count, const QString &what); void EarlierStatsDeleted(const QString &activity, int months); // // End D-BUS Interface methods // private Q_SLOTS: void addEvents(const EventList &events); void loadConfiguration(); void openResourceEvent(const QString &usedActivity, const QString &initiatingAgent, const QString &targettedResource, const QDateTime &start, const QDateTime &end = QDateTime()); void closeResourceEvent(const QString &usedActivity, const QString &initiatingAgent, const QString &targettedResource, const QDateTime &end); void saveResourceTitle(const QString &uri, const QString &title, bool autoTitle = false); void saveResourceMimetype(const QString &uri, const QString &mimetype, bool autoMimetype = false); bool insertResourceInfo(const QString &uri); void detectResourceInfo(const QString &uri); void deleteOldEvents(); private: inline bool acceptedEvent(const Event &event); inline Event validateEvent(Event event); enum WhatToRemember { AllApplications = 0, SpecificApplications = 1, NoApplications = 2 }; QObject *m_activities; QObject *m_resources; boost::container::flat_set m_apps; QList m_urlFilters; QStringList m_otrActivities; std::unique_ptr openResourceEventQuery; std::unique_ptr closeResourceEventQuery; std::unique_ptr insertResourceInfoQuery; std::unique_ptr getResourceInfoQuery; std::unique_ptr saveResourceTitleQuery; std::unique_ptr saveResourceMimetypeQuery; QTimer m_deleteOldEventsTimer; bool m_blockedByDefault : 1; bool m_blockAll : 1; WhatToRemember m_whatToRemember : 2; ResourceLinking *m_resourceLinking; static StatsPlugin *s_instance; }; #endif // PLUGINS_SQLITE_STATS_PLUGIN_H diff --git a/src/service/plugins/sqlite/Utils.h b/src/service/plugins/sqlite/Utils.h index 4947954..8903246 100644 --- a/src/service/plugins/sqlite/Utils.h +++ b/src/service/plugins/sqlite/Utils.h @@ -1,87 +1,92 @@ /* * Copyright (C) 2014 Ivan Cukic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PLUGINS_SQLITE_DATABASE_UTILS_H #define PLUGINS_SQLITE_DATABASE_UTILS_H #include +#include #include #include #include "DebugResources.h" namespace Utils { static unsigned int errorCount = 0; inline bool prepare(Common::Database &database, QSqlQuery &query, const QString &queryString) { Q_UNUSED(database); return query.prepare(queryString); } inline bool prepare(Common::Database &database, std::unique_ptr &query, const QString &queryString) { if (query) { return true; } query.reset(new QSqlQuery(database.createQuery())); return prepare(database, *query, queryString); } enum ErrorHandling { IgnoreError, FailOnError }; - inline bool exec(ErrorHandling eh, QSqlQuery &query) + inline bool exec(Common::Database &database, ErrorHandling eh, QSqlQuery &query) { bool success = query.exec(); if (eh == FailOnError) { if ((!success) && (errorCount++ < 2)) { qCWarning(KAMD_LOG_RESOURCES) << query.lastQuery(); qCWarning(KAMD_LOG_RESOURCES) << query.lastError(); } Q_ASSERT_X(success, "Uils::exec", qPrintable(QStringLiteral("Query failed:") + query.lastError().text())); + + if (!success) { + database.reportError(query.lastError()); + } } return success; } template - inline bool exec(ErrorHandling eh, QSqlQuery &query, + inline bool exec(Common::Database &database, ErrorHandling eh, QSqlQuery &query, const T1 &variable, const T2 &value, Ts... ts) { query.bindValue(variable, value); - return exec(eh, query, ts...); + return exec(database, eh, query, ts...); } } // namespace Utils #endif /* !PLUGINS_SQLITE_DATABASE_UTILS_H */