File indexing completed on 2024-05-19 04:27:37

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 #include "KisResourceCacheDb.h"
0007 
0008 #include <QtSql>
0009 #include <QStandardPaths>
0010 #include <QDir>
0011 #include <QDirIterator>
0012 #include <QStringList>
0013 #include <QElapsedTimer>
0014 #include <QDataStream>
0015 #include <QByteArray>
0016 #include <QMessageBox>
0017 
0018 #include <KritaVersionWrapper.h>
0019 
0020 #include <klocalizedstring.h>
0021 #include <KisBackup.h>
0022 
0023 #include <kis_debug.h>
0024 #include <KisUsageLogger.h>
0025 
0026 #include "KisResourceLocator.h"
0027 #include "KisResourceLoaderRegistry.h"
0028 
0029 #include "ResourceDebug.h"
0030 #include <kis_assert.h>
0031 
0032 #include <KisCppQuirks.h>
0033 
0034 const QString dbDriver = "QSQLITE";
0035 
0036 const QString KisResourceCacheDb::resourceCacheDbFilename { "resourcecache.sqlite" };
0037 const QString KisResourceCacheDb::databaseVersion { "0.0.17" };
0038 QStringList KisResourceCacheDb::storageTypes { QStringList() };
0039 QStringList KisResourceCacheDb::disabledBundles { QStringList() << "Krita_3_Default_Resources.bundle" };
0040 
0041 bool KisResourceCacheDb::s_valid {false};
0042 QString KisResourceCacheDb::s_lastError {QString()};
0043 
0044 bool KisResourceCacheDb::isValid()
0045 {
0046     return s_valid;
0047 }
0048 
0049 QString KisResourceCacheDb::lastError()
0050 {
0051     return s_lastError;
0052 }
0053 
0054 // use in WHERE QSqlQuery clauses
0055 // because if the string is null, the query will also have null there
0056 // and every comparison with null is false, so the query won't find anything
0057 // (especially important for storage location where empty string is common)
0058 QString changeToEmptyIfNull(QString s)
0059 {
0060     return s.isNull() ? QString("") : s;
0061 }
0062 
0063 bool updateSchemaVersion()
0064 {
0065     QFile f(":/fill_version_information.sql");
0066     if (f.open(QFile::ReadOnly)) {
0067         QString sql = f.readAll();
0068         QSqlQuery q;
0069         if (!q.prepare(sql)) {
0070             qWarning() << "Could not prepare the schema information query" << q.lastError() << q.boundValues();
0071             return false;
0072         }
0073         q.addBindValue(KisResourceCacheDb::databaseVersion);
0074         q.addBindValue(KritaVersionWrapper::versionString());
0075         q.addBindValue(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
0076         if (!q.exec()) {
0077             qWarning() << "Could not insert the current version" << q.lastError() << q.boundValues();
0078             return false;
0079         }
0080         infoResources << "Filled version table";
0081     }
0082     return true;
0083 }
0084 
0085 
0086 
0087 QSqlError createDatabase(const QString &location)
0088 {
0089     // NOTE: if the id's of Unknown and Memory in the database
0090     //       will change, and that will break the queries that
0091     //       remove Unknown and Memory storages on start-up.
0092     KisResourceCacheDb::storageTypes << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(1))
0093                                      << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(2))
0094                                      << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(3))
0095                                      << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(4))
0096                                      << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(5))
0097                                      << KisResourceStorage::storageTypeToUntranslatedString(KisResourceStorage::StorageType(6))
0098                                      ;
0099 
0100     if (!QSqlDatabase::connectionNames().isEmpty()) {
0101         return QSqlError();
0102     }
0103 
0104     QDir dbLocation(location);
0105     if (!dbLocation.exists()) {
0106         dbLocation.mkpath(dbLocation.path());
0107     }
0108 
0109     QSqlDatabase db = QSqlDatabase::addDatabase(dbDriver);
0110     db.setDatabaseName(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
0111 
0112     if (!db.open()) {
0113         qWarning() << "Could not connect to resource cache database";
0114         return db.lastError();
0115     }
0116 
0117     // will be filled correctly later
0118     QVersionNumber oldSchemaVersionNumber;
0119     QVersionNumber newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
0120 
0121 
0122     QStringList tables = QStringList() << "version_information"
0123                                        << "storage_types"
0124                                        << "resource_types"
0125                                        << "storages"
0126                                        << "tags"
0127                                        << "resources"
0128                                        << "versioned_resources"
0129                                        << "resource_tags"
0130                                        << "metadata"
0131                                        << "tags_storages"
0132                                        << "tag_translations";
0133 
0134     QStringList dbTables;
0135     // Verify whether we should recreate the database
0136     {
0137         bool allTablesPresent = true;
0138         dbTables = db.tables();
0139         Q_FOREACH(const QString &table, tables) {
0140             if (!dbTables.contains(table)) {
0141                 allTablesPresent = false;
0142                 break;
0143             }
0144         }
0145 
0146         bool schemaIsOutDated = false;
0147         QString schemaVersion = "0.0.0";
0148         QString kritaVersion = "Unknown";
0149         int creationDate = 0;
0150 
0151         if (dbTables.contains("version_information")) {
0152             // Verify the version number
0153 
0154             QSqlQuery q("SELECT database_version\n"
0155                         ",      krita_version\n"
0156                         ",      creation_date\n"
0157                         "FROM version_information\n"
0158                         "ORDER BY id\n"
0159                         "DESC\n"
0160                         "LIMIT 1;\n");
0161 
0162             if (!q.exec()) {
0163                 qWarning() << "Could not retrieve version information from the database." << q.lastError();
0164                 abort();
0165             }
0166             q.first();
0167             schemaVersion = q.value(0).toString();
0168             kritaVersion = q.value(1).toString();
0169             creationDate = q.value(2).toInt();
0170 
0171             oldSchemaVersionNumber = QVersionNumber::fromString(schemaVersion);
0172             newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
0173 
0174             if (QVersionNumber::compare(oldSchemaVersionNumber, newSchemaVersionNumber) != 0) {
0175 
0176                 qWarning() << "Old schema:" << schemaVersion << "New schema:" << newSchemaVersionNumber;
0177 
0178                 schemaIsOutDated = true;
0179                 KisBackup::numberedBackupFile(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
0180 
0181                 if (newSchemaVersionNumber == QVersionNumber::fromString("0.0.17")
0182                         && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) > 0
0183                         && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.17")) < 0) {
0184                     bool from14to15 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14");
0185                     bool from15to16 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
0186                             || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15");
0187                     bool from16to17 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
0188                             || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15")
0189                             || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.16");
0190 
0191                     bool success = true;
0192                     if (from14to15) {
0193                         qWarning() << "Going to update resource_tags table";
0194 
0195                         QSqlQuery q;
0196                         q.prepare("ALTER TABLE  resource_tags\n"
0197                                   "ADD   COLUMN active INTEGER NOT NULL DEFAULT 1");
0198                         if (!q.exec()) {
0199                             qWarning() << "Could not update the resource_tags table." << q.lastError();
0200                             success = false;
0201                         }
0202                         else {
0203                             qWarning() << "Updated table resource_tags: success.";
0204                         }
0205                     }
0206                     if (from15to16) {
0207                         qWarning() << "Going to update indices";
0208 
0209                         QStringList indexes = QStringList() << "tags" << "resources" << "tag_translations" << "resource_tags";
0210 
0211                         Q_FOREACH(const QString &index, indexes) {
0212                             QFile f(":/create_index_" + index + ".sql");
0213                             if (f.open(QFile::ReadOnly)) {
0214                                 QSqlQuery q;
0215                                 if (!q.exec(f.readAll())) {
0216                                     qWarning() << "Could not create index" << index << q.lastError();
0217                                     return db.lastError();
0218                                 }
0219                                 infoResources << "Created index" << index;
0220                             }
0221                             else {
0222                                 return QSqlError("Error executing SQL", QString("Could not find SQL file %1").arg(index), QSqlError::StatementError);
0223                             }
0224                         }
0225                     }
0226 
0227                     if (from16to17) {
0228                         qWarning() << "Going to update resource signature index";
0229 
0230                         QFile f(":/create_index_resources_signature.sql");
0231                         if (f.open(QFile::ReadOnly)) {
0232                             QSqlQuery q;
0233                             if (!q.exec(f.readAll())) {
0234                                 qWarning() << "Could not create index for resources signature" << q.lastError();
0235                                 return db.lastError();
0236                             }
0237                             infoResources << "Created resources signature index";
0238                         }
0239                         else {
0240                             return QSqlError("Error executing SQL", QString("Could not find SQL file for resources signature index"), QSqlError::StatementError);
0241                         }
0242                     }
0243 
0244                     if (success) {
0245                         if (!updateSchemaVersion()) {
0246                             return QSqlError("Error executing SQL", QString("Could not update schema version."), QSqlError::StatementError);
0247                         }
0248                     }
0249 
0250                     schemaIsOutDated = !success;
0251 
0252                 }
0253 
0254                 if (schemaIsOutDated) {
0255                     QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The resource database scheme has changed. Krita will backup your database and create a new database."));
0256                     if (QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) > 0) {
0257                         KisResourceLocator::instance()->saveTags();
0258                     }
0259                     db.close();
0260                     QFile::remove(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
0261                     db.open();
0262                 }
0263             }
0264 
0265         }
0266 
0267         if (allTablesPresent && !schemaIsOutDated) {
0268             KisUsageLogger::log(QString("Database is up to date. Version: %1, created by Krita %2, at %3")
0269                                 .arg(schemaVersion)
0270                                 .arg(kritaVersion)
0271                                 .arg(QDateTime::fromSecsSinceEpoch(creationDate).toString()));
0272             return QSqlError();
0273         }
0274     }
0275 
0276     KisUsageLogger::log(QString("Creating database from scratch (%1, %2).")
0277                         .arg(oldSchemaVersionNumber.toString().isEmpty() ? QString("database didn't exist") : ("old schema version: " + oldSchemaVersionNumber.toString()))
0278                         .arg("new schema version: " + newSchemaVersionNumber.toString()));
0279 
0280     // Create tables
0281     Q_FOREACH(const QString &table, tables) {
0282         QFile f(":/create_" + table + ".sql");
0283         if (f.open(QFile::ReadOnly)) {
0284             QSqlQuery q;
0285             if (!q.exec(f.readAll())) {
0286                 qWarning() << "Could not create table" << table << q.lastError();
0287                 return db.lastError();
0288             }
0289             infoResources << "Created table" << table;
0290         }
0291         else {
0292             return QSqlError("Error executing SQL", QString("Could not find SQL file %1").arg(table), QSqlError::StatementError);
0293         }
0294     }
0295 
0296     // Create indexes
0297     QStringList indexes;
0298 
0299     // these indexes came in version 0.0.16
0300     indexes << "storages" << "versioned_resources" << "tags" << "resources" << "tag_translations" << "resource_tags";
0301 
0302     // this indexes came in version 0.0.17
0303     indexes << "resources_signature";
0304 
0305     Q_FOREACH(const QString &index, indexes) {
0306         QFile f(":/create_index_" + index + ".sql");
0307         if (f.open(QFile::ReadOnly)) {
0308             QSqlQuery q;
0309             if (!q.exec(f.readAll())) {
0310                 qWarning() << "Could not create index" << index;
0311                 return db.lastError();
0312             }
0313             infoResources << "Created table" << index;
0314         }
0315         else {
0316             return QSqlError("Error executing SQL", QString("Could not find SQL file %1").arg(index), QSqlError::StatementError);
0317         }
0318     }
0319 
0320     // Fill lookup tables
0321     {
0322         if (dbTables.contains("storage_types")) {
0323             QSqlQuery q;
0324             if (!q.exec("DELETE FROM storage_types;")) {
0325                 qWarning() << "Could not clear table storage_types" << db.lastError();
0326             }
0327         }
0328 
0329         QFile f(":/fill_storage_types.sql");
0330         if (f.open(QFile::ReadOnly)) {
0331             QString sql = f.readAll();
0332             Q_FOREACH(const QString &originType, KisResourceCacheDb::storageTypes) {
0333                 QSqlQuery q(sql);
0334                 q.addBindValue(originType);
0335                 if (!q.exec()) {
0336                     qWarning() << "Could not insert" << originType << db.lastError() << q.executedQuery();
0337                     return db.lastError();
0338                 }
0339             }
0340             infoResources << "Filled lookup table storage_types";
0341         }
0342         else {
0343             return QSqlError("Error executing SQL", QString("Could not find SQL fill_storage_types.sql."), QSqlError::StatementError);
0344         }
0345     }
0346 
0347     {
0348         if (dbTables.contains("resource_types")) {
0349             QSqlQuery q;
0350             if (!q.exec("DELETE FROM resource_types;")) {
0351                 qWarning() << "Could not clear table resource_types" << db.lastError();
0352             }
0353         }
0354         QFile f(":/fill_resource_types.sql");
0355         if (f.open(QFile::ReadOnly)) {
0356             QString sql = f.readAll();
0357             Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
0358                 QSqlQuery q(sql);
0359                 q.addBindValue(resourceType);
0360                 if (!q.exec()) {
0361                     qWarning() << "Could not insert" << resourceType << db.lastError() << q.executedQuery();
0362                     return db.lastError();
0363                 }
0364             }
0365             infoResources << "Filled lookup table resource_types";
0366         }
0367         else {
0368             return QSqlError("Error executing SQL", QString("Could not find SQL fill_resource_types.sql."), QSqlError::StatementError);
0369         }
0370     }
0371 
0372     if (!updateSchemaVersion()) {
0373        return QSqlError("Error executing SQL", QString("Could not update schema version."), QSqlError::StatementError);
0374     }
0375 
0376     return QSqlError();
0377 }
0378 
0379 bool KisResourceCacheDb::initialize(const QString &location)
0380 {
0381     QSqlError err = createDatabase(location);
0382 
0383     s_valid = !err.isValid();
0384     switch (err.type()) {
0385     case QSqlError::NoError:
0386         s_lastError = QString();
0387         break;
0388     case QSqlError::ConnectionError:
0389         s_lastError = QString("Could not initialize the resource cache database. Connection error: %1").arg(err.text());
0390         break;
0391     case QSqlError::StatementError:
0392         s_lastError = QString("Could not initialize the resource cache database. Statement error: %1").arg(err.text());
0393         break;
0394     case QSqlError::TransactionError:
0395         s_lastError = QString("Could not initialize the resource cache database. Transaction error: %1").arg(err.text());
0396         break;
0397     case QSqlError::UnknownError:
0398         s_lastError = QString("Could not initialize the resource cache database. Unknown error: %1").arg(err.text());
0399         break;
0400     }
0401 
0402     // Delete all storages that are no longer known to the resource locator (including the memory storages)
0403     deleteTemporaryResources();
0404 
0405     return s_valid;
0406 }
0407 
0408 QVector<int> KisResourceCacheDb::resourcesForStorage(const QString &resourceType, const QString &storageLocation)
0409 {
0410     QVector<int> result;
0411 
0412     QSqlQuery q;
0413 
0414     if (!q.prepare("SELECT resources.id\n"
0415                    "FROM   resources\n"
0416                    ",      resource_types\n"
0417                    ",      storages\n"
0418                    "WHERE  resources.resource_type_id = resource_types.id\n"
0419                    "AND    storages.id = resources.storage_id\n"
0420                    "AND    storages.location = :storage_location\n"
0421                    "AND    resource_types.name = :resource_type\n")) {
0422 
0423         qWarning() << "Could not read and prepare resourcesForStorage" << q.lastError();
0424         return result;
0425     }
0426 
0427     q.bindValue(":resource_type", resourceType);
0428     q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
0429 
0430     if (!q.exec()) {
0431         qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
0432         return result;
0433     }
0434 
0435     while (q.next()) {
0436         result << q.value(0).toInt();
0437     }
0438 
0439     return result;
0440 }
0441 
0442 int KisResourceCacheDb::resourceIdForResource(const QString &resourceFileName, const QString &resourceType, const QString &storageLocation)
0443 {
0444     //qDebug() << "resourceIdForResource" << resourceName << resourceFileName << resourceType << storageLocation;
0445 
0446     QSqlQuery q;
0447 
0448     if (!q.prepare("SELECT resources.id\n"
0449                    "FROM   resources\n"
0450                    ",      resource_types\n"
0451                    ",      storages\n"
0452                    "WHERE  resources.resource_type_id = resource_types.id\n"
0453                    "AND    storages.id = resources.storage_id\n"
0454                    "AND    storages.location = :storage_location\n"
0455                    "AND    resource_types.name = :resource_type\n"
0456                    "AND    resources.filename = :filename\n")) {
0457         qWarning() << "Could not read and prepare resourceIdForResource" << q.lastError();
0458         return -1;
0459     }
0460 
0461     q.bindValue(":filename", resourceFileName);
0462     q.bindValue(":resource_type", resourceType);
0463     q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
0464 
0465     if (!q.exec()) {
0466         qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
0467         return -1;
0468     }
0469 
0470     if (q.first()) {
0471         return q.value(0).toInt();
0472     }
0473 
0474     // couldn't be found in the `resources` table, but can still be in versioned_resources
0475 
0476     if (!q.prepare("SELECT versioned_resources.resource_id\n"
0477                    "FROM   resources\n"
0478                    ",      resource_types\n"
0479                    ",      versioned_resources\n"
0480                    ",      storages\n"
0481                    "WHERE  resources.resource_type_id = resource_types.id\n"    // join resources and resource_types by resource id
0482                    "AND    versioned_resources.resource_id = resources.id\n"    // join versioned_resources and resources by resource id
0483                    "AND    storages.id = versioned_resources.storage_id\n"      // join storages and versioned_resources by storage id
0484                    "AND    storages.location = :storage_location\n"             // storage location must be the same as asked for
0485                    "AND    resource_types.name = :resource_type\n"              // resource type must be the same as asked for
0486                    "AND    versioned_resources.filename = :filename\n")) {      // filename must be the same as asked for
0487         qWarning() << "Could not read and prepare resourceIdForResource (in versioned resources)" << q.lastError();
0488         return -1;
0489     }
0490 
0491     q.bindValue(":filename", resourceFileName);
0492     q.bindValue(":resource_type", resourceType);
0493     q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
0494 
0495     if (!q.exec()) {
0496         qWarning() << "Could not query resourceIdForResource (in versioned resources)" << q.boundValues() << q.lastError();
0497         return -1;
0498     }
0499 
0500     if (q.first()) {
0501         return q.value(0).toInt();
0502     }
0503 
0504     // commenting out, because otherwise it spams the console on every new resource in the local resources folder
0505     // qWarning() << "Could not find resource" << resourceName << resourceFileName << resourceType << storageLocation;
0506     return -1;
0507 
0508 }
0509 
0510 bool KisResourceCacheDb::resourceNeedsUpdating(int resourceId, QDateTime timestamp)
0511 {
0512     QSqlQuery q;
0513     if (!q.prepare("SELECT timestamp\n"
0514                    "FROM   versioned_resources\n"
0515                    "WHERE  resource_id = :resource_id\n"
0516                    "AND    version = (SELECT MAX(version)\n"
0517                    "                  FROM   versioned_resources\n"
0518                    "                  WHERE  resource_id = :resource_id);")) {
0519         qWarning() << "Could not prepare resourceNeedsUpdating statement" << q.lastError();
0520         return false;
0521     }
0522 
0523     q.bindValue(":resource_id", resourceId);
0524 
0525     if (!q.exec()) {
0526         qWarning() << "Could not query for the most recent timestamp" << q.boundValues() << q.lastError();
0527         return false;
0528     }
0529 
0530     if (!q.first()) {
0531         qWarning() << "Inconsistent database: could not find a version for resource with Id" << resourceId;
0532         return false;
0533     }
0534 
0535     QVariant resourceTimeStamp = q.value(0);
0536 
0537     if (!resourceTimeStamp.isValid()) {
0538         qWarning() << "Could not retrieve timestamp from versioned_resources" << resourceId;
0539         return false;
0540     }
0541 
0542     return (timestamp.toSecsSinceEpoch() > resourceTimeStamp.toInt());
0543 }
0544 
0545 bool KisResourceCacheDb::addResourceVersion(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
0546 {
0547     bool r = false;
0548 
0549 
0550     r = addResourceVersionImpl(resourceId, timestamp, storage, resource);
0551 
0552     if (!r) return r;
0553 
0554     r = makeResourceTheCurrentVersion(resourceId, resource);
0555 
0556     return r;
0557 }
0558 
0559 bool KisResourceCacheDb::addResourceVersionImpl(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
0560 {
0561     bool r = false;
0562 
0563     // Create the new version. The resource is expected to have an updated version number, or
0564     // this will fail on the unique index on resource_id, storage_id and version.
0565     //
0566     // This function **only** adds to the versioned_resources table.
0567     // The resources table should be updated by the caller manually using
0568     // updateResourceTableForResourceIfNeeded()
0569 
0570     Q_ASSERT(resource->version() >= 0);
0571 
0572     QSqlQuery q;
0573     r = q.prepare("INSERT INTO versioned_resources \n"
0574                   "(resource_id, storage_id, version, filename, timestamp, md5sum)\n"
0575                   "VALUES\n"
0576                   "( :resource_id\n"
0577                   ", (SELECT id \n"
0578                   "   FROM   storages \n"
0579                   "   WHERE  location = :storage_location)\n"
0580                   ", :version\n"
0581                   ", :filename\n"
0582                   ", :timestamp\n"
0583                   ", :md5sum\n"
0584                   ");");
0585 
0586     if (!r) {
0587         qWarning() << "Could not prepare addResourceVersion statement" << q.lastError();
0588         return r;
0589     }
0590 
0591     q.bindValue(":resource_id", resourceId);
0592     q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
0593     q.bindValue(":version", resource->version());
0594     q.bindValue(":filename", resource->filename());
0595     q.bindValue(":timestamp", timestamp.toSecsSinceEpoch());
0596     KIS_SAFE_ASSERT_RECOVER_NOOP(!resource->md5Sum().isEmpty());
0597     q.bindValue(":md5sum", resource->md5Sum());
0598     r = q.exec();
0599     if (!r) {
0600 
0601         qWarning() << "Could not execute addResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << resource->name() << resource->filename() << "version" << resource->version();
0602         return r;
0603     }
0604 
0605     return r;
0606 }
0607 
0608 bool KisResourceCacheDb::removeResourceVersionImpl(int resourceId, int version, KisResourceStorageSP storage)
0609 {
0610     bool r = false;
0611 
0612     // Remove a version of the resource. This function **only** removes data from
0613     // the versioned_resources table. The resources table should be updated by
0614     // the caller manually using updateResourceTableForResourceIfNeeded()
0615 
0616     QSqlQuery q;
0617     r = q.prepare("DELETE FROM versioned_resources \n"
0618                   "WHERE resource_id = :resource_id\n"
0619                   "AND version = :version\n"
0620                   "AND storage_id = (SELECT id \n"
0621                   "                  FROM   storages \n"
0622                   "                  WHERE  location = :storage_location);");
0623 
0624     if (!r) {
0625         qWarning() << "Could not prepare removeResourceVersionImpl statement" << q.lastError();
0626         return r;
0627     }
0628 
0629     q.bindValue(":resource_id", resourceId);
0630     q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
0631     q.bindValue(":version", version);
0632     r = q.exec();
0633     if (!r) {
0634 
0635         qWarning() << "Could not execute removeResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << "version" << version;
0636         return r;
0637     }
0638 
0639     return r;
0640 }
0641 
0642 bool KisResourceCacheDb::updateResourceTableForResourceIfNeeded(int resourceId, const QString &resourceType, KisResourceStorageSP storage)
0643 {
0644     bool r = false;
0645 
0646     int maxVersion = -1;
0647     {
0648         QSqlQuery q;
0649         r = q.prepare("SELECT MAX(version)\n"
0650                       "FROM   versioned_resources\n"
0651                       "WHERE  resource_id = :resource_id;");
0652         if (!r) {
0653             qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
0654             return r;
0655         }
0656 
0657         q.bindValue(":resource_id", resourceId);
0658 
0659         r = q.exec();
0660         if (!r) {
0661             qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
0662             return r;
0663         }
0664 
0665         r = q.first();
0666         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(r, false);
0667 
0668         maxVersion = q.value(0).toInt();
0669     }
0670 
0671     QString maxVersionFilename;
0672     {
0673         QSqlQuery q;
0674         r = q.prepare("SELECT filename\n"
0675                       "FROM   versioned_resources\n"
0676                       "WHERE  resource_id = :resource_id\n"
0677                       "AND    version = :version;");
0678         if (!r) {
0679             qWarning() << "Could not prepare findMaxVersionFilename statement" << q.lastError();
0680             return r;
0681         }
0682 
0683         q.bindValue(":resource_id", resourceId);
0684         q.bindValue(":version", maxVersion);
0685 
0686         r = q.exec();
0687         if (!r) {
0688             qWarning() << "Could not execute findMaxVersionFilename query" << q.boundValues() << q.lastError();
0689             return r;
0690         }
0691 
0692         if (!q.first()) {
0693             return removeResourceCompletely(resourceId);
0694         } else {
0695             maxVersionFilename = q.value(0).toString();
0696         }
0697     }
0698 
0699     QString currentFilename;
0700     {
0701         QSqlQuery q;
0702         r = q.prepare("SELECT filename\n"
0703                       "FROM   resources\n"
0704                       "WHERE  id = :resource_id;");
0705         if (!r) {
0706             qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
0707             return r;
0708         }
0709 
0710         q.bindValue(":resource_id", resourceId);
0711 
0712         r = q.exec();
0713         if (!r) {
0714             qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
0715             return r;
0716         }
0717 
0718         r = q.first();
0719         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(r, false);
0720 
0721         currentFilename = q.value(0).toString();
0722     }
0723 
0724     if (currentFilename != maxVersionFilename) {
0725         const QString url = resourceType + "/" + maxVersionFilename;
0726         KoResourceSP resource = storage->resource(url);
0727         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(resource, false);
0728         resource->setVersion(maxVersion);
0729         resource->setMD5Sum(storage->resourceMd5(url));
0730         r = makeResourceTheCurrentVersion(resourceId, resource);
0731     }
0732 
0733     return r;
0734 }
0735 
0736 bool KisResourceCacheDb::makeResourceTheCurrentVersion(int resourceId, KoResourceSP resource)
0737 {
0738     bool r = false;
0739 
0740     QSqlQuery q;
0741     r = q.prepare("UPDATE resources\n"
0742                   "SET name    = :name\n"
0743                   ", filename  = :filename\n"
0744                   ", tooltip   = :tooltip\n"
0745                   ", thumbnail = :thumbnail\n"
0746                   ", status    = 1\n"
0747                   ", md5sum    = :md5sum\n"
0748                   "WHERE id    = :id");
0749     if (!r) {
0750         qWarning() << "Could not prepare updateResource statement" << q.lastError();
0751         return r;
0752     }
0753 
0754     q.bindValue(":name", resource->name());
0755     q.bindValue(":filename", resource->filename());
0756     q.bindValue(":tooltip", i18n(resource->name().toUtf8()));
0757     q.bindValue(":md5sum", resource->md5Sum());
0758 
0759     QBuffer buf;
0760     buf.open(QBuffer::WriteOnly);
0761     resource->thumbnail().save(&buf, "PNG");
0762     buf.close();
0763     q.bindValue(":thumbnail", buf.data());
0764     q.bindValue(":id", resourceId);
0765 
0766     r = q.exec();
0767     if (!r) {
0768         qWarning() << "Could not update resource" << q.boundValues() << q.lastError();
0769     }
0770 
0771     return r;
0772 }
0773 
0774 bool KisResourceCacheDb::removeResourceCompletely(int resourceId)
0775 {
0776     bool r = false;
0777 
0778     {
0779         QSqlQuery q;
0780         r = q.prepare("DELETE FROM versioned_resources \n"
0781                       "WHERE resource_id = :resource_id;");
0782 
0783         if (!r) {
0784             qWarning() << "Could not prepare removeResourceCompletely1 statement" << q.lastError();
0785             return r;
0786         }
0787 
0788         q.bindValue(":resource_id", resourceId);
0789         r = q.exec();
0790         if (!r) {
0791             qWarning() << "Could not execute removeResourceCompletely1 statement" << q.lastError() << resourceId;
0792             return r;
0793         }
0794     }
0795 
0796     {
0797         QSqlQuery q;
0798         r = q.prepare("DELETE FROM resources \n"
0799                       "WHERE id = :resource_id;");
0800 
0801         if (!r) {
0802             qWarning() << "Could not prepare removeResourceCompletely2 statement" << q.lastError();
0803             return r;
0804         }
0805 
0806         q.bindValue(":resource_id", resourceId);
0807         r = q.exec();
0808         if (!r) {
0809             qWarning() << "Could not execute removeResourceCompletely2 statement" << q.lastError() << resourceId;
0810             return r;
0811         }
0812     }
0813 
0814     {
0815         QSqlQuery q;
0816         r = q.prepare("DELETE FROM resource_tags \n"
0817                       "WHERE resource_id = :resource_id;");
0818 
0819         if (!r) {
0820             qWarning() << "Could not prepare removeResourceCompletely3 statement" << q.lastError();
0821             return r;
0822         }
0823 
0824         q.bindValue(":resource_id", resourceId);
0825         r = q.exec();
0826         if (!r) {
0827             qWarning() << "Could not execute removeResourceCompletely3 statement" << q.lastError() << resourceId;
0828             return r;
0829         }
0830     }
0831 
0832     return r;
0833 }
0834 
0835 bool KisResourceCacheDb::getResourceIdFromFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
0836 {
0837     QSqlQuery q;
0838 
0839     bool r = q.prepare("SELECT resources.id FROM resources\n"
0840                        ", resource_types\n"
0841                        ", storages\n"
0842                        "WHERE resources.filename = :filename\n" // bind to filename
0843                        "AND resource_types.id = resources.resource_type_id\n"  // join resources_types + resources
0844                        "AND resource_types.name = :resourceType\n" // bind to resource type
0845                        "AND resources.storage_id = storages.id\n" // join resources + storages
0846                        "AND storages.location = :storageLocation"); // bind to storage location
0847 
0848     if (!r) {
0849         qWarning() << "Could not prepare getResourceIdFromFilename statement" << q.lastError() << q.executedQuery();
0850         return r;
0851     }
0852 
0853     q.bindValue(":filename", filename);
0854     q.bindValue(":resourceType", resourceType);
0855     q.bindValue(":storageLocation",  changeToEmptyIfNull(storageLocation));
0856 
0857     r = q.exec();
0858     if (!r) {
0859         qWarning() << "Could not execute getResourceIdFromFilename statement" << q.lastError() << filename << resourceType;
0860         return r;
0861     }
0862 
0863     r = q.first();
0864     if (r) {
0865         outResourceId = q.value("resources.id").toInt();
0866     }
0867 
0868     return r;
0869 }
0870 
0871 bool KisResourceCacheDb::getResourceIdFromVersionedFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
0872 {
0873     QSqlQuery q;
0874 
0875     bool r = q.prepare("SELECT resource_id FROM versioned_resources\n"
0876                        ", resources\n"
0877                        ", resource_types\n"
0878                        ", storages\n"
0879                        "WHERE versioned_resources.filename = :filename\n" // bind to filename
0880                        "AND resources.id = versioned_resources.resource_id\n" // join resources + versioned_resources
0881                        "AND resource_types.id = resources.resource_type_id\n"  // join resources_types + resources
0882                        "AND resource_types.name = :resourceType\n" // bind to resource type
0883                        "AND resources.storage_id = storages.id\n" // join resources + storages
0884                        "AND storages.location = :storageLocation"); // bind to storage location
0885 
0886     if (!r) {
0887         qWarning() << "Could not prepare getResourceIdFromVersionedFilename statement" << q.lastError() << q.executedQuery();
0888         return r;
0889     }
0890 
0891 
0892     q.bindValue(":filename", filename);
0893     q.bindValue(":resourceType", resourceType);
0894     q.bindValue(":storageLocation",  changeToEmptyIfNull(storageLocation));
0895 
0896     r = q.exec();
0897     if (!r) {
0898         qWarning() << "Could not execute getResourceIdFromVersionedFilename statement" << q.lastError() << filename << resourceType;
0899         return r;
0900     }
0901 
0902     r = q.first();
0903     if (r) {
0904         outResourceId = q.value("resource_id").toInt();
0905     }
0906 
0907     return r;
0908 }
0909 
0910 bool KisResourceCacheDb::getAllVersionsLocations(int resourceId, QStringList &outVersionsLocationsList)
0911 {
0912     QSqlQuery q;
0913     bool r = q.prepare("SELECT filename FROM versioned_resources \n"
0914                   "WHERE resource_id = :resource_id;");
0915 
0916     if (!r) {
0917         qWarning() << "Could not prepare getAllVersionsLocations statement" << q.lastError();
0918         return r;
0919     }
0920 
0921     q.bindValue(":resource_id", resourceId);
0922     r = q.exec();
0923     if (!r) {
0924         qWarning() << "Could not execute getAllVersionsLocations statement" << q.lastError() << resourceId;
0925         return r;
0926     }
0927 
0928     outVersionsLocationsList = QStringList();
0929     while (q.next()) {
0930         outVersionsLocationsList << q.value("filename").toString();
0931     }
0932 
0933     return r;
0934 
0935 }
0936 
0937 bool KisResourceCacheDb::addResource(KisResourceStorageSP storage, QDateTime timestamp, KoResourceSP resource, const QString &resourceType)
0938 {
0939     bool r = false;
0940 
0941     if (!s_valid) {
0942         qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
0943         return false;
0944     }
0945 
0946     if (!resource || !resource->valid()) {
0947         qWarning() << "KisResourceCacheDb::addResource: The resource is not valid:" << resource->filename();
0948         // We don't care about invalid resources and will just ignore them.
0949         return true;
0950     }
0951     bool temporary = (storage->type() == KisResourceStorage::StorageType::Memory);
0952 
0953     // Check whether it already exists
0954     int resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
0955     if (resourceId > -1) {
0956         return true;
0957     }
0958 
0959     QSqlQuery q;
0960     r = q.prepare("INSERT INTO resources \n"
0961                   "(storage_id, resource_type_id, name, filename, tooltip, thumbnail, status, temporary, md5sum) \n"
0962                   "VALUES \n"
0963                   "((SELECT  id "
0964                   "  FROM    storages "
0965                   "  WHERE   location = :storage_location)\n"
0966                   ", (SELECT id\n"
0967                   "   FROM   resource_types\n"
0968                   "   WHERE  name = :resource_type)\n"
0969                   ", :name\n"
0970                   ", :filename\n"
0971                   ", :tooltip\n"
0972                   ", :thumbnail\n"
0973                   ", :status\n"
0974                   ", :temporary\n"
0975                   ", :md5sum)");
0976 
0977     if (!r) {
0978         qWarning() << "Could not prepare addResource statement" << q.lastError();
0979         return r;
0980     }
0981 
0982     q.bindValue(":resource_type", resourceType);
0983     q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
0984     q.bindValue(":name", resource->name());
0985     q.bindValue(":filename", resource->filename());
0986 
0987     QString translationContext;
0988     if (storage->type() == KisResourceStorage::StorageType::Bundle) {
0989         translationContext = "./krita/data/bundles/" + KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())
0990                 + ":" + resourceType + "/" + resource->filename();
0991     } else if (storage->location() == "memory") {
0992         translationContext = "memory/" + resourceType + "/" + resource->filename();
0993     }
0994     else if (resource->filename().endsWith(".myb", Qt::CaseInsensitive)) {
0995         translationContext = "./plugins/paintops/mypaint/brushes/" + resource->filename();
0996     } else {
0997         translationContext = "./krita/data/" + resourceType + "/" + resource->filename();
0998     }
0999 
1000     {
1001         QByteArray ctx = translationContext.toUtf8();
1002         QString translatedName = i18nc(ctx, resource->name().toUtf8());
1003         if (translatedName == resource->name()) {
1004             // Try using the file name without the file extension, and replaces '_' with spaces.
1005             QString altName = QFileInfo(resource->filename()).completeBaseName().replace('_', ' ');
1006             QString altTranslatedName = i18nc(ctx, altName.toUtf8());
1007             if (altName != altTranslatedName) {
1008                 translatedName = altTranslatedName;
1009             }
1010         }
1011         q.bindValue(":tooltip", translatedName);
1012     }
1013 
1014     QBuffer buf;
1015     buf.open(QBuffer::WriteOnly);
1016     resource->image().save(&buf, "PNG");
1017     buf.close();
1018     q.bindValue(":thumbnail", buf.data());
1019 
1020     q.bindValue(":status", resource->active());
1021     q.bindValue(":temporary", (temporary ? 1 : 0));
1022     q.bindValue(":md5sum", resource->md5Sum());
1023 
1024     r = q.exec();
1025     if (!r) {
1026         qWarning() << "Could not execute addResource statement" << q.lastError() << q.boundValues();
1027         return r;
1028     }
1029     resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1030 
1031     if (resourceId < 0) {
1032 
1033         qWarning() << "Adding to database failed, resource id after adding is " << resourceId << "! (Probable reason: the resource has the same filename, storage, resource type as an existing resource). Resource is: "
1034                    << resource->name()
1035                    << resource->filename()
1036                    << resourceType
1037                    << KisResourceLocator::instance()->makeStorageLocationRelative(storage->location());
1038         return false;
1039     }
1040 
1041     resource->setResourceId(resourceId);
1042 
1043     if (!addResourceVersionImpl(resourceId, timestamp, storage, resource)) {
1044         qWarning() << "Could not add resource version" << resource;
1045         return false;
1046     }
1047 
1048     if (!resource->metadata().isEmpty()) {
1049         return addMetaDataForId(resource->metadata(), resource->resourceId(), "resources");
1050     }
1051 
1052     return true;
1053 
1054 
1055 }
1056 
1057 bool KisResourceCacheDb::addResources(KisResourceStorageSP storage, QString resourceType)
1058 {
1059     QSqlDatabase::database().transaction();
1060     QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1061     while (iter->hasNext()) {
1062         iter->next();
1063 
1064         QSharedPointer<KisResourceStorage::ResourceIterator> verIt =
1065             iter->versions();
1066 
1067         int resourceId = -1;
1068 
1069         while (verIt->hasNext()) {
1070             verIt->next();
1071 
1072             KoResourceSP resource = verIt->resource();
1073             if (resource && resource->valid()) {
1074                 resource->setVersion(verIt->guessedVersion());
1075                 resource->setMD5Sum(storage->resourceMd5(verIt->url()));
1076 
1077                 if (resourceId < 0) {
1078                     if (addResource(storage, iter->lastModified(), resource, iter->type())) {
1079                         resourceId = resource->resourceId();
1080                     } else {
1081                         qWarning() << "Could not add resource" << resource->filename() << "to the database";
1082                     }
1083                 } else {
1084                     if (!addResourceVersion(resourceId, iter->lastModified(), storage, resource)) {
1085                         qWarning() << "Could not add resource version" << resource->filename() << "to the database";
1086                     }
1087                 }
1088             }
1089         }
1090     }
1091     QSqlDatabase::database().commit();
1092     return true;
1093 }
1094 
1095 bool KisResourceCacheDb::setResourceActive(int resourceId, bool active)
1096 {
1097     if (resourceId < 0) {
1098         qWarning() << "Invalid resource id; cannot remove resource";
1099         return false;
1100     }
1101     QSqlQuery q;
1102     bool r = q.prepare("UPDATE resources\n"
1103                        "SET    status = :status\n"
1104                        "WHERE  id = :resource_id");
1105     if (!r) {
1106         qWarning() << "Could not prepare removeResource query" << q.lastError();
1107     }
1108     q.bindValue(":status", active);
1109     q.bindValue(":resource_id", resourceId);
1110     if (!q.exec()) {
1111         qWarning() << "Could not update resource" << resourceId << "to  inactive" << q.lastError();
1112         return false;
1113     }
1114 
1115     return true;
1116 }
1117 
1118 bool KisResourceCacheDb::tagResource(const QString &resourceFileName, KisTagSP tag, const QString &resourceType)
1119 {
1120     // Get tag id
1121     int tagId {-1};
1122     {
1123         QFile f(":/select_tag.sql");
1124         if (f.open(QFile::ReadOnly)) {
1125             QSqlQuery q;
1126             if (!q.prepare(f.readAll())) {
1127                 qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1128                 return false;
1129             }
1130             q.bindValue(":url", tag->url());
1131             q.bindValue(":resource_type", resourceType);
1132 
1133             if (!q.exec()) {
1134                 qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1135                 return false;
1136             }
1137 
1138             if (!q.first()) {
1139                 qWarning() << "Could not find tag" << q.boundValues() << q.lastError();
1140                 return false;
1141             }
1142 
1143             tagId = q.value(0).toInt();
1144         }
1145     }
1146 
1147 
1148     // Get resource id
1149     QSqlQuery q;
1150     bool r = q.prepare("SELECT resources.id\n"
1151                        "FROM   resources\n"
1152                        ",      resource_types\n"
1153                        "WHERE  resources.resource_type_id = resource_types.id\n"
1154                        "AND    resource_types.name = :resource_type\n"
1155                        "AND    resources.filename = :resource_filename\n");
1156     if (!r) {
1157         qWarning() << "Could not prepare tagResource query" << q.lastError();
1158         return false;
1159     }
1160 
1161     q.bindValue(":resource_type", resourceType);
1162     q.bindValue(":resource_filename", resourceFileName);
1163 
1164     if (!q.exec()) {
1165         qWarning() << "Could not execute tagResource statement" << q.boundValues() << q.lastError();
1166         return false;
1167     }
1168 
1169 
1170     while (q.next()) {
1171 
1172         int resourceId = q.value(0).toInt();
1173 
1174         if (resourceId < 0) {
1175             qWarning() << "Could not find resource to tag" << resourceFileName << resourceType;
1176             continue;
1177         }
1178 
1179         {
1180             QSqlQuery q;
1181             if (!q.prepare("SELECT COUNT(*)\n"
1182                            "FROM   resource_tags\n"
1183                            "WHERE  resource_id = :resource_id\n"
1184                            "AND    tag_id = :tag_id")) {
1185                 qWarning() << "Could not prepare tagResource query 2" << q.lastError();
1186                 continue;
1187             }
1188             q.bindValue(":resource_id", resourceId);
1189             q.bindValue(":tag_id", tagId);
1190 
1191             if (!q.exec()) {
1192                 qWarning() << "Could not execute tagResource query 2" << q.lastError() << q.boundValues();
1193                 continue;
1194             }
1195 
1196             q.first();
1197             int count = q.value(0).toInt();
1198             if (count > 0) {
1199                 continue;
1200             }
1201         }
1202 
1203         {
1204             QSqlQuery q;
1205             if (!q.prepare("INSERT INTO resource_tags\n"
1206                            "(resource_id, tag_id)\n"
1207                            "VALUES\n"
1208                            "(:resource_id, :tag_id);")) {
1209                 qWarning() << "Could not prepare tagResource insert statement" << q.lastError();
1210                 continue;
1211             }
1212 
1213             q.bindValue(":resource_id", resourceId);
1214             q.bindValue(":tag_id", tagId);
1215 
1216             if (!q.exec()) {
1217                 qWarning() << "Could not execute tagResource stagement" << q.boundValues() << q.lastError();
1218                 continue;
1219             }
1220         }
1221     }
1222     return true;
1223 }
1224 
1225 bool KisResourceCacheDb::hasTag(const QString &url, const QString &resourceType)
1226 {
1227     QFile f(":/select_tag.sql");
1228     if (f.open(QFile::ReadOnly)) {
1229         QSqlQuery q;
1230         if (!q.prepare(f.readAll())) {
1231             qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1232             return false;
1233         }
1234         q.bindValue(":url", url);
1235         q.bindValue(":resource_type", resourceType);
1236         if (!q.exec()) {
1237             qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1238         }
1239         return q.first();
1240     }
1241     qWarning() << "Could not open select_tag.sql";
1242     return false;
1243 }
1244 
1245 bool KisResourceCacheDb::linkTagToStorage(const QString &url, const QString &resourceType, const QString &storageLocation)
1246 {
1247     QSqlQuery q;
1248     if (!q.prepare("INSERT INTO tags_storages\n"
1249                    "(tag_id, storage_id)\n"
1250                    "VALUES\n"
1251                    "(\n"
1252                    " ( SELECT id\n"
1253                    "   FROM  tags\n"
1254                    "   WHERE url = :url\n"
1255                    "   AND   resource_type_id = (SELECT id \n"
1256                    "                              FROM   resource_types\n"
1257                    "                              WHERE  name = :resource_type)"
1258                    " )\n"
1259                    ",( SELECT id\n"
1260                    "   FROM   storages\n"
1261                    "   WHERE  location = :storage_location\n"
1262                    " )\n"
1263                    ");")) {
1264         qWarning() << "Could not prepare add tag/storage statement" << q.lastError();
1265         return false;
1266     }
1267 
1268     q.bindValue(":url", url);
1269     q.bindValue(":resource_type", resourceType);
1270     q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storageLocation)));
1271 
1272     if (!q.exec()) {
1273         qWarning() << "Could not insert tag/storage link" << q.boundValues() << q.lastError();
1274         return false;
1275     }
1276     return true;
1277 }
1278 
1279 
1280 bool KisResourceCacheDb::addTag(const QString &resourceType, const QString storageLocation, KisTagSP tag)
1281 {
1282 
1283     if (hasTag(tag->url(), resourceType)) {
1284         // Check whether this storage is already registered for this tag
1285         QSqlQuery q;
1286         if (!q.prepare("SELECT storages.location\n"
1287                        "FROM   tags_storages\n"
1288                        ",      tags\n"
1289                        ",      storages\n"
1290                        "WHERE  tags.id = tags_storages.tag_id\n"
1291                        "AND    storages.id = tags_storages.storage_id\n"
1292                        "AND    tags.resource_type_id = (SELECT id\n"
1293                        "                                FROM   resource_types\n"
1294                        "                                WHERE  name = :resource_type)\n"
1295                        "AND    tags.url = :url"))
1296         {
1297             qWarning() << "Could not prepare select tags from tags_storages query" << q.lastError();
1298         }
1299 
1300         q.bindValue(":url", tag->url());
1301         q.bindValue(":resource_type", resourceType);
1302 
1303         if (!q.exec()) {
1304             qWarning() << "Could not execute tags_storages query" << q.boundValues() << q.lastError();
1305         }
1306 
1307         // If this tag is not yet linked to the storage, link it
1308         if (!q.first()) {
1309             return linkTagToStorage(tag->url(), resourceType, storageLocation);
1310         }
1311 
1312         return true;
1313     }
1314 
1315     int tagId;
1316 
1317     // Insert the tag
1318     {
1319         QSqlQuery q;
1320         if (!q.prepare("INSERT INTO tags\n"
1321                        "(url, name, comment, resource_type_id, active, filename)\n"
1322                        "VALUES\n"
1323                        "( :url\n"
1324                        ", :name\n"
1325                        ", :comment\n"
1326                        ", (SELECT id\n"
1327                        "   FROM   resource_types\n"
1328                        "   WHERE  name = :resource_type)\n"
1329                        ", 1\n"
1330                        ", :filename\n"
1331                        ");")) {
1332             qWarning() << "Could not prepare insert tag statement" << q.lastError();
1333             return false;
1334         }
1335 
1336         q.bindValue(":url", tag->url());
1337         q.bindValue(":name", tag->name(false));
1338         q.bindValue(":comment", tag->comment(false));
1339         q.bindValue(":resource_type", resourceType);
1340         q.bindValue(":filename", tag->filename());
1341 
1342         if (!q.exec()) {
1343             qWarning() << "Could not insert tag" << q.boundValues() << q.lastError();
1344         }
1345 
1346         tagId = q.lastInsertId().toInt();
1347     }
1348 
1349     {
1350         Q_FOREACH(const QString language, tag->names().keys()) {
1351 
1352             QString name = tag->names()[language];
1353             QString comment = name;
1354             if (tag->comments().contains(language)) {
1355                 comment = tag->comments()[language];
1356             }
1357 
1358             QSqlQuery q;
1359             if (!q.prepare("INSERT INTO tag_translations\n"
1360                            "( tag_id\n"
1361                            ", language\n"
1362                            ", name\n"
1363                            ", comment\n"
1364                            ")\n"
1365                            "VALUES\n"
1366                            "( :id\n"
1367                            ", :language\n"
1368                            ", :name\n"
1369                            ", :comment\n"
1370                            ");")) {
1371                 qWarning() << "Could not prepare insert tag_translation query" << q.lastError();
1372             }
1373 
1374             q.bindValue(":id", tagId);
1375             q.bindValue(":language", language);
1376             q.bindValue(":name", name);
1377             q.bindValue(":comment", comment);
1378 
1379             if (!q.exec()) {
1380                 qWarning() << "Could not execute insert tag_translation query" << q.lastError() << q.boundValues();
1381             }
1382         }
1383 
1384     }
1385 
1386 
1387     linkTagToStorage(tag->url(), resourceType, storageLocation);
1388 
1389     return true;
1390 }
1391 
1392 bool KisResourceCacheDb::addTags(KisResourceStorageSP storage, QString resourceType)
1393 {
1394     QSqlDatabase::database().transaction();
1395     QSharedPointer<KisResourceStorage::TagIterator> iter = storage->tags(resourceType);
1396     while(iter->hasNext()) {
1397         iter->next();
1398         KisTagSP tag = iter->tag();
1399         if (tag && tag->valid()) {
1400             if (!addTag(resourceType, storage->location(), tag)) {
1401                 qWarning() << "Could not add tag" << tag << "to the database";
1402                 continue;
1403             }
1404             if (!tag->defaultResources().isEmpty()) {
1405                 Q_FOREACH(const QString &resourceFileName, tag->defaultResources()) {
1406                     if (!tagResource(resourceFileName, tag, resourceType)) {
1407                         qWarning() << "Could not tag resource" << QFileInfo(resourceFileName).baseName() << "from" << storage->name() << "filename" << resourceFileName << "with tag" << iter->tag();
1408                     }
1409                 }
1410             }
1411         }
1412     }
1413     QSqlDatabase::database().commit();
1414     return true;
1415 }
1416 
1417 bool KisResourceCacheDb::addStorage(KisResourceStorageSP storage, bool preinstalled)
1418 {
1419     bool r = true;
1420 
1421     if (!s_valid) {
1422         qWarning() << "The database is not valid";
1423         return false;
1424     }
1425 
1426     {
1427         QSqlQuery q;
1428         r = q.prepare("SELECT * FROM storages WHERE location = :location");
1429         q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1430         r = q.exec();
1431         if (!r) {
1432             qWarning() << "Could not select from storages";
1433             return r;
1434         }
1435         if (q.first()) {
1436             debugResource << "Storage already exists" << storage;
1437             return true;
1438         }
1439     }
1440 
1441     // Insert the storage;
1442     {
1443         QSqlQuery q;
1444 
1445         r = q.prepare("INSERT INTO storages\n "
1446                       "(storage_type_id, location, timestamp, pre_installed, active, thumbnail)\n"
1447                       "VALUES\n"
1448                       "(:storage_type_id, :location, :timestamp, :pre_installed, :active, :thumbnail);");
1449 
1450         if (!r) {
1451             qWarning() << "Could not prepare query" << q.lastError();
1452             return r;
1453         }
1454 
1455         q.bindValue(":storage_type_id", static_cast<int>(storage->type()));
1456         q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1457         q.bindValue(":timestamp", storage->timestamp().toSecsSinceEpoch());
1458         q.bindValue(":pre_installed", preinstalled ? 1 : 0);
1459         q.bindValue(":active", !disabledBundles.contains(storage->name()));
1460 
1461         QBuffer buf;
1462         buf.open(QBuffer::WriteOnly);
1463         storage->thumbnail().save(&buf, "PNG");
1464         buf.close();
1465         q.bindValue(":thumbnail", buf.data());
1466 
1467         r = q.exec();
1468 
1469         if (!r) qWarning() << "Could not execute query" << q.lastError();
1470 
1471     }
1472 
1473     // Insert the metadata
1474     {
1475         QStringList keys = storage->metaDataKeys();
1476         if (keys.size() > 0) {
1477 
1478             QSqlQuery q;
1479             if (!q.prepare("SELECT MAX(id)\n"
1480                            "FROM   storages\n")) {
1481                 qWarning() << "Could not create select storages query for metadata" << q.lastError();
1482             }
1483 
1484             if (!q.exec()) {
1485                 qWarning() << "Could not execute select storages query for metadata" << q.lastError();
1486             }
1487 
1488             q.first();
1489             int id = q.value(0).toInt();
1490 
1491             QMap<QString, QVariant> metadata;
1492 
1493             Q_FOREACH(const QString &key, storage->metaDataKeys()) {
1494                 metadata[key] = storage->metaData(key);
1495             }
1496 
1497             addMetaDataForId(metadata, id, "storages");
1498         }
1499     }
1500 
1501     Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1502         if (!KisResourceCacheDb::addResources(storage, resourceType)) {
1503             qWarning() << "Failed to add all resources for storage" << storage;
1504             r = false;
1505         }
1506     }
1507 
1508     return r;
1509 }
1510 
1511 bool KisResourceCacheDb::addStorageTags(KisResourceStorageSP storage)
1512 {
1513 
1514     bool r = true;
1515     Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1516         if (!KisResourceCacheDb::addTags(storage, resourceType)) {
1517             qWarning() << "Failed to add all tags for storage" << storage;
1518             r = false;
1519         }
1520     }
1521     return r;
1522 }
1523 
1524 bool KisResourceCacheDb::deleteStorage(QString location)
1525 {
1526     // location is already relative
1527     {
1528         QSqlQuery q;
1529         if (!q.prepare("DELETE FROM resources\n"
1530                        "WHERE       id IN (SELECT versioned_resources.resource_id\n"
1531                        "                   FROM   versioned_resources\n"
1532                        "                   WHERE  versioned_resources.storage_id = (SELECT storages.id\n"
1533                        "                                                            FROM   storages\n"
1534                        "                                                            WHERE storages.location = :location)\n"
1535                        "                   );")) {
1536             qWarning() << "Could not prepare delete resources query in deleteStorage" << q.lastError();
1537             return false;
1538         }
1539         q.bindValue(":location", changeToEmptyIfNull(location));
1540         if (!q.exec()) {
1541             qWarning() << "Could not execute delete resources query in deleteStorage" << q.lastError();
1542             return false;
1543         }
1544     }
1545 
1546     {
1547         QSqlQuery q;
1548         if (!q.prepare("DELETE FROM tags \n"
1549                        "WHERE id IN (SELECT tags_storages.tag_id \n "
1550                        "             FROM tags_storages \n"
1551                        "             WHERE tags_storages.storage_id = \n"
1552                        "                   (SELECT storages.id\n"
1553                        "                    FROM   storages\n"
1554                        "                    WHERE  storages.location = :location)\n"
1555                        "           );")) {
1556             qWarning() << "Could not prepare delete tag query" << q.lastError();
1557             return false;
1558         }
1559         q.bindValue(":location", location);
1560         if (!q.exec()) {
1561             qWarning() << "Could not execute delete tag query" << q.lastError();
1562             return false;
1563         }
1564     }
1565 
1566     {
1567         QSqlQuery q;
1568         if (!q.prepare("DELETE FROM tags_storages \n"
1569                        "       WHERE tags_storages.storage_id = \n"
1570                        "             (SELECT storages.id\n"
1571                        "              FROM   storages\n"
1572                        "              WHERE  storages.location = :location);")) {
1573             qWarning() << "Could not prepare delete tag storage query" << q.lastError();
1574             return false;
1575         }
1576         q.bindValue(":location", location);
1577         if (!q.exec()) {
1578             qWarning() << "Could not execute delete tag storage query" << q.lastError();
1579             return false;
1580         }
1581     }
1582 
1583     {
1584         QSqlQuery q;
1585         if (!q.prepare("DELETE FROM versioned_resources\n"
1586                        "WHERE storage_id = (SELECT storages.id\n"
1587                        "                    FROM   storages\n"
1588                        "                    WHERE  storages.location = :location);")) {
1589             qWarning() << "Could not prepare delete versioned_resources query" << q.lastError();
1590             return false;
1591         }
1592         q.bindValue(":location", changeToEmptyIfNull(location));
1593         if (!q.exec()) {
1594             qWarning() << "Could not execute delete versioned_resources query" << q.lastError();
1595             return false;
1596         }
1597     }
1598 
1599     {
1600         QSqlQuery q;
1601         if (!q.prepare("DELETE FROM storages\n"
1602                        "WHERE location = :location;")) {
1603             qWarning() << "Could not prepare delete storages query" << q.lastError();
1604             return false;
1605         }
1606         q.bindValue(":location", changeToEmptyIfNull(location));
1607         if (!q.exec()) {
1608             qWarning() << "Could not execute delete storages query" << q.lastError();
1609             return false;
1610         }
1611     }
1612     return true;
1613 }
1614 
1615 bool KisResourceCacheDb::deleteStorage(KisResourceStorageSP storage)
1616 {
1617     return deleteStorage(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1618 }
1619 
1620 namespace {
1621 struct ResourceVersion : public boost::less_than_comparable<ResourceVersion>
1622 {
1623     int resourceId = -1;
1624     int version = -1;
1625     QDateTime timestamp;
1626     QString url;
1627 
1628     bool operator<(const ResourceVersion &rhs) const {
1629         return resourceId < rhs.resourceId ||
1630                 (resourceId == rhs.resourceId && version < rhs.version);
1631     }
1632 
1633     struct CompareByResourceId {
1634         bool operator() (const ResourceVersion &lhs, const ResourceVersion &rhs) const {
1635             return lhs.resourceId < rhs.resourceId;
1636         }
1637     };
1638 
1639 
1640 };
1641 
1642 [[maybe_unused]]
1643 QDebug operator<<(QDebug dbg, const ResourceVersion &ver)
1644 {
1645     dbg.nospace() << "ResourceVersion("
1646                   << ver.resourceId << ", "
1647                   << ver.version << ", "
1648                   << ver.timestamp << ", "
1649                   << ver.url << ")";
1650 
1651     return dbg.space();
1652 }
1653 }
1654 
1655 bool KisResourceCacheDb::synchronizeStorage(KisResourceStorageSP storage)
1656 {
1657     QElapsedTimer t;
1658     t.start();
1659 
1660     QSqlDatabase::database().transaction();
1661 
1662     if (!s_valid) {
1663         qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
1664         return false;
1665     }
1666 
1667     bool success = true;
1668 
1669     // Find the storage in the database
1670     QSqlQuery q;
1671     if (!q.prepare("SELECT id\n"
1672                    ",      timestamp\n"
1673                    ",      pre_installed\n"
1674                    "FROM   storages\n"
1675                    "WHERE  location = :location\n")) {
1676         qWarning() << "Could not prepare storage timestamp statement" << q.lastError();
1677     }
1678 
1679     q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1680     if (!q.exec()) {
1681         qWarning() << "Could not execute storage timestamp statement" << q.boundValues() << q.lastError();
1682     }
1683 
1684     if (!q.first()) {
1685         // This is a new storage, the user must have dropped it in the path before restarting Krita, so add it.
1686         debugResource << "Adding storage to the database:" << storage;
1687         if (!addStorage(storage, false)) {
1688             qWarning() << "Could not add new storage" << storage->name() << "to the database";
1689             success = false;
1690         }
1691         return true;
1692     }
1693 
1694     storage->setStorageId(q.value("id").toInt());
1695 
1696     /// We compare resource versions one-by-one because the storage may have multiple
1697     /// versions of them
1698 
1699     Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1700 
1701         /// Firstly, fetch information about the existing resources
1702         /// in the storage
1703 
1704         QVector<ResourceVersion> resourcesInStorage;
1705 
1706         /// A fake resourceId to group resources which are not yet present
1707         /// in the database. This value is always negative, therefore it
1708         /// cannot overlap with normal ids.
1709 
1710         int nextInexistentResourceId = std::numeric_limits<int>::min();
1711 
1712         QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1713         while (iter->hasNext()) {
1714             iter->next();
1715 
1716             const int firstResourceVersionPosition = resourcesInStorage.size();
1717 
1718             int detectedResourceId = nextInexistentResourceId;
1719             QSharedPointer<KisResourceStorage::ResourceIterator> verIt =
1720                     iter->versions();
1721 
1722             while (verIt->hasNext()) {
1723                 verIt->next();
1724 
1725                 // verIt->url() contains paths like "brushes/ink.png" or "brushes/subfolder/splash.png".
1726                 // we need to cut off the first part and get "ink.png" in the first case,
1727                 // but "subfolder/splash.png" in the second case in order for subfolders to work
1728                 // so it cannot just use QFileInfo(verIt->url()).fileName() here.
1729                 QString path = QDir::fromNativeSeparators(verIt->url()); // make sure it uses Unix separators
1730                 int folderEndIdx = path.indexOf("/");
1731                 QString properFilenameWithSubfolders = path.right(path.length() - folderEndIdx - 1);
1732                 int id = resourceIdForResource(properFilenameWithSubfolders,
1733                                                verIt->type(),
1734                                                KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1735 
1736                 ResourceVersion item;
1737                 item.url = verIt->url();
1738                 item.version = verIt->guessedVersion();
1739 
1740                 // we use lower precision than the normal QDateTime
1741                 item.timestamp = QDateTime::fromSecsSinceEpoch(verIt->lastModified().toSecsSinceEpoch());
1742 
1743                 item.resourceId = id;
1744 
1745                 if (detectedResourceId < 0 && id >= 0) {
1746                     detectedResourceId = id;
1747                 }
1748 
1749                 resourcesInStorage.append(item);
1750             }
1751 
1752             /// Assign the detected resource id to all the versions of
1753             /// this resource (if they are not present in the database).
1754             /// If no id has been detected, then a fake one will be assigned.
1755 
1756             for (int i = firstResourceVersionPosition; i < resourcesInStorage.size(); i++) {
1757                 if (resourcesInStorage[i].resourceId < 0) {
1758                     resourcesInStorage[i].resourceId = detectedResourceId;
1759                 }
1760             }
1761 
1762             nextInexistentResourceId++;
1763         }
1764 
1765 
1766         /// Secondly, fetch the resources present in the database
1767 
1768         QVector<ResourceVersion> resourcesInDatabase;
1769 
1770         QSqlQuery q;
1771         q.setForwardOnly(true);
1772         if (!q.prepare("SELECT versioned_resources.resource_id, versioned_resources.filename, versioned_resources.version, versioned_resources.timestamp\n"
1773                        "FROM   versioned_resources\n"
1774                        ",      resource_types\n"
1775                        ",      resources\n"
1776                        "WHERE  resources.resource_type_id = resource_types.id\n"
1777                        "AND    resources.id = versioned_resources.resource_id\n"
1778                        "AND    resource_types.name = :resource_type\n"
1779                        "AND    versioned_resources.storage_id == :storage_id")) {
1780             qWarning() << "Could not prepare resource by type query" << q.lastError();
1781             success = false;
1782             continue;
1783         }
1784 
1785         q.bindValue(":resource_type", resourceType);
1786         q.bindValue(":storage_id", int(storage->storageId()));
1787 
1788         if (!q.exec()) {
1789             qWarning() << "Could not exec resource by type query" << q.boundValues() << q.lastError();
1790             success = false;
1791             continue;
1792         }
1793 
1794         while (q.next()) {
1795             ResourceVersion item;
1796             item.url = resourceType + "/" + q.value(1).toString();
1797             item.version = q.value(2).toInt();
1798             item.timestamp = QDateTime::fromSecsSinceEpoch(q.value(3).toInt());
1799             item.resourceId = q.value(0).toInt();
1800 
1801             resourcesInDatabase.append(item);
1802         }
1803 
1804         QSet<int> resourceIdForUpdate;
1805 
1806         std::sort(resourcesInStorage.begin(), resourcesInStorage.end());
1807         std::sort(resourcesInDatabase.begin(), resourcesInDatabase.end());
1808 
1809         auto itA = resourcesInStorage.begin();
1810         auto endA = resourcesInStorage.end();
1811 
1812         auto itB = resourcesInDatabase.begin();
1813         auto endB = resourcesInDatabase.end();
1814 
1815         /// The head of itA array contains some resources with fake
1816         /// (negative) resourceId. These resources are obviously new
1817         /// resources and should be added to the cache database.
1818 
1819         while (itA != endA) {
1820             if (itA->resourceId >= 0) break;
1821 
1822             KoResourceSP res = storage->resource(itA->url);
1823 
1824             if (!res) {
1825                 KisUsageLogger::log("Could not load resource " + itA->url);
1826                 ++itA;
1827                 continue;
1828             }
1829 
1830             res->setVersion(itA->version);
1831             res->setMD5Sum(storage->resourceMd5(itA->url));
1832             if (!res->valid()) {
1833                 KisUsageLogger::log("Could not retrieve md5 for resource " + itA->url);
1834                 ++itA;
1835                 continue;
1836             }
1837 
1838             const bool retval = addResource(storage, itA->timestamp, res, resourceType);
1839             if (!retval) {
1840                 KisUsageLogger::log("Could not add resource " + itA->url);
1841                 ++itA;
1842                 continue;
1843             }
1844 
1845             const int resourceId = res->resourceId();
1846             KIS_SAFE_ASSERT_RECOVER(resourceId >= 0) {
1847                 KisUsageLogger::log("Could not get id for resource " + itA->url);
1848                 ++itA;
1849                 continue;
1850             }
1851 
1852             auto nextResource = std::upper_bound(itA, endA, *itA, ResourceVersion::CompareByResourceId());
1853             for (auto it = std::next(itA); it != nextResource; ++it) {
1854                 KoResourceSP res = storage->resource(it->url);
1855                 res->setVersion(it->version);
1856                 res->setMD5Sum(storage->resourceMd5(it->url));
1857                 if (!res->valid()) {
1858                     continue;
1859                 }
1860 
1861                 const bool retval = addResourceVersion(resourceId, it->timestamp, storage, res);
1862                 KIS_SAFE_ASSERT_RECOVER(retval) {
1863                     KisUsageLogger::log("Could not add version for resource " + itA->url);
1864                     continue;
1865                 }
1866             }
1867 
1868             itA = nextResource;
1869         }
1870 
1871         /// Now both arrays are sorted in resourceId/version/timestamp
1872         /// order. It lets us easily find the resources that are unique
1873         /// to the storage or database. If *itA < *itB, then the resource
1874         /// is present in the storage only and should be added to the
1875         /// database. If *itA > *itB, then the resource is present in
1876         /// the database only and should be removed (because it has been
1877         /// removed from the storage);
1878 
1879         while (itA != endA || itB != endB) {
1880             if ((itA != endA && itB != endB && *itA < *itB) ||
1881                     itB == endB) {
1882 
1883                 // add a version to the database
1884 
1885                 KoResourceSP res = storage->resource(itA->url);
1886                 if (res) {
1887                     res->setVersion(itA->version);
1888                     res->setMD5Sum(storage->resourceMd5(itA->url));
1889 
1890                     const bool result = addResourceVersionImpl(itA->resourceId, itA->timestamp, storage, res);
1891                     KIS_SAFE_ASSERT_RECOVER_NOOP(result);
1892 
1893                     resourceIdForUpdate.insert(itA->resourceId);
1894                 }
1895                 ++itA;
1896 
1897             } else if ((itA != endA && itB != endB && *itA > *itB) ||
1898                        itA == endA) {
1899 
1900                 // remove a version from the database
1901                 const bool result = removeResourceVersionImpl(itB->resourceId, itB->version, storage);
1902                 KIS_SAFE_ASSERT_RECOVER_NOOP(result);
1903                 resourceIdForUpdate.insert(itB->resourceId);
1904                 ++itB;
1905 
1906             } else {
1907                 // resources are equal, just skip them
1908                 ++itA;
1909                 ++itB;
1910             }
1911         }
1912 
1913 
1914         /// In the main loop we modified the versioned_resource table only,
1915         /// now we should update the head resources table with the latest
1916         /// version of the resource (and upload the thumbnail as well)
1917 
1918         for (auto it = resourceIdForUpdate.begin(); it != resourceIdForUpdate.end(); ++it) {
1919             updateResourceTableForResourceIfNeeded(*it, resourceType, storage);
1920         }
1921     }
1922 
1923     QSqlDatabase::database().commit();
1924     debugResource << "Synchronizing the storages took" << t.elapsed() << "milliseconds for" << storage->location();
1925 
1926     return success;
1927 }
1928 
1929 void KisResourceCacheDb::deleteTemporaryResources()
1930 {
1931     QSqlDatabase::database().transaction();
1932 
1933     QSqlQuery q;
1934 
1935     if (!q.prepare("DELETE FROM versioned_resources\n"
1936                    "WHERE  storage_id in (SELECT id\n"
1937                    "                      FROM   storages\n"
1938                    "                      WHERE  storage_type_id == :storage_type)"))
1939     {
1940         qWarning() << "Could not prepare delete versioned resources from Unknown or Memory storages query." << q.lastError();
1941     }
1942 
1943     q.bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
1944 
1945     if (!q.exec()) {
1946         qWarning() << "Could not execute delete versioned resources from Unknown or Memory storages query." << q.lastError();
1947     }
1948 
1949     if (!q.prepare("DELETE FROM resources\n"
1950                    "WHERE  storage_id in (SELECT id\n"
1951                    "                      FROM   storages\n"
1952                    "                      WHERE  storage_type_id  == :storage_type)"))
1953     {
1954         qWarning() << "Could not prepare delete resources from Unknown or Memory storages query." << q.lastError();
1955     }
1956 
1957     q.bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
1958 
1959     if (!q.exec()) {
1960         qWarning() << "Could not execute delete resources from Unknown or Memory storages query." << q.lastError();
1961     }
1962 
1963 
1964     if (!q.prepare("DELETE FROM versioned_resources\n"
1965                    "WHERE resource_id IN (SELECT id FROM resources\n"
1966                    "                      WHERE  temporary = 1)")) {
1967         qWarning() << "Could not prepare delete temporary versioned resources query." << q.lastError();
1968     }
1969 
1970     if (!q.exec()) {
1971         qWarning() << "Could not execute delete temporary versioned resources query." << q.lastError();
1972     }
1973 
1974     if (!q.prepare("DELETE FROM resources\n"
1975                    "WHERE  temporary = 1")) {
1976         qWarning() << "Could not prepare delete temporary resources query." << q.lastError();
1977         return;
1978     }
1979 
1980     if (!q.exec()) {
1981         qWarning() << "Could not execute delete temporary resources query." << q.lastError();
1982     }
1983 
1984     if (!q.prepare("DELETE FROM storages\n"
1985                    "WHERE  storage_type_id  == :storage_type\n"))
1986     {
1987         qWarning() << "Could not prepare delete Unknown or Memory storages query." << q.lastError();
1988     }
1989 
1990     q.bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
1991 
1992     if (!q.exec()) {
1993         qWarning() << "Could not execute delete Unknown or Memory storages query." << q.lastError();
1994     }
1995 
1996     QSqlDatabase::database().commit();
1997 }
1998 
1999 bool KisResourceCacheDb::registerResourceType(const QString &resourceType)
2000 {
2001     // Check whether the type already exists
2002     {
2003         QSqlQuery q;
2004         if (!q.prepare("SELECT count(*)\n"
2005                        "FROM   resource_types\n"
2006                        "WHERE  name = :resource_type\n")) {
2007             qWarning() << "Could not prepare select from resource_types query" << q.lastError();
2008             return false;
2009         }
2010         q.bindValue(":resource_type", resourceType);
2011         if (!q.exec()) {
2012             qWarning() << "Could not execute select from resource_types query" << q.lastError();
2013             return false;
2014         }
2015         q.first();
2016         int rowCount = q.value(0).toInt();
2017         if (rowCount > 0) {
2018             return true;
2019         }
2020     }
2021     // if not, add it
2022     QFile f(":/fill_resource_types.sql");
2023     if (f.open(QFile::ReadOnly)) {
2024         QString sql = f.readAll();
2025         QSqlQuery q(sql);
2026         q.addBindValue(resourceType);
2027         if (!q.exec()) {
2028             qWarning() << "Could not insert" << resourceType << q.lastError();
2029             return false;
2030         }
2031         return true;
2032     }
2033     qWarning() << "Could not open fill_resource_types.sql";
2034     return false;
2035 }
2036 
2037 QMap<QString, QVariant> KisResourceCacheDb::metaDataForId(int id, const QString &tableName)
2038 {
2039     QMap<QString, QVariant> map;
2040 
2041     QSqlQuery q;
2042     q.setForwardOnly(true);
2043     if (!q.prepare("SELECT key\n"
2044                    ",      value\n"
2045                    "FROM   metadata\n"
2046                    "WHERE  foreign_id = :id\n"
2047                    "AND    table_name = :table")) {
2048         qWarning() << "Could not prepare metadata query" << q.lastError();
2049         return map;
2050     }
2051 
2052     q.bindValue(":id", id);
2053     q.bindValue(":table", tableName);
2054 
2055     if (!q.exec()) {
2056         qWarning() << "Could not execute metadata query" << q.lastError();
2057         return map;
2058     }
2059 
2060     while (q.next()) {
2061         QString key = q.value(0).toString();
2062         QByteArray ba = q.value(1).toByteArray();
2063         if (!ba.isEmpty()) {
2064             QDataStream ds(QByteArray::fromBase64(ba));
2065             QVariant value;
2066             ds >> value;
2067             map[key] = value;
2068         }
2069     }
2070 
2071     return map;
2072 }
2073 
2074 bool KisResourceCacheDb::updateMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2075 {
2076     QSqlDatabase::database().transaction();
2077 
2078     {
2079         QSqlQuery q;
2080         if (!q.prepare("DELETE FROM metadata\n"
2081                        "WHERE  foreign_id = :id\n"
2082                        "AND    table_name = :table\n")) {
2083             qWarning() << "Could not prepare delete metadata query" << q.lastError();
2084             return false;
2085         }
2086 
2087         q.bindValue(":id", id);
2088         q.bindValue(":table", tableName);
2089 
2090         if (!q.exec()) {
2091             QSqlDatabase::database().rollback();
2092             qWarning() << "Could not execute delete metadata query" << q.lastError();
2093             return false;
2094 
2095         }
2096     }
2097 
2098     if (addMetaDataForId(map, id, tableName)) {
2099         QSqlDatabase::database().commit();
2100     }
2101     else {
2102         QSqlDatabase::database().rollback();
2103     }
2104     return true;
2105 }
2106 
2107 bool KisResourceCacheDb::addMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2108 {
2109 
2110     QSqlQuery q;
2111     if (!q.prepare("INSERT INTO metadata\n"
2112                    "(foreign_id, table_name, key, value)\n"
2113                    "VALUES\n"
2114                    "(:id, :table, :key, :value)")) {
2115         QSqlDatabase::database().rollback();
2116         qWarning() << "Could not create insert metadata query" << q.lastError();
2117         return false;
2118     }
2119 
2120     QMap<QString, QVariant>::const_iterator iter = map.cbegin();
2121     while (iter != map.cend()) {
2122         q.bindValue(":id", id);
2123         q.bindValue(":table", tableName);
2124         q.bindValue(":key", iter.key());
2125 
2126         QVariant v = iter.value();
2127         if (!v.isNull() && v.isValid()) {
2128             QByteArray ba;
2129             QDataStream ds(&ba, QIODevice::WriteOnly);
2130             ds << v;
2131             ba = ba.toBase64();
2132             q.bindValue(":value", QString::fromLatin1(ba));
2133 
2134             if (!q.exec()) {
2135                 qWarning() << "Could not insert metadata" << q.lastError();
2136                 return false;
2137             }
2138         }
2139         ++iter;
2140     }
2141     return true;
2142 }