Krita Source Code Documentation
Loading...
Searching...
No Matches
KisResourceCacheDb.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
7
8#include <QSqlError>
9#include <QSqlQuery>
10#include <QSqlDatabase>
11
12#include <QBuffer>
13#include <QVersionNumber>
14#include <QStandardPaths>
15#include <QDir>
16#include <QDirIterator>
17#include <QStringList>
18#include <QElapsedTimer>
19#include <QDataStream>
20#include <QByteArray>
21#include <QMessageBox>
22
23#include <KritaVersionWrapper.h>
24
25#include <klocalizedstring.h>
26#include <KisBackup.h>
27
28#include <kis_debug.h>
29#include <KisUsageLogger.h>
30
31#include <KisSqlQueryLoader.h>
33#include "KisResourceLocator.h"
35
36#include "ResourceDebug.h"
37#include <kis_assert.h>
38
39#include <KisCppQuirks.h>
40
41const QString dbDriver = "QSQLITE";
42const QString METADATA_RESOURCES = "resources";
43const QString METADATA_STORAGES = "storages";
44
45const QString KisResourceCacheDb::resourceCacheDbFilename { "resourcecache.sqlite" };
46const QString KisResourceCacheDb::databaseVersion { "0.0.18" };
48QStringList KisResourceCacheDb::disabledBundles { QStringList() << "Krita_3_Default_Resources.bundle" };
49
51QString KisResourceCacheDb::s_lastError {QString()};
52
54{
55 return s_valid;
56}
57
59{
60 return s_lastError;
61}
62
63// use in WHERE QSqlQuery clauses
64// because if the string is null, the query will also have null there
65// and every comparison with null is false, so the query won't find anything
66// (especially important for storage location where empty string is common)
67QString changeToEmptyIfNull(QString s)
68{
69 return s.isNull() ? QString("") : s;
70}
71
73{
74 QFile f(":/fill_version_information.sql");
75 if (f.open(QFile::ReadOnly)) {
76 QString sql = f.readAll();
77 QSqlQuery q;
78 if (!q.prepare(sql)) {
79 warnDbMigration << "Could not prepare the schema information query" << q.lastError() << q.boundValues();
80 return false;
81 }
83 q.addBindValue(KritaVersionWrapper::versionString());
84 q.addBindValue(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
85 if (!q.exec()) {
86 warnDbMigration << "Could not insert the current version" << q.lastError() << q.boundValues();
87 return false;
88 }
89
90 infoDbMigration << "Filled version table";
91 }
92 return true;
93}
94
95QSqlError runUpdateScriptFile(const QString &path, const QString &message)
96{
97 try {
98
99 KisSqlQueryLoader loader(path);
100 loader.exec();
101
102 } catch (const KisSqlQueryLoader::FileException &e) {
103 warnDbMigration.noquote() << "ERROR: Could not execute DB update step:" << message;
104 warnDbMigration.noquote() << " error" << e.message;
105 warnDbMigration.noquote() << " file:" << e.filePath;
106 warnDbMigration.noquote() << " file-error:" << e.fileErrorString;
107 return
108 QSqlError("Error executing SQL",
109 QString("Could not find SQL file %1").arg(e.filePath),
110 QSqlError::StatementError);
111 } catch (const KisSqlQueryLoader::SQLException &e) {
112 warnDbMigration.noquote() << "ERROR: Could not execute DB update step:" << message;
113 warnDbMigration.noquote() << " error" << e.message;
114 warnDbMigration.noquote() << " file:" << e.filePath;
115 warnDbMigration.noquote() << " statement:" << e.statementIndex;
116 warnDbMigration.noquote() << " sql-error:" << e.sqlError.text();
117 return e.sqlError;
118 }
119
120 infoDbMigration << "Completed DB update step:" << message;
121 return QSqlError();
122}
123
124QSqlError runUpdateScript(const QString &script, const QString &message)
125{
126 try {
127
128 KisSqlQueryLoader loader("", script);
129 loader.exec();
130
131 } catch (const KisSqlQueryLoader::SQLException &e) {
132 warnDbMigration.noquote() << "ERROR: Could execute DB update step:" << message;
133 warnDbMigration.noquote() << " error" << e.message;
134 warnDbMigration.noquote() << " sql-error:" << e.sqlError.text();
135 return e.sqlError;
136 }
137
138 infoDbMigration << "Completed DB update step:" << message;
139 return QSqlError();
140}
141
142QSqlError createDatabase(const QString &location)
143{
144 // NOTE: if the id's of Unknown and Memory in the database
145 // will change, and that will break the queries that
146 // remove Unknown and Memory storages on start-up.
155
156 QDir dbLocation(location);
157 if (!dbLocation.exists()) {
158 dbLocation.mkpath(dbLocation.path());
159 }
160
161 std::optional<QSqlDatabase> existingDatabase =
162 QSqlDatabase::database(QSqlDatabase::defaultConnection, false);
163
164 const bool databaseConnectionExists = !QSqlDatabase::connectionNames().isEmpty()
165 && existingDatabase->isValid() && existingDatabase->isOpen();
166
167 if (databaseConnectionExists && existingDatabase->tables().contains("version_information")) {
168 return QSqlError();
169 }
170
171 existingDatabase = std::nullopt;
172
173 QSqlDatabase db;
174
175 if (!databaseConnectionExists) {
176 db = QSqlDatabase::addDatabase(dbDriver);
177 db.setDatabaseName(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
178
179 if (!db.open()) {
180 warnDbMigration << "Could not connect to resource cache database";
181 return db.lastError();
182 }
183 } else {
184 db = QSqlDatabase::database();
185 }
186
187 // will be filled correctly later
188 QVersionNumber oldSchemaVersionNumber;
189 QVersionNumber newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
190
191
192 QStringList tables = QStringList() << "version_information"
193 << "storage_types"
194 << "resource_types"
195 << "storages"
196 << "tags"
197 << "resources"
198 << "versioned_resources"
199 << "resource_tags"
200 << "metadata"
201 << "tags_storages"
202 << "tag_translations";
203
204 QStringList dbTables;
205 // Verify whether we should recreate the database
206 {
207 bool allTablesPresent = true;
208 dbTables = db.tables();
209 Q_FOREACH(const QString &table, tables) {
210 if (!dbTables.contains(table)) {
211 allTablesPresent = false;
212 break;
213 }
214 }
215
216 bool schemaIsOutDated = false;
217 QString schemaVersion = "0.0.0";
218 QString kritaVersion = "Unknown";
219 int creationDate = 0;
220
221 if (dbTables.contains("version_information")) {
222 // Verify the version number
223
224 {
225 QSqlQuery q(
226 "SELECT database_version\n"
227 ", krita_version\n"
228 ", creation_date\n"
229 "FROM version_information\n"
230 "ORDER BY id\n"
231 "DESC\n"
232 "LIMIT 1;\n");
233
234 if (!q.exec()) {
235 warnDbMigration << "Could not retrieve version information from the database." << q.lastError();
236 abort();
237 }
238 q.first();
239 schemaVersion = q.value(0).toString();
240 kritaVersion = q.value(1).toString();
241 creationDate = q.value(2).toInt();
242 }
243
244 oldSchemaVersionNumber = QVersionNumber::fromString(schemaVersion);
245 newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
246
247 if (QVersionNumber::compare(oldSchemaVersionNumber, newSchemaVersionNumber) != 0) {
248
249 infoDbMigration << "Old schema:" << schemaVersion << "New schema:" << newSchemaVersionNumber;
250
251 schemaIsOutDated = true;
253
254 if (newSchemaVersionNumber == QVersionNumber::fromString("0.0.18")
255 && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) >= 0
256 && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.18")) < 0) {
257
258 bool from14to15 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14");
259
260 bool from15to16 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
261 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15");
262
263 bool from16to17 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
264 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15")
265 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.16");
266
267 bool from17to18 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
268 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15")
269 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.16")
270 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.17");
271
272 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
273
274 bool success = true;
275 if (from14to15) {
276 QSqlError error = runUpdateScript(
277 "ALTER TABLE resource_tags\n"
278 "ADD COLUMN active INTEGER NOT NULL DEFAULT 1",
279 "Update resource tags table (add \'active\' column)");
280 if (error.type() != QSqlError::NoError) {
281 success = false;
282 }
283 }
284 if (success && from15to16) {
285 infoDbMigration << "Going to update indices";
286
287 QStringList indexes = QStringList() << "tags" << "resources" << "tag_translations" << "resource_tags";
288
289 Q_FOREACH(const QString &index, indexes) {
290 QSqlError error = runUpdateScriptFile(":/create_index_" + index + ".sql",
291 QString("Create index for %1").arg(index));
292 if (error.type() != QSqlError::NoError) {
293 success = false;
294 }
295 }
296 }
297
298 if (success && from16to17) {
299 QSqlError error = runUpdateScriptFile(":/create_index_resources_signature.sql",
300 "Create index for resources_signature");
301 if (error.type() != QSqlError::NoError) {
302 success = false;
303 }
304 }
305
306 if (success && from17to18) {
307 {
308 QSqlError error = runUpdateScriptFile(":/0_0_18_0001_cleanup_metadata_table.sql",
309 "Cleanup and deduplicate metadata table");
310 if (error.type() != QSqlError::NoError) {
311 success = false;
312 }
313 }
314 if (success) {
315 QSqlError error = runUpdateScriptFile(":/0_0_18_0002_update_metadata_table_constraints.sql",
316 "Update metadata table constraints");
317 if (error.type() != QSqlError::NoError) {
318 success = false;
319 }
320 }
321 if (success) {
322 QSqlError error = runUpdateScriptFile(":/create_index_metadata_key.sql",
323 "Create index for metadata_key");
324 if (error.type() != QSqlError::NoError) {
325 success = false;
326 }
327 }
328 }
329
330 if (success) {
331 if (!updateSchemaVersion()) {
332 success = false;
333 }
334
335 transactionLock.commit();
336
337 if (success) {
338 QSqlError error = runUpdateScript("VACUUM",
339 "Vacuum database after updating schema");
340 if (error.type() != QSqlError::NoError) {
341 success = false;
342 }
343 }
344 } else {
345 transactionLock.rollback();
346 }
347
348 schemaIsOutDated = !success;
349
350 }
351
352 if (schemaIsOutDated) {
353 QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The resource database scheme has changed. Krita will backup your database and create a new database."));
354 if (QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) > 0) {
356 }
357 db.close();
358 QFile::remove(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
359 db.open();
360 }
361 }
362
363 }
364
365 if (allTablesPresent && !schemaIsOutDated) {
366 KisUsageLogger::log(QString("Database is up to date. Version: %1, created by Krita %2, at %3")
367 .arg(schemaVersion)
368 .arg(kritaVersion)
369 .arg(QDateTime::fromSecsSinceEpoch(creationDate).toString()));
370
374
375 return QSqlError();
376 }
377 }
378
379 KisUsageLogger::log(QString("Creating database from scratch (%1, %2).")
380 .arg(oldSchemaVersionNumber.toString().isEmpty() ? QString("database didn't exist") : ("old schema version: " + oldSchemaVersionNumber.toString()))
381 .arg("new schema version: " + newSchemaVersionNumber.toString()));
382
383 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
384
385 // Create tables
386 Q_FOREACH(const QString &table, tables) {
387 QSqlError error =
388 runUpdateScriptFile(":/create_" + table + ".sql", QString("Create table %1").arg(table));
389 if (error.type() != QSqlError::NoError) {
390 return error;
391 }
392 }
393
394 {
395 // metadata table constraints were updated in version 0.0.18
396 QSqlError error = runUpdateScriptFile(":/0_0_18_0002_update_metadata_table_constraints.sql",
397 "Update metadata table constraints");
398
399 if (error.type() != QSqlError::NoError) {
400 return error;
401 }
402 }
403
404 // Create indexes
405 QStringList indexes;
406
407 // these indexes came in version 0.0.16
408 indexes << "storages" << "versioned_resources" << "tags" << "resources" << "tag_translations" << "resource_tags";
409
410 // this index came in version 0.0.17
411 indexes << "resources_signature";
412
413 // this index came in version 0.0.18
414 indexes << "metadata_key";
415
416 Q_FOREACH(const QString &index, indexes) {
417 QSqlError error = runUpdateScriptFile(":/create_index_" + index + ".sql",
418 QString("Create index for %1").arg(index));
419 if (error.type() != QSqlError::NoError) {
420 return error;
421 }
422 }
423
424 // Fill lookup tables
425 {
426 QFile f(":/fill_storage_types.sql");
427 if (f.open(QFile::ReadOnly)) {
428 QString sql = f.readAll();
429 Q_FOREACH(const QString &originType, KisResourceCacheDb::storageTypes) {
430 const QString updateStep = QString("Register storage type: %1").arg(originType);
431 QSqlQuery q(sql);
432 q.addBindValue(originType);
433 if (!q.exec()) {
434 warnDbMigration << "Could execute DB update step:" << updateStep << q.lastError();
435 warnDbMigration << " faulty statement:" << sql;
436 return db.lastError();
437 }
438 infoDbMigration << "Completed DB update step:" << updateStep;
439 }
440 }
441 else {
442 return QSqlError("Error executing SQL", QString("Could not find SQL fill_storage_types.sql."), QSqlError::StatementError);
443 }
444 }
445
446 {
447 QFile f(":/fill_resource_types.sql");
448 if (f.open(QFile::ReadOnly)) {
449 QString sql = f.readAll();
450 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
451 const QString updateStep = QString("Register resource type: %1").arg(resourceType);
452 QSqlQuery q(sql);
453 q.addBindValue(resourceType);
454 if (!q.exec()) {
455 warnDbMigration << "Could execute DB update step:" << updateStep << q.lastError();
456 warnDbMigration << " faulty statement:" << sql;
457 return db.lastError();
458 }
459 infoDbMigration << "Completed DB update step:" << updateStep;
460 }
461 }
462 else {
463 return QSqlError("Error executing SQL", QString("Could not find SQL fill_resource_types.sql."), QSqlError::StatementError);
464 }
465 }
466
467 if (!updateSchemaVersion()) {
468 return QSqlError("Error executing SQL", QString("Could not update schema version."), QSqlError::StatementError);
469 }
470
471 transactionLock.commit();
472
476
477 return QSqlError();
478}
479
480bool KisResourceCacheDb::initialize(const QString &location)
481{
482 QSqlError err = createDatabase(location);
483
484 s_valid = !err.isValid();
485 switch (err.type()) {
486 case QSqlError::NoError:
487 s_lastError = QString();
488 break;
489 case QSqlError::ConnectionError:
490 s_lastError = QString("Could not initialize the resource cache database. Connection error: %1").arg(err.text());
491 break;
492 case QSqlError::StatementError:
493 s_lastError = QString("Could not initialize the resource cache database. Statement error: %1").arg(err.text());
494 break;
495 case QSqlError::TransactionError:
496 s_lastError = QString("Could not initialize the resource cache database. Transaction error: %1").arg(err.text());
497 break;
498 case QSqlError::UnknownError:
499 s_lastError = QString("Could not initialize the resource cache database. Unknown error: %1").arg(err.text());
500 break;
501 }
502
503 // Delete all storages that are no longer known to the resource locator (including the memory storages)
505
506 return s_valid;
507}
508
509QVector<int> KisResourceCacheDb::resourcesForStorage(const QString &resourceType, const QString &storageLocation)
510{
511 QVector<int> result;
512
513 QSqlQuery q;
514
515 if (!q.prepare("SELECT resources.id\n"
516 "FROM resources\n"
517 ", resource_types\n"
518 ", storages\n"
519 "WHERE resources.resource_type_id = resource_types.id\n"
520 "AND storages.id = resources.storage_id\n"
521 "AND storages.location = :storage_location\n"
522 "AND resource_types.name = :resource_type\n")) {
523
524 qWarning() << "Could not read and prepare resourcesForStorage" << q.lastError();
525 return result;
526 }
527
528 q.bindValue(":resource_type", resourceType);
529 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
530
531 if (!q.exec()) {
532 qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
533 return result;
534 }
535
536 while (q.next()) {
537 result << q.value(0).toInt();
538 }
539
540 return result;
541}
542
543int KisResourceCacheDb::resourceIdForResource(const QString &resourceFileName, const QString &resourceType, const QString &storageLocation)
544{
545 //qDebug() << "resourceIdForResource" << resourceName << resourceFileName << resourceType << storageLocation;
546
547 QSqlQuery q;
548
549 if (!q.prepare("SELECT resources.id\n"
550 "FROM resources\n"
551 ", resource_types\n"
552 ", storages\n"
553 "WHERE resources.resource_type_id = resource_types.id\n"
554 "AND storages.id = resources.storage_id\n"
555 "AND storages.location = :storage_location\n"
556 "AND resource_types.name = :resource_type\n"
557 "AND resources.filename = :filename\n")) {
558 qWarning() << "Could not read and prepare resourceIdForResource" << q.lastError();
559 return -1;
560 }
561
562 q.bindValue(":filename", resourceFileName);
563 q.bindValue(":resource_type", resourceType);
564 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
565
566 if (!q.exec()) {
567 qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
568 return -1;
569 }
570
571 if (q.first()) {
572 return q.value(0).toInt();
573 }
574
575 // couldn't be found in the `resources` table, but can still be in versioned_resources
576
577 if (!q.prepare("SELECT versioned_resources.resource_id\n"
578 "FROM resources\n"
579 ", resource_types\n"
580 ", versioned_resources\n"
581 ", storages\n"
582 "WHERE resources.resource_type_id = resource_types.id\n" // join resources and resource_types by resource id
583 "AND versioned_resources.resource_id = resources.id\n" // join versioned_resources and resources by resource id
584 "AND storages.id = versioned_resources.storage_id\n" // join storages and versioned_resources by storage id
585 "AND storages.location = :storage_location\n" // storage location must be the same as asked for
586 "AND resource_types.name = :resource_type\n" // resource type must be the same as asked for
587 "AND versioned_resources.filename = :filename\n")) { // filename must be the same as asked for
588 qWarning() << "Could not read and prepare resourceIdForResource (in versioned resources)" << q.lastError();
589 return -1;
590 }
591
592 q.bindValue(":filename", resourceFileName);
593 q.bindValue(":resource_type", resourceType);
594 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
595
596 if (!q.exec()) {
597 qWarning() << "Could not query resourceIdForResource (in versioned resources)" << q.boundValues() << q.lastError();
598 return -1;
599 }
600
601 if (q.first()) {
602 return q.value(0).toInt();
603 }
604
605 // commenting out, because otherwise it spams the console on every new resource in the local resources folder
606 // qWarning() << "Could not find resource" << resourceName << resourceFileName << resourceType << storageLocation;
607 return -1;
608
609}
610
611bool KisResourceCacheDb::resourceNeedsUpdating(int resourceId, QDateTime timestamp)
612{
613 QSqlQuery q;
614 if (!q.prepare("SELECT timestamp\n"
615 "FROM versioned_resources\n"
616 "WHERE resource_id = :resource_id\n"
617 "AND version = (SELECT MAX(version)\n"
618 " FROM versioned_resources\n"
619 " WHERE resource_id = :resource_id);")) {
620 qWarning() << "Could not prepare resourceNeedsUpdating statement" << q.lastError();
621 return false;
622 }
623
624 q.bindValue(":resource_id", resourceId);
625
626 if (!q.exec()) {
627 qWarning() << "Could not query for the most recent timestamp" << q.boundValues() << q.lastError();
628 return false;
629 }
630
631 if (!q.first()) {
632 qWarning() << "Inconsistent database: could not find a version for resource with Id" << resourceId;
633 return false;
634 }
635
636 QVariant resourceTimeStamp = q.value(0);
637
638 if (!resourceTimeStamp.isValid()) {
639 qWarning() << "Could not retrieve timestamp from versioned_resources" << resourceId;
640 return false;
641 }
642
643 return (timestamp.toSecsSinceEpoch() > resourceTimeStamp.toInt());
644}
645
646bool KisResourceCacheDb::addResourceVersion(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
647{
648 bool r = false;
649
650
651 r = addResourceVersionImpl(resourceId, timestamp, storage, resource);
652
653 if (!r) return r;
654
655 r = makeResourceTheCurrentVersion(resourceId, resource);
656
657 return r;
658}
659
660bool KisResourceCacheDb::addResourceVersionImpl(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
661{
662 bool r = false;
663
664 // Create the new version. The resource is expected to have an updated version number, or
665 // this will fail on the unique index on resource_id, storage_id and version.
666 //
667 // This function **only** adds to the versioned_resources table.
668 // The resources table should be updated by the caller manually using
669 // updateResourceTableForResourceIfNeeded()
670
671 Q_ASSERT(resource->version() >= 0);
672
673 QSqlQuery q;
674 r = q.prepare("INSERT INTO versioned_resources \n"
675 "(resource_id, storage_id, version, filename, timestamp, md5sum)\n"
676 "VALUES\n"
677 "( :resource_id\n"
678 ", (SELECT id \n"
679 " FROM storages \n"
680 " WHERE location = :storage_location)\n"
681 ", :version\n"
682 ", :filename\n"
683 ", :timestamp\n"
684 ", :md5sum\n"
685 ");");
686
687 if (!r) {
688 qWarning() << "Could not prepare addResourceVersion statement" << q.lastError();
689 return r;
690 }
691
692 q.bindValue(":resource_id", resourceId);
693 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
694 q.bindValue(":version", resource->version());
695 q.bindValue(":filename", resource->filename());
696 q.bindValue(":timestamp", timestamp.toSecsSinceEpoch());
697 KIS_SAFE_ASSERT_RECOVER_NOOP(!resource->md5Sum().isEmpty());
698 q.bindValue(":md5sum", resource->md5Sum());
699 r = q.exec();
700 if (!r) {
701
702 qWarning() << "Could not execute addResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << resource->name() << resource->filename() << "version" << resource->version();
703 return r;
704 }
705
706 return r;
707}
708
710{
711 bool r = false;
712
713 // Remove a version of the resource. This function **only** removes data from
714 // the versioned_resources table. The resources table should be updated by
715 // the caller manually using updateResourceTableForResourceIfNeeded()
716
717 QSqlQuery q;
718 r = q.prepare("DELETE FROM versioned_resources \n"
719 "WHERE resource_id = :resource_id\n"
720 "AND version = :version\n"
721 "AND storage_id = (SELECT id \n"
722 " FROM storages \n"
723 " WHERE location = :storage_location);");
724
725 if (!r) {
726 qWarning() << "Could not prepare removeResourceVersionImpl statement" << q.lastError();
727 return r;
728 }
729
730 q.bindValue(":resource_id", resourceId);
731 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
732 q.bindValue(":version", version);
733 r = q.exec();
734 if (!r) {
735
736 qWarning() << "Could not execute removeResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << "version" << version;
737 return r;
738 }
739
740 return r;
741}
742
743bool KisResourceCacheDb::updateResourceTableForResourceIfNeeded(int resourceId, const QString &resourceType, KisResourceStorageSP storage)
744{
745 bool r = false;
746
747 int maxVersion = -1;
748 {
749 QSqlQuery q;
750 r = q.prepare("SELECT MAX(version)\n"
751 "FROM versioned_resources\n"
752 "WHERE resource_id = :resource_id;");
753 if (!r) {
754 qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
755 return r;
756 }
757
758 q.bindValue(":resource_id", resourceId);
759
760 r = q.exec();
761 if (!r) {
762 qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
763 return r;
764 }
765
766 r = q.first();
768
769 maxVersion = q.value(0).toInt();
770 }
771
772 QString maxVersionFilename;
773 {
774 QSqlQuery q;
775 r = q.prepare("SELECT filename\n"
776 "FROM versioned_resources\n"
777 "WHERE resource_id = :resource_id\n"
778 "AND version = :version;");
779 if (!r) {
780 qWarning() << "Could not prepare findMaxVersionFilename statement" << q.lastError();
781 return r;
782 }
783
784 q.bindValue(":resource_id", resourceId);
785 q.bindValue(":version", maxVersion);
786
787 r = q.exec();
788 if (!r) {
789 qWarning() << "Could not execute findMaxVersionFilename query" << q.boundValues() << q.lastError();
790 return r;
791 }
792
793 if (!q.first()) {
794 return removeResourceCompletely(resourceId);
795 } else {
796 maxVersionFilename = q.value(0).toString();
797 }
798 }
799
800 QString currentFilename;
801 {
802 QSqlQuery q;
803 r = q.prepare("SELECT filename\n"
804 "FROM resources\n"
805 "WHERE id = :resource_id;");
806 if (!r) {
807 qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
808 return r;
809 }
810
811 q.bindValue(":resource_id", resourceId);
812
813 r = q.exec();
814 if (!r) {
815 qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
816 return r;
817 }
818
819 r = q.first();
821
822 currentFilename = q.value(0).toString();
823 }
824
825 if (currentFilename != maxVersionFilename) {
826 const QString url = resourceType + "/" + maxVersionFilename;
827 KoResourceSP resource = storage->resource(url);
829 resource->setVersion(maxVersion);
830 resource->setMD5Sum(storage->resourceMd5(url));
831 resource->setStorageLocation(storage->location());
832 r = makeResourceTheCurrentVersion(resourceId, resource);
833 }
834
835 return r;
836}
837
839{
840 bool r = false;
841
842 QSqlQuery q;
843 r = q.prepare("UPDATE resources\n"
844 "SET name = :name\n"
845 ", filename = :filename\n"
846 ", tooltip = :tooltip\n"
847 ", thumbnail = :thumbnail\n"
848 ", status = 1\n"
849 ", md5sum = :md5sum\n"
850 "WHERE id = :id");
851 if (!r) {
852 qWarning() << "Could not prepare updateResource statement" << q.lastError();
853 return r;
854 }
855
856 q.bindValue(":name", resource->name());
857 q.bindValue(":filename", resource->filename());
858 q.bindValue(":tooltip", i18n(resource->name().toUtf8()));
859 q.bindValue(":md5sum", resource->md5Sum());
860
861 QBuffer buf;
862 buf.open(QBuffer::WriteOnly);
863 resource->thumbnail().save(&buf, "PNG");
864 buf.close();
865 q.bindValue(":thumbnail", buf.data());
866 q.bindValue(":id", resourceId);
867
868 r = q.exec();
869 if (!r) {
870 qWarning() << "Could not update resource" << q.boundValues() << q.lastError();
871 }
872
873 if (!resource->metadata().isEmpty()) {
874 return updateMetaDataForId(resource->metadata(), resourceId, METADATA_RESOURCES);
875 }
876
877 return r;
878}
879
881{
882 bool r = false;
883
884 {
885 QSqlQuery q;
886 r = q.prepare("DELETE FROM versioned_resources \n"
887 "WHERE resource_id = :resource_id;");
888
889 if (!r) {
890 qWarning() << "Could not prepare removeResourceCompletely1 statement" << q.lastError();
891 return r;
892 }
893
894 q.bindValue(":resource_id", resourceId);
895 r = q.exec();
896 if (!r) {
897 qWarning() << "Could not execute removeResourceCompletely1 statement" << q.lastError() << resourceId;
898 return r;
899 }
900 }
901
902 {
903 QSqlQuery q;
904 r = q.prepare("DELETE FROM resources \n"
905 "WHERE id = :resource_id;");
906
907 if (!r) {
908 qWarning() << "Could not prepare removeResourceCompletely2 statement" << q.lastError();
909 return r;
910 }
911
912 q.bindValue(":resource_id", resourceId);
913 r = q.exec();
914 if (!r) {
915 qWarning() << "Could not execute removeResourceCompletely2 statement" << q.lastError() << resourceId;
916 return r;
917 }
918 }
919
920 {
921 QSqlQuery q;
922 r = q.prepare("DELETE FROM resource_tags \n"
923 "WHERE resource_id = :resource_id;");
924
925 if (!r) {
926 qWarning() << "Could not prepare removeResourceCompletely3 statement" << q.lastError();
927 return r;
928 }
929
930 q.bindValue(":resource_id", resourceId);
931 r = q.exec();
932 if (!r) {
933 qWarning() << "Could not execute removeResourceCompletely3 statement" << q.lastError() << resourceId;
934 return r;
935 }
936 }
937
938 {
939 QSqlQuery q;
940 r = q.prepare("DELETE FROM metadata \n"
941 "WHERE foreign_id = :resource_id\n"
942 "AND table_name = :table;");
943
944 if (!r) {
945 qWarning() << "Could not prepare removeResourceCompletely4 statement" << q.lastError();
946 return r;
947 }
948
949 q.bindValue(":resource_id", resourceId);
950 q.bindValue(":table", METADATA_RESOURCES);
951 r = q.exec();
952 if (!r) {
953 qWarning() << "Could not execute removeResourceCompletely4 statement" << q.lastError() << resourceId;
954 return r;
955 }
956 }
957
958 return r;
959}
960
961bool KisResourceCacheDb::getResourceIdFromFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
962{
963 QSqlQuery q;
964
965 bool r = q.prepare("SELECT resources.id FROM resources\n"
966 ", resource_types\n"
967 ", storages\n"
968 "WHERE resources.filename = :filename\n" // bind to filename
969 "AND resource_types.id = resources.resource_type_id\n" // join resources_types + resources
970 "AND resource_types.name = :resourceType\n" // bind to resource type
971 "AND resources.storage_id = storages.id\n" // join resources + storages
972 "AND storages.location = :storageLocation"); // bind to storage location
973
974 if (!r) {
975 qWarning() << "Could not prepare getResourceIdFromFilename statement" << q.lastError() << q.executedQuery();
976 return r;
977 }
978
979 q.bindValue(":filename", filename);
980 q.bindValue(":resourceType", resourceType);
981 q.bindValue(":storageLocation", changeToEmptyIfNull(storageLocation));
982
983 r = q.exec();
984 if (!r) {
985 qWarning() << "Could not execute getResourceIdFromFilename statement" << q.lastError() << filename << resourceType;
986 return r;
987 }
988
989 r = q.first();
990 if (r) {
991 outResourceId = q.value("resources.id").toInt();
992 }
993
994 return r;
995}
996
997bool KisResourceCacheDb::getResourceIdFromVersionedFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
998{
999 QSqlQuery q;
1000
1001 bool r = q.prepare("SELECT resource_id FROM versioned_resources\n"
1002 ", resources\n"
1003 ", resource_types\n"
1004 ", storages\n"
1005 "WHERE versioned_resources.filename = :filename\n" // bind to filename
1006 "AND resources.id = versioned_resources.resource_id\n" // join resources + versioned_resources
1007 "AND resource_types.id = resources.resource_type_id\n" // join resources_types + resources
1008 "AND resource_types.name = :resourceType\n" // bind to resource type
1009 "AND resources.storage_id = storages.id\n" // join resources + storages
1010 "AND storages.location = :storageLocation"); // bind to storage location
1011
1012 if (!r) {
1013 qWarning() << "Could not prepare getResourceIdFromVersionedFilename statement" << q.lastError() << q.executedQuery();
1014 return r;
1015 }
1016
1017
1018 q.bindValue(":filename", filename);
1019 q.bindValue(":resourceType", resourceType);
1020 q.bindValue(":storageLocation", changeToEmptyIfNull(storageLocation));
1021
1022 r = q.exec();
1023 if (!r) {
1024 qWarning() << "Could not execute getResourceIdFromVersionedFilename statement" << q.lastError() << filename << resourceType;
1025 return r;
1026 }
1027
1028 r = q.first();
1029 if (r) {
1030 outResourceId = q.value("resource_id").toInt();
1031 }
1032
1033 return r;
1034}
1035
1036bool KisResourceCacheDb::getAllVersionsLocations(int resourceId, QStringList &outVersionsLocationsList)
1037{
1038 QSqlQuery q;
1039 bool r = q.prepare("SELECT filename FROM versioned_resources \n"
1040 "WHERE resource_id = :resource_id;");
1041
1042 if (!r) {
1043 qWarning() << "Could not prepare getAllVersionsLocations statement" << q.lastError();
1044 return r;
1045 }
1046
1047 q.bindValue(":resource_id", resourceId);
1048 r = q.exec();
1049 if (!r) {
1050 qWarning() << "Could not execute getAllVersionsLocations statement" << q.lastError() << resourceId;
1051 return r;
1052 }
1053
1054 outVersionsLocationsList = QStringList();
1055 while (q.next()) {
1056 outVersionsLocationsList << q.value("filename").toString();
1057 }
1058
1059 return r;
1060
1061}
1062
1063bool KisResourceCacheDb::addResource(KisResourceStorageSP storage, QDateTime timestamp, KoResourceSP resource, const QString &resourceType)
1064{
1065 bool r = false;
1066
1067 if (!s_valid) {
1068 qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
1069 return false;
1070 }
1071
1072 if (!resource || !resource->valid()) {
1073 qWarning() << "KisResourceCacheDb::addResource: The resource is not valid:" << resource->filename();
1074 // We don't care about invalid resources and will just ignore them.
1075 return true;
1076 }
1077 bool temporary = (storage->type() == KisResourceStorage::StorageType::Memory);
1078
1079 // Check whether it already exists
1080 int resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1081 if (resourceId > -1) {
1082 return true;
1083 }
1084
1085 QSqlQuery q;
1086 r = q.prepare("INSERT INTO resources \n"
1087 "(storage_id, resource_type_id, name, filename, tooltip, thumbnail, status, temporary, md5sum) \n"
1088 "VALUES \n"
1089 "((SELECT id "
1090 " FROM storages "
1091 " WHERE location = :storage_location)\n"
1092 ", (SELECT id\n"
1093 " FROM resource_types\n"
1094 " WHERE name = :resource_type)\n"
1095 ", :name\n"
1096 ", :filename\n"
1097 ", :tooltip\n"
1098 ", :thumbnail\n"
1099 ", :status\n"
1100 ", :temporary\n"
1101 ", :md5sum)");
1102
1103 if (!r) {
1104 qWarning() << "Could not prepare addResource statement" << q.lastError();
1105 return r;
1106 }
1107
1108 q.bindValue(":resource_type", resourceType);
1109 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1110 q.bindValue(":name", resource->name());
1111 q.bindValue(":filename", resource->filename());
1112
1113 QString translationContext;
1114 if (storage->type() == KisResourceStorage::StorageType::Bundle) {
1115 translationContext = "./krita/data/bundles/" + KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())
1116 + ":" + resourceType + "/" + resource->filename();
1117 } else if (storage->location() == "memory") {
1118 translationContext = "memory/" + resourceType + "/" + resource->filename();
1119 }
1120 else if (resource->filename().endsWith(".myb", Qt::CaseInsensitive)) {
1121 translationContext = "./plugins/paintops/mypaint/brushes/" + resource->filename();
1122 } else {
1123 translationContext = "./krita/data/" + resourceType + "/" + resource->filename();
1124 }
1125
1126 {
1127 QByteArray ctx = translationContext.toUtf8();
1128 QString translatedName = i18nc(ctx, resource->name().toUtf8());
1129 if (translatedName == resource->name()) {
1130 // Try using the file name without the file extension, and replaces '_' with spaces.
1131 QString altName = QFileInfo(resource->filename()).completeBaseName().replace('_', ' ');
1132 QString altTranslatedName = i18nc(ctx, altName.toUtf8());
1133 if (altName != altTranslatedName) {
1134 translatedName = altTranslatedName;
1135 }
1136 }
1137 q.bindValue(":tooltip", translatedName);
1138 }
1139
1140 QBuffer buf;
1141 buf.open(QBuffer::WriteOnly);
1142 resource->image().save(&buf, "PNG");
1143 buf.close();
1144 q.bindValue(":thumbnail", buf.data());
1145
1146 q.bindValue(":status", resource->active());
1147 q.bindValue(":temporary", (temporary ? 1 : 0));
1148 q.bindValue(":md5sum", resource->md5Sum());
1149
1150 r = q.exec();
1151 if (!r) {
1152 qWarning() << "Could not execute addResource statement" << q.lastError() << q.boundValues();
1153 return r;
1154 }
1155 resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1156
1157 if (resourceId < 0) {
1158
1159 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: "
1160 << resource->name()
1161 << resource->filename()
1162 << resourceType
1164 return false;
1165 }
1166
1167 resource->setResourceId(resourceId);
1168
1169 if (!addResourceVersionImpl(resourceId, timestamp, storage, resource)) {
1170 qWarning() << "Could not add resource version" << resource;
1171 return false;
1172 }
1173
1174 if (!resource->metadata().isEmpty()) {
1175 return updateMetaDataForId(resource->metadata(), resource->resourceId(), METADATA_RESOURCES);
1176 }
1177
1178 return true;
1179
1180
1181}
1182
1184{
1185 QSqlDatabase::database().transaction();
1186 QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1187 while (iter->hasNext()) {
1188 iter->next();
1189
1191 iter->versions();
1192
1193 int resourceId = -1;
1194
1195 while (verIt->hasNext()) {
1196 verIt->next();
1197
1198 KoResourceSP resource = verIt->resource();
1199 if (resource && resource->valid()) {
1200 resource->setVersion(verIt->guessedVersion());
1201 resource->setMD5Sum(storage->resourceMd5(verIt->url()));
1202
1203 if (resourceId < 0) {
1204 if (addResource(storage, iter->lastModified(), resource, iter->type())) {
1205 resourceId = resource->resourceId();
1206 } else {
1207 qWarning() << "Could not add resource" << resource->filename() << "to the database";
1208 }
1209 } else {
1210 if (!addResourceVersion(resourceId, iter->lastModified(), storage, resource)) {
1211 qWarning() << "Could not add resource version" << resource->filename() << "to the database";
1212 }
1213 }
1214 }
1215 }
1216 }
1217 QSqlDatabase::database().commit();
1218 return true;
1219}
1220
1221bool KisResourceCacheDb::setResourceActive(int resourceId, bool active)
1222{
1223 if (resourceId < 0) {
1224 qWarning() << "Invalid resource id; cannot remove resource";
1225 return false;
1226 }
1227 QSqlQuery q;
1228 bool r = q.prepare("UPDATE resources\n"
1229 "SET status = :status\n"
1230 "WHERE id = :resource_id");
1231 if (!r) {
1232 qWarning() << "Could not prepare removeResource query" << q.lastError();
1233 }
1234 q.bindValue(":status", active);
1235 q.bindValue(":resource_id", resourceId);
1236 if (!q.exec()) {
1237 qWarning() << "Could not update resource" << resourceId << "to inactive" << q.lastError();
1238 return false;
1239 }
1240
1241 return true;
1242}
1243
1244bool KisResourceCacheDb::tagResource(const QString &resourceFileName, KisTagSP tag, const QString &resourceType)
1245{
1246 // Get tag id
1247 int tagId {-1};
1248 {
1249 QFile f(":/select_tag.sql");
1250 if (f.open(QFile::ReadOnly)) {
1251 QSqlQuery q;
1252 if (!q.prepare(f.readAll())) {
1253 qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1254 return false;
1255 }
1256 q.bindValue(":url", tag->url());
1257 q.bindValue(":resource_type", resourceType);
1258
1259 if (!q.exec()) {
1260 qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1261 return false;
1262 }
1263
1264 if (!q.first()) {
1265 qWarning() << "Could not find tag" << q.boundValues() << q.lastError();
1266 return false;
1267 }
1268
1269 tagId = q.value(0).toInt();
1270 }
1271 }
1272
1273
1274 // Get resource id
1275 QSqlQuery q;
1276 bool r = q.prepare("SELECT resources.id\n"
1277 "FROM resources\n"
1278 ", resource_types\n"
1279 "WHERE resources.resource_type_id = resource_types.id\n"
1280 "AND resource_types.name = :resource_type\n"
1281 "AND resources.filename = :resource_filename\n");
1282 if (!r) {
1283 qWarning() << "Could not prepare tagResource query" << q.lastError();
1284 return false;
1285 }
1286
1287 q.bindValue(":resource_type", resourceType);
1288 q.bindValue(":resource_filename", resourceFileName);
1289
1290 if (!q.exec()) {
1291 qWarning() << "Could not execute tagResource statement" << q.boundValues() << q.lastError();
1292 return false;
1293 }
1294
1295
1296 while (q.next()) {
1297
1298 int resourceId = q.value(0).toInt();
1299
1300 if (resourceId < 0) {
1301 qWarning() << "Could not find resource to tag" << resourceFileName << resourceType;
1302 continue;
1303 }
1304
1305 {
1306 QSqlQuery q;
1307 if (!q.prepare("SELECT COUNT(*)\n"
1308 "FROM resource_tags\n"
1309 "WHERE resource_id = :resource_id\n"
1310 "AND tag_id = :tag_id")) {
1311 qWarning() << "Could not prepare tagResource query 2" << q.lastError();
1312 continue;
1313 }
1314 q.bindValue(":resource_id", resourceId);
1315 q.bindValue(":tag_id", tagId);
1316
1317 if (!q.exec()) {
1318 qWarning() << "Could not execute tagResource query 2" << q.lastError() << q.boundValues();
1319 continue;
1320 }
1321
1322 q.first();
1323 int count = q.value(0).toInt();
1324 if (count > 0) {
1325 continue;
1326 }
1327 }
1328
1329 {
1330 QSqlQuery q;
1331 if (!q.prepare("INSERT INTO resource_tags\n"
1332 "(resource_id, tag_id)\n"
1333 "VALUES\n"
1334 "(:resource_id, :tag_id);")) {
1335 qWarning() << "Could not prepare tagResource insert statement" << q.lastError();
1336 continue;
1337 }
1338
1339 q.bindValue(":resource_id", resourceId);
1340 q.bindValue(":tag_id", tagId);
1341
1342 if (!q.exec()) {
1343 qWarning() << "Could not execute tagResource stagement" << q.boundValues() << q.lastError();
1344 continue;
1345 }
1346 }
1347 }
1348 return true;
1349}
1350
1351bool KisResourceCacheDb::hasTag(const QString &url, const QString &resourceType)
1352{
1353 QFile f(":/select_tag.sql");
1354 if (f.open(QFile::ReadOnly)) {
1355 QSqlQuery q;
1356 if (!q.prepare(f.readAll())) {
1357 qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1358 return false;
1359 }
1360 q.bindValue(":url", url);
1361 q.bindValue(":resource_type", resourceType);
1362 if (!q.exec()) {
1363 qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1364 }
1365 return q.first();
1366 }
1367 qWarning() << "Could not open select_tag.sql";
1368 return false;
1369}
1370
1371bool KisResourceCacheDb::linkTagToStorage(const QString &url, const QString &resourceType, const QString &storageLocation)
1372{
1373 QSqlQuery q;
1374 if (!q.prepare("INSERT INTO tags_storages\n"
1375 "(tag_id, storage_id)\n"
1376 "VALUES\n"
1377 "(\n"
1378 " ( SELECT id\n"
1379 " FROM tags\n"
1380 " WHERE url = :url\n"
1381 " AND resource_type_id = (SELECT id \n"
1382 " FROM resource_types\n"
1383 " WHERE name = :resource_type)"
1384 " )\n"
1385 ",( SELECT id\n"
1386 " FROM storages\n"
1387 " WHERE location = :storage_location\n"
1388 " )\n"
1389 ");")) {
1390 qWarning() << "Could not prepare add tag/storage statement" << q.lastError();
1391 return false;
1392 }
1393
1394 q.bindValue(":url", url);
1395 q.bindValue(":resource_type", resourceType);
1396 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storageLocation)));
1397
1398 if (!q.exec()) {
1399 qWarning() << "Could not insert tag/storage link" << q.boundValues() << q.lastError();
1400 return false;
1401 }
1402 return true;
1403}
1404
1405
1406bool KisResourceCacheDb::addTag(const QString &resourceType, const QString storageLocation, KisTagSP tag)
1407{
1408
1409 if (hasTag(tag->url(), resourceType)) {
1410 // Check whether this storage is already registered for this tag
1411 QSqlQuery q;
1412 if (!q.prepare("SELECT storages.location\n"
1413 "FROM tags_storages\n"
1414 ", tags\n"
1415 ", storages\n"
1416 "WHERE tags.id = tags_storages.tag_id\n"
1417 "AND storages.id = tags_storages.storage_id\n"
1418 "AND tags.resource_type_id = (SELECT id\n"
1419 " FROM resource_types\n"
1420 " WHERE name = :resource_type)\n"
1421 "AND tags.url = :url"))
1422 {
1423 qWarning() << "Could not prepare select tags from tags_storages query" << q.lastError();
1424 }
1425
1426 q.bindValue(":url", tag->url());
1427 q.bindValue(":resource_type", resourceType);
1428
1429 if (!q.exec()) {
1430 qWarning() << "Could not execute tags_storages query" << q.boundValues() << q.lastError();
1431 }
1432
1433 // If this tag is not yet linked to the storage, link it
1434 if (!q.first()) {
1435 return linkTagToStorage(tag->url(), resourceType, storageLocation);
1436 }
1437
1438 return true;
1439 }
1440
1441 int tagId;
1442
1443 // Insert the tag
1444 {
1445 QSqlQuery q;
1446 if (!q.prepare("INSERT INTO tags\n"
1447 "(url, name, comment, resource_type_id, active, filename)\n"
1448 "VALUES\n"
1449 "( :url\n"
1450 ", :name\n"
1451 ", :comment\n"
1452 ", (SELECT id\n"
1453 " FROM resource_types\n"
1454 " WHERE name = :resource_type)\n"
1455 ", 1\n"
1456 ", :filename\n"
1457 ");")) {
1458 qWarning() << "Could not prepare insert tag statement" << q.lastError();
1459 return false;
1460 }
1461
1462 q.bindValue(":url", tag->url());
1463 q.bindValue(":name", tag->name(false));
1464 q.bindValue(":comment", tag->comment(false));
1465 q.bindValue(":resource_type", resourceType);
1466 q.bindValue(":filename", tag->filename());
1467
1468 if (!q.exec()) {
1469 qWarning() << "Could not insert tag" << q.boundValues() << q.lastError();
1470 }
1471
1472 tagId = q.lastInsertId().toInt();
1473 }
1474
1475 {
1476 Q_FOREACH(const QString language, tag->names().keys()) {
1477
1478 QString name = tag->names()[language];
1479 QString comment = name;
1480 if (tag->comments().contains(language)) {
1481 comment = tag->comments()[language];
1482 }
1483
1484 QSqlQuery q;
1485 if (!q.prepare("INSERT INTO tag_translations\n"
1486 "( tag_id\n"
1487 ", language\n"
1488 ", name\n"
1489 ", comment\n"
1490 ")\n"
1491 "VALUES\n"
1492 "( :id\n"
1493 ", :language\n"
1494 ", :name\n"
1495 ", :comment\n"
1496 ");")) {
1497 qWarning() << "Could not prepare insert tag_translation query" << q.lastError();
1498 }
1499
1500 q.bindValue(":id", tagId);
1501 q.bindValue(":language", language);
1502 q.bindValue(":name", name);
1503 q.bindValue(":comment", comment);
1504
1505 if (!q.exec()) {
1506 qWarning() << "Could not execute insert tag_translation query" << q.lastError() << q.boundValues();
1507 }
1508 }
1509
1510 }
1511
1512
1513 linkTagToStorage(tag->url(), resourceType, storageLocation);
1514
1515 return true;
1516}
1517
1518bool KisResourceCacheDb::addTags(KisResourceStorageSP storage, QString resourceType)
1519{
1520 QSqlDatabase::database().transaction();
1521 QSharedPointer<KisResourceStorage::TagIterator> iter = storage->tags(resourceType);
1522 while(iter->hasNext()) {
1523 iter->next();
1524 KisTagSP tag = iter->tag();
1525 if (tag && tag->valid()) {
1526 if (!addTag(resourceType, storage->location(), tag)) {
1527 qWarning() << "Could not add tag" << tag << "to the database";
1528 continue;
1529 }
1530 if (!tag->defaultResources().isEmpty()) {
1531 Q_FOREACH(const QString &resourceFileName, tag->defaultResources()) {
1532 if (!tagResource(resourceFileName, tag, resourceType)) {
1533 qWarning() << "Could not tag resource" << QFileInfo(resourceFileName).baseName() << "from" << storage->name() << "filename" << resourceFileName << "with tag" << iter->tag();
1534 }
1535 }
1536 }
1537 }
1538 }
1539 QSqlDatabase::database().commit();
1540 return true;
1541}
1542
1544{
1545 // Check whether the type already exists
1546 const QString name = KisResourceStorage::storageTypeToUntranslatedString(storageType);
1547
1548 {
1549 QSqlQuery q;
1550 if (!q.prepare("SELECT count(*)\n"
1551 "FROM storage_types\n"
1552 "WHERE name = :storage_type\n")) {
1553 qWarning() << "Could not prepare select from storage_types query" << q.lastError();
1554 return false;
1555 }
1556 q.bindValue(":storage_type", name);
1557 if (!q.exec()) {
1558 qWarning() << "Could not execute select from storage_types query" << q.lastError();
1559 return false;
1560 }
1561 q.first();
1562 int rowCount = q.value(0).toInt();
1563 if (rowCount > 0) {
1564 return true;
1565 }
1566 }
1567 // if not, add it
1568 QFile f(":/fill_storage_types.sql");
1569 if (f.open(QFile::ReadOnly)) {
1570 QString sql = f.readAll();
1571 QSqlQuery q(sql);
1572 q.addBindValue(name);
1573 if (!q.exec()) {
1574 qWarning() << "Could not insert" << name << q.lastError();
1575 return false;
1576 }
1577 return true;
1578 }
1579 qWarning() << "Could not open fill_storage_types.sql";
1580 return false;
1581}
1582
1584{
1585 bool r = true;
1586
1587 if (!s_valid) {
1588 qWarning() << "The database is not valid";
1589 return false;
1590 }
1591
1592 {
1593 QSqlQuery q;
1594 r = q.prepare("SELECT * FROM storages WHERE location = :location");
1595 q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1596 r = q.exec();
1597 if (!r) {
1598 qWarning() << "Could not select from storages";
1599 return r;
1600 }
1601 if (q.first()) {
1602 debugResource << "Storage already exists" << storage;
1603 return true;
1604 }
1605 }
1606
1607 // Insert the storage;
1608 {
1609 QSqlQuery q;
1610
1611 r = q.prepare("INSERT INTO storages\n "
1612 "(storage_type_id, location, timestamp, pre_installed, active, thumbnail)\n"
1613 "VALUES\n"
1614 "(:storage_type_id, :location, :timestamp, :pre_installed, :active, :thumbnail);");
1615
1616 if (!r) {
1617 qWarning() << "Could not prepare query" << q.lastError();
1618 return r;
1619 }
1620
1621 const QString sanitizedStorageLocation =
1622 changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1623
1624 q.bindValue(":storage_type_id", static_cast<int>(storage->type()));
1625 q.bindValue(":location", sanitizedStorageLocation);
1626 q.bindValue(":timestamp", storage->timestamp().toSecsSinceEpoch());
1627 q.bindValue(":pre_installed", preinstalled ? 1 : 0);
1628 q.bindValue(":active", !disabledBundles.contains(storage->name()));
1629
1630 QBuffer buf;
1631 buf.open(QBuffer::WriteOnly);
1632 storage->thumbnail().save(&buf, "PNG");
1633 buf.close();
1634 q.bindValue(":thumbnail", buf.data());
1635
1636 r = q.exec();
1637
1638 if (!r) qWarning() << "Could not execute query" << q.lastError();
1639
1640 if (!q.prepare("SELECT id\n"
1641 "FROM storages\n"
1642 "WHERE location = :location\n")) {
1643 qWarning() << "Could not prepare storage id statement" << q.lastError();
1644 }
1645
1646 q.bindValue(":location", sanitizedStorageLocation);
1647 if (!q.exec()) {
1648 qWarning() << "Could not execute storage id statement" << q.boundValues() << q.lastError();
1649 }
1650
1651 if (!q.first()) {
1652 qWarning() << "Could not find id for the newly added storage" << q.lastError();
1653 } else {
1654 storage->setStorageId(q.value("id").toInt());
1655 }
1656 }
1657
1658 // Insert the metadata
1659 {
1660 QStringList keys = storage->metaDataKeys();
1661 if (keys.size() > 0 && storage->storageId() >= 0) {
1662
1663 QMap<QString, QVariant> metadata;
1664
1665 Q_FOREACH(const QString &key, storage->metaDataKeys()) {
1666 metadata[key] = storage->metaData(key);
1667 }
1668
1669 updateMetaDataForId(metadata, storage->storageId(), METADATA_STORAGES);
1670 }
1671 }
1672
1673 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1674 if (!KisResourceCacheDb::addResources(storage, resourceType)) {
1675 qWarning() << "Failed to add all resources for storage" << storage;
1676 r = false;
1677 }
1678 }
1679
1680 return r;
1681}
1682
1684{
1685
1686 bool r = true;
1687 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1688 if (!KisResourceCacheDb::addTags(storage, resourceType)) {
1689 qWarning() << "Failed to add all tags for storage" << storage;
1690 r = false;
1691 }
1692 }
1693 return r;
1694}
1695
1697{
1698 // location is already relative
1699
1700 try {
1701 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
1702
1703 {
1704 KisSqlQueryLoader loader(":/sql/delete_versioned_resources_for_storage_indirect.sql",
1706 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1707 loader.exec();
1708 }
1709
1710 {
1711 KisSqlQueryLoader loader(":/sql/delete_versioned_resources_for_storage_direct.sql",
1713 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1714 loader.exec();
1715 if (loader.query().numRowsAffected() > 0) {
1716 qWarning() << "WARNING: deleteStorage: versioned_resurces table contained resource versions not being "
1717 "present in the main table. Deleted: "
1718 << loader.query().numRowsAffected();
1719 }
1720 }
1721
1722 {
1723 KisSqlQueryLoader loader(":/sql/delete_resource_metadata_for_storage.sql",
1725 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1726 loader.query().bindValue(":table", METADATA_RESOURCES);
1727 loader.exec();
1728 }
1729
1730 {
1731 KisSqlQueryLoader loader("inline://delete_current_resources_for_storage",
1732 "DELETE FROM resources\n"
1733 "WHERE storage_id = (SELECT storages.id\n"
1734 " FROM storages\n"
1735 " WHERE storages.location = :location)\n",
1737 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1738 loader.exec();
1739 }
1740
1741 {
1742 KisSqlQueryLoader loader("inline://delete_tags_for_storage",
1743 "DELETE FROM tags \n"
1744 "WHERE id IN (SELECT tags_storages.tag_id \n "
1745 " FROM tags_storages \n"
1746 " WHERE tags_storages.storage_id = \n"
1747 " (SELECT storages.id\n"
1748 " FROM storages\n"
1749 " WHERE storages.location = :location)\n"
1750 " )",
1752 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1753 loader.exec();
1754 }
1755
1756 {
1757 KisSqlQueryLoader loader("inline://delete_tags_storage_links_for_storage",
1758 "DELETE FROM tags_storages \n"
1759 "WHERE tags_storages.storage_id = \n"
1760 " (SELECT storages.id\n"
1761 " FROM storages\n"
1762 " WHERE storages.location = :location)",
1764 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1765 loader.exec();
1766 }
1767
1768 {
1769 KisSqlQueryLoader loader("inline://delete_starage_metadata_for_storage",
1770 "DELETE FROM metadata\n"
1771 "WHERE foreign_id = (SELECT storages.id\n"
1772 " FROM storages\n"
1773 " WHERE storages.location = :location)"
1774 "AND table_name = :table;",
1776 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1777 loader.query().bindValue(":table", METADATA_STORAGES);
1778 loader.exec();
1779 }
1780
1781 {
1782 KisSqlQueryLoader loader("inline://delete_storage",
1783 "DELETE FROM storages\n"
1784 "WHERE location = :location;",
1786 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1787 loader.exec();
1788 }
1789
1790 transactionLock.commit();
1791
1792 } catch (const KisSqlQueryLoader::FileException &e) {
1793 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
1794 qWarning().noquote() << " file:" << e.filePath;
1795 qWarning().noquote() << " error:" << e.fileErrorString;
1796 return false;
1797 } catch (const KisSqlQueryLoader::SQLException &e) {
1798 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
1799 qWarning().noquote() << " file:" << e.filePath;
1800 qWarning().noquote() << " statement:" << e.statementIndex;
1801 qWarning().noquote() << " error:" << e.sqlError.text();
1802 return false;
1803 }
1804
1805 return true;
1806}
1807
1809{
1810 return deleteStorage(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1811}
1812
1813namespace {
1814struct ResourceVersion : public boost::less_than_comparable<ResourceVersion>
1815{
1816 int resourceId = -1;
1817 int version = -1;
1818 QDateTime timestamp;
1819 QString url;
1820
1821 bool operator<(const ResourceVersion &rhs) const {
1822 return resourceId < rhs.resourceId ||
1823 (resourceId == rhs.resourceId && version < rhs.version);
1824 }
1825
1826 struct CompareByResourceId {
1827 bool operator() (const ResourceVersion &lhs, const ResourceVersion &rhs) const {
1828 return lhs.resourceId < rhs.resourceId;
1829 }
1830 };
1831
1832
1833};
1834
1835[[maybe_unused]]
1836QDebug operator<<(QDebug dbg, const ResourceVersion &ver)
1837{
1838 dbg.nospace() << "ResourceVersion("
1839 << ver.resourceId << ", "
1840 << ver.version << ", "
1841 << ver.timestamp << ", "
1842 << ver.url << ")";
1843
1844 return dbg.space();
1845}
1846}
1847
1849{
1850 QElapsedTimer t;
1851 t.start();
1852
1853 if (!s_valid) {
1854 qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
1855 return false;
1856 }
1857
1858 bool success = true;
1859
1860 // Find the storage in the database
1861 QSqlQuery q;
1862 if (!q.prepare("SELECT id\n"
1863 ", timestamp\n"
1864 ", pre_installed\n"
1865 "FROM storages\n"
1866 "WHERE location = :location\n")) {
1867 qWarning() << "Could not prepare storage timestamp statement" << q.lastError();
1868 }
1869
1870 q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1871 if (!q.exec()) {
1872 qWarning() << "Could not execute storage timestamp statement" << q.boundValues() << q.lastError();
1873 }
1874
1875 if (!q.first()) {
1876 // This is a new storage, the user must have dropped it in the path before restarting Krita, so add it.
1877 debugResource << "Adding storage to the database:" << storage;
1878 if (!addStorage(storage, false)) {
1879 qWarning() << "Could not add new storage" << storage->name() << "to the database";
1880 success = false;
1881 }
1882 return success;
1883 }
1884
1885 storage->setStorageId(q.value("id").toInt());
1886
1888 QSqlDatabase::database().transaction();
1889
1892
1893 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1894
1897
1898 QVector<ResourceVersion> resourcesInStorage;
1899
1903
1904 int nextInexistentResourceId = std::numeric_limits<int>::min();
1905
1906 QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1907 while (iter->hasNext()) {
1908 iter->next();
1909
1910 const int firstResourceVersionPosition = resourcesInStorage.size();
1911
1912 int detectedResourceId = nextInexistentResourceId;
1914 iter->versions();
1915
1916 while (verIt->hasNext()) {
1917 verIt->next();
1918
1919 // verIt->url() contains paths like "brushes/ink.png" or "brushes/subfolder/splash.png".
1920 // we need to cut off the first part and get "ink.png" in the first case,
1921 // but "subfolder/splash.png" in the second case in order for subfolders to work
1922 // so it cannot just use QFileInfo(verIt->url()).fileName() here.
1923 QString path = QDir::fromNativeSeparators(verIt->url()); // make sure it uses Unix separators
1924 int folderEndIdx = path.indexOf("/");
1925 QString properFilenameWithSubfolders = path.right(path.length() - folderEndIdx - 1);
1926 int id = resourceIdForResource(properFilenameWithSubfolders,
1927 verIt->type(),
1929
1930 ResourceVersion item;
1931 item.url = verIt->url();
1932 item.version = verIt->guessedVersion();
1933
1934 // we use lower precision than the normal QDateTime
1935 item.timestamp = QDateTime::fromSecsSinceEpoch(verIt->lastModified().toSecsSinceEpoch());
1936
1937 item.resourceId = id;
1938
1939 if (detectedResourceId < 0 && id >= 0) {
1940 detectedResourceId = id;
1941 }
1942
1943 resourcesInStorage.append(item);
1944 }
1945
1949
1950 for (int i = firstResourceVersionPosition; i < resourcesInStorage.size(); i++) {
1951 if (resourcesInStorage[i].resourceId < 0) {
1952 resourcesInStorage[i].resourceId = detectedResourceId;
1953 }
1954 }
1955
1956 nextInexistentResourceId++;
1957 }
1958
1959
1961
1962 QVector<ResourceVersion> resourcesInDatabase;
1963
1964 QSqlQuery q;
1965 q.setForwardOnly(true);
1966 if (!q.prepare("SELECT versioned_resources.resource_id, versioned_resources.filename, versioned_resources.version, versioned_resources.timestamp\n"
1967 "FROM versioned_resources\n"
1968 ", resource_types\n"
1969 ", resources\n"
1970 "WHERE resources.resource_type_id = resource_types.id\n"
1971 "AND resources.id = versioned_resources.resource_id\n"
1972 "AND resource_types.name = :resource_type\n"
1973 "AND versioned_resources.storage_id == :storage_id")) {
1974 qWarning() << "Could not prepare resource by type query" << q.lastError();
1975 success = false;
1976 continue;
1977 }
1978
1979 q.bindValue(":resource_type", resourceType);
1980 q.bindValue(":storage_id", int(storage->storageId()));
1981
1982 if (!q.exec()) {
1983 qWarning() << "Could not exec resource by type query" << q.boundValues() << q.lastError();
1984 success = false;
1985 continue;
1986 }
1987
1988 while (q.next()) {
1989 ResourceVersion item;
1990 item.url = resourceType + "/" + q.value(1).toString();
1991 item.version = q.value(2).toInt();
1992 item.timestamp = QDateTime::fromSecsSinceEpoch(q.value(3).toInt());
1993 item.resourceId = q.value(0).toInt();
1994
1995 resourcesInDatabase.append(item);
1996 }
1997
1998 QSet<int> resourceIdForUpdate;
1999
2000 std::sort(resourcesInStorage.begin(), resourcesInStorage.end());
2001 std::sort(resourcesInDatabase.begin(), resourcesInDatabase.end());
2002
2003 auto itA = resourcesInStorage.begin();
2004 auto endA = resourcesInStorage.end();
2005
2006 auto itB = resourcesInDatabase.begin();
2007 auto endB = resourcesInDatabase.end();
2008
2012
2013 while (itA != endA) {
2014 if (itA->resourceId >= 0) break;
2015
2016 KoResourceSP res = storage->resource(itA->url);
2017
2018 if (!res) {
2019 KisUsageLogger::log("Could not load resource " + itA->url);
2020 ++itA;
2021 continue;
2022 }
2023
2024 res->setVersion(itA->version);
2025 res->setMD5Sum(storage->resourceMd5(itA->url));
2026 if (!res->valid()) {
2027 KisUsageLogger::log("Could not retrieve md5 for resource " + itA->url);
2028 ++itA;
2029 continue;
2030 }
2031
2032 const bool retval = addResource(storage, itA->timestamp, res, resourceType);
2033 if (!retval) {
2034 KisUsageLogger::log("Could not add resource " + itA->url);
2035 ++itA;
2036 continue;
2037 }
2038
2039 const int resourceId = res->resourceId();
2040 KIS_SAFE_ASSERT_RECOVER(resourceId >= 0) {
2041 KisUsageLogger::log("Could not get id for resource " + itA->url);
2042 ++itA;
2043 continue;
2044 }
2045
2046 auto nextResource = std::upper_bound(itA, endA, *itA, ResourceVersion::CompareByResourceId());
2047 for (auto it = std::next(itA); it != nextResource; ++it) {
2048 KoResourceSP res = storage->resource(it->url);
2049 res->setVersion(it->version);
2050 res->setMD5Sum(storage->resourceMd5(it->url));
2051 if (!res->valid()) {
2052 continue;
2053 }
2054
2055 const bool retval = addResourceVersion(resourceId, it->timestamp, storage, res);
2056 KIS_SAFE_ASSERT_RECOVER(retval) {
2057 KisUsageLogger::log("Could not add version for resource " + itA->url);
2058 continue;
2059 }
2060 }
2061
2062 itA = nextResource;
2063 }
2064
2072
2073 while (itA != endA || itB != endB) {
2074 if ((itA != endA && itB != endB && *itA < *itB) ||
2075 itB == endB) {
2076
2077 // add a version to the database
2078
2079 KoResourceSP res = storage->resource(itA->url);
2080 if (res) {
2081 res->setVersion(itA->version);
2082 res->setMD5Sum(storage->resourceMd5(itA->url));
2083
2084 const bool result = addResourceVersionImpl(itA->resourceId, itA->timestamp, storage, res);
2086
2087 resourceIdForUpdate.insert(itA->resourceId);
2088 }
2089 ++itA;
2090
2091 } else if ((itA != endA && itB != endB && *itA > *itB) ||
2092 itA == endA) {
2093
2094 // remove a version from the database
2095 const bool result = removeResourceVersionImpl(itB->resourceId, itB->version, storage);
2097 resourceIdForUpdate.insert(itB->resourceId);
2098 ++itB;
2099
2100 } else {
2101 // resources are equal, just skip them
2102 ++itA;
2103 ++itB;
2104 }
2105 }
2106
2107
2111
2112 for (auto it = resourceIdForUpdate.begin(); it != resourceIdForUpdate.end(); ++it) {
2113 updateResourceTableForResourceIfNeeded(*it, resourceType, storage);
2114 }
2115 }
2116
2117 QSqlDatabase::database().commit();
2118 debugResource << "Synchronizing the storages took" << t.elapsed() << "milliseconds for" << storage->location();
2119
2120 return success;
2121}
2122
2124{
2125 try {
2126 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
2127
2131 {
2132 KisSqlQueryLoader loader(
2133 "inline://delete_metadata_for_resources_in_memory_storages",
2134 "DELETE FROM metadata\n"
2135 "WHERE foreign_id IN (SELECT id\n"
2136 " FROM resources\n"
2137 " WHERE storage_id in (SELECT id\n"
2138 " FROM storages\n"
2139 " WHERE storage_type_id == :storage_type))\n"
2140 "AND table_name = :table",
2142 loader.query().bindValue(":table", METADATA_RESOURCES);
2143 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2144 loader.exec();
2145 }
2146
2147 {
2148 KisSqlQueryLoader loader("inline://delete_metadata_for_temporary_resources",
2149 "DELETE FROM metadata\n"
2150 "WHERE foreign_id IN (SELECT id\n"
2151 " FROM resources\n"
2152 " WHERE temporary = 1)\n"
2153 "AND table_name = :table",
2155 loader.query().bindValue(":table", METADATA_RESOURCES);
2156 loader.exec();
2157 }
2158
2159 {
2160 KisSqlQueryLoader loader("inline://delete_versions_of_resources_in_temporary_storages",
2161 "DELETE FROM versioned_resources\n"
2162 "WHERE storage_id in (SELECT id\n"
2163 " FROM storages\n"
2164 " WHERE storage_type_id == :storage_type)",
2166 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2167 loader.exec();
2168 }
2169
2170 {
2171 KisSqlQueryLoader loader("inline://delete_versions_of_temporary_resources",
2172 "DELETE FROM versioned_resources\n"
2173 "WHERE resource_id IN (SELECT id FROM resources\n"
2174 " WHERE temporary = 1)",
2176 loader.exec();
2177 }
2178
2179 {
2180 KisSqlQueryLoader loader("inline://delete_current_resources_in_temporary_storages",
2181 "DELETE FROM resources\n"
2182 "WHERE storage_id in (SELECT id\n"
2183 " FROM storages\n"
2184 " WHERE storage_type_id == :storage_type)",
2186 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2187 loader.exec();
2188 }
2189
2190 {
2191 KisSqlQueryLoader loader("inline://delete_current_temporary_resources",
2192 "DELETE FROM resources\n"
2193 "WHERE temporary = 1",
2195 loader.exec();
2196 }
2197
2202 {
2203 KisSqlQueryLoader loader("inline://delete_metadata_for_temporary_storages",
2204 "DELETE FROM metadata\n"
2205 "WHERE foreign_id IN (SELECT id\n"
2206 " FROM storages\n"
2207 " WHERE storage_type_id == :storage_type)\n"
2208 "AND table_name = :table;",
2210 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2211 loader.query().bindValue(":table", METADATA_STORAGES);
2212 loader.exec();
2213 }
2214
2215 {
2216 KisSqlQueryLoader loader("inline://delete_temporary_storages",
2217 "DELETE FROM storages\n"
2218 "WHERE storage_type_id == :storage_type\n",
2220 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2221 loader.exec();
2222 }
2223
2224 transactionLock.commit();
2225 } catch (const KisSqlQueryLoader::SQLException &e) {
2226 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2227 qWarning().noquote() << " file:" << e.filePath;
2228 qWarning().noquote() << " statement:" << e.statementIndex;
2229 qWarning().noquote() << " error:" << e.sqlError.text();
2230 }
2231}
2232
2234{
2235 QSqlQuery q;
2236
2237 if (!q.prepare("PRAGMA optimize;")) {
2238 qWarning() << "Could not prepare query" << q.lastQuery() << q.lastError();
2239 }
2240
2241 if (!q.exec()) {
2242 qWarning() << "Could not execute query" << q.lastQuery() << q.lastError();
2243 }
2244}
2245
2247{
2248 KisSqlQueryLoader loader("inline://set_foreign_keys_state",
2249 QString("PRAGMA foreign_keys = %1").arg(isEnabled ? "ON" : "OFF"));
2250 loader.exec();
2251}
2252
2254{
2255 KisSqlQueryLoader loader("inline://get_foreign_keys_state",
2256 "PRAGMA foreign_keys");
2257
2258 loader.exec();
2259
2260 if (loader.query().first()) {
2261 return loader.query().value(0).toInt();
2262 }
2263
2264 return false;
2265}
2266
2268{
2269#ifdef KRITA_STABLE
2270 bool useForeignKeys = false;
2271 KisUsageLogger::log("INFO: detected stable build of Krita, foreign_keys constraint will be disabled");
2272#else
2273 bool useForeignKeys = true;
2274 KisUsageLogger::log("INFO: detected unstable build of Krita, foreign_keys constraint will be enabled");
2275#endif
2276
2277 if (qEnvironmentVariableIsSet("KRITA_OVERRIDE_USE_FOREIGN_KEYS")) {
2278 useForeignKeys = qEnvironmentVariableIntValue("KRITA_OVERRIDE_USE_FOREIGN_KEYS") > 0;
2279 KisUsageLogger::log("INFO: foreign_keys constraint was overridden by KRITA_OVERRIDE_USE_FOREIGN_KEYS: " + QString::number(useForeignKeys));
2280 }
2281
2282 try {
2283 const bool oldForeignKeysState = KisResourceCacheDb::getForeignKeysStateImpl();
2284
2285 if (oldForeignKeysState != useForeignKeys) {
2287 "INFO: switch foreign_keys state: " +
2288 QString::number(oldForeignKeysState) +
2289 " -> " +
2290 QString::number(useForeignKeys));
2291
2293 }
2294
2295 } catch (const KisSqlQueryLoader::SQLException &e) {
2296 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2297 qWarning().noquote() << " file:" << e.filePath;
2298 qWarning().noquote() << " statement:" << e.statementIndex;
2299 qWarning().noquote() << " error:" << e.sqlError.text();
2300 }
2301
2302}
2303
2304bool KisResourceCacheDb::registerResourceType(const QString &resourceType)
2305{
2306 // Check whether the type already exists
2307 {
2308 QSqlQuery q;
2309 if (!q.prepare("SELECT count(*)\n"
2310 "FROM resource_types\n"
2311 "WHERE name = :resource_type\n")) {
2312 qWarning() << "Could not prepare select from resource_types query" << q.lastError();
2313 return false;
2314 }
2315 q.bindValue(":resource_type", resourceType);
2316 if (!q.exec()) {
2317 qWarning() << "Could not execute select from resource_types query" << q.lastError();
2318 return false;
2319 }
2320 q.first();
2321 int rowCount = q.value(0).toInt();
2322 if (rowCount > 0) {
2323 return true;
2324 }
2325 }
2326 // if not, add it
2327 QFile f(":/fill_resource_types.sql");
2328 if (f.open(QFile::ReadOnly)) {
2329 QString sql = f.readAll();
2330 QSqlQuery q(sql);
2331 q.addBindValue(resourceType);
2332 if (!q.exec()) {
2333 qWarning() << "Could not insert" << resourceType << q.lastError();
2334 return false;
2335 }
2336 return true;
2337 }
2338 qWarning() << "Could not open fill_resource_types.sql";
2339 return false;
2340}
2341
2342QMap<QString, QVariant> KisResourceCacheDb::metaDataForId(int id, const QString &tableName)
2343{
2344 QMap<QString, QVariant> map;
2345
2346 QSqlQuery q;
2347 q.setForwardOnly(true);
2348 if (!q.prepare("SELECT key\n"
2349 ", value\n"
2350 "FROM metadata\n"
2351 "WHERE foreign_id = :id\n"
2352 "AND table_name = :table")) {
2353 qWarning() << "Could not prepare metadata query" << q.lastError();
2354 return map;
2355 }
2356
2357 q.bindValue(":id", id);
2358 q.bindValue(":table", tableName);
2359
2360 if (!q.exec()) {
2361 qWarning() << "Could not execute metadata query" << q.lastError();
2362 return map;
2363 }
2364
2365 while (q.next()) {
2366 QString key = q.value(0).toString();
2367 QByteArray ba = q.value(1).toByteArray();
2368 if (!ba.isEmpty()) {
2369 QDataStream ds(QByteArray::fromBase64(ba));
2370 QVariant value;
2371 ds.setVersion(QDataStream::Qt_5_15); // so Qt6 can read metatypes written by Qt5
2372 ds >> value;
2373 map[key] = value;
2374 }
2375 }
2376
2377 return map;
2378}
2379
2380bool KisResourceCacheDb::updateMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2381{
2382 QSqlDatabase::database().transaction();
2383
2384 {
2385 QSqlQuery q;
2386 if (!q.prepare("DELETE FROM metadata\n"
2387 "WHERE foreign_id = :id\n"
2388 "AND table_name = :table\n")) {
2389 QSqlDatabase::database().rollback();
2390 qWarning() << "Could not prepare delete metadata query" << q.lastError();
2391 return false;
2392 }
2393
2394 q.bindValue(":id", id);
2395 q.bindValue(":table", tableName);
2396
2397 if (!q.exec()) {
2398 QSqlDatabase::database().rollback();
2399 qWarning() << "Could not execute delete metadata query" << q.lastError();
2400 return false;
2401
2402 }
2403 }
2404
2405 if (addMetaDataForId(map, id, tableName)) {
2406 QSqlDatabase::database().commit();
2407 }
2408 else {
2409 QSqlDatabase::database().rollback();
2410 }
2411 return true;
2412}
2413
2414bool KisResourceCacheDb::addMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2415{
2416
2417 QSqlQuery q;
2418 if (!q.prepare("INSERT INTO metadata\n"
2419 "(foreign_id, table_name, key, value)\n"
2420 "VALUES\n"
2421 "(:id, :table, :key, :value)")) {
2422 QSqlDatabase::database().rollback();
2423 qWarning() << "Could not create insert metadata query" << q.lastError();
2424 return false;
2425 }
2426
2427 QMap<QString, QVariant>::const_iterator iter = map.cbegin();
2428 while (iter != map.cend()) {
2429 q.bindValue(":id", id);
2430 q.bindValue(":table", tableName);
2431 q.bindValue(":key", iter.key());
2432
2433 QVariant v = iter.value();
2434 if (!v.isNull() && v.isValid()) {
2435 QByteArray ba;
2436 QDataStream ds(&ba, QIODevice::WriteOnly);
2437 ds.setVersion(QDataStream::Qt_5_15); // so Qt6 can write metatypes readable by Qt5
2438 ds << v;
2439 ba = ba.toBase64();
2440 q.bindValue(":value", QString::fromLatin1(ba));
2441
2442 if (!q.exec()) {
2443 qWarning() << "Could not insert metadata" << q.lastError();
2444 return false;
2445 }
2446 }
2447 ++iter;
2448 }
2449 return true;
2450}
2451
2453{
2454 auto deleteMetadataForType = [] (const QString &tableName) {
2455 KisSqlQueryLoader loader("inline://delete_orphaned_records (" + tableName + ")",
2456 QString("DELETE FROM metadata\n"
2457 "WHERE foreign_id NOT IN (SELECT id FROM %1)\n"
2458 "AND table_name = \"%1\"\n")
2459 .arg(tableName));
2460 loader.exec();
2461
2462 if (loader.query().numRowsAffected() > 0) {
2463 qWarning().noquote().nospace() << "WARNING: orphaned metadata records were found for " << tableName << "!";
2464 qWarning().noquote().nospace() << " Num records removed: " << loader.query().numRowsAffected();
2465 }
2466 };
2467
2468 try {
2469 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
2470
2471 deleteMetadataForType(METADATA_RESOURCES);
2472 deleteMetadataForType(METADATA_STORAGES);
2473
2474 transactionLock.commit();
2475
2476 } catch (const KisSqlQueryLoader::SQLException &e) {
2477 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2478 qWarning().noquote() << " file:" << e.filePath;
2479 qWarning().noquote() << " statement:" << e.statementIndex;
2480 qWarning().noquote() << " error:" << e.sqlError.text();
2481
2482 return false;
2483 }
2484
2485 return true;
2486}
float value(const T *src, size_t ch)
qreal v
QDebug KRITACOMMAND_EXPORT operator<<(QDebug dbg, const KisCumulativeUndoData &data)
QList< QString > QStringList
QString changeToEmptyIfNull(QString s)
const QString dbDriver
bool updateSchemaVersion()
QSqlError runUpdateScriptFile(const QString &path, const QString &message)
QSqlError runUpdateScript(const QString &script, const QString &message)
const QString METADATA_RESOURCES
QSqlError createDatabase(const QString &location)
const QString METADATA_STORAGES
bool operator<(KoSnapStrategy::SnapType lhs, KoSnapStrategy::SnapType rhs)
#define debugResource
static bool numberedBackupFile(const QString &filename, const QString &backupDir=QString(), const QString &backupExtension=QStringLiteral("~"), const uint maxBackups=10)
Definition KisBackup.cpp:38
static QStringList disabledBundles
the list of compatibility bundles that need to inactive by default
static bool addStorage(KisResourceStorageSP storage, bool preinstalled)
static bool addResource(KisResourceStorageSP storage, QDateTime timestamp, KoResourceSP resource, const QString &resourceType)
static void setForeignKeysStateImpl(bool isEnabled)
static bool isValid()
isValid
static bool resourceNeedsUpdating(int resourceId, QDateTime timestamp)
static bool getAllVersionsLocations(int resourceId, QStringList &outVersionsLocationsList)
static bool addResourceVersionImpl(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
static bool getForeignKeysStateImpl()
static bool addResourceVersion(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
addResourceVersion adds a new version of the resource to the database. The resource itself already sh...
static void performHouseKeepingOnExit()
perform optimize and vacuum when necessary
static bool removeOrphanedMetaData()
removeOrphanedMetaData Previous versions of Krita never removed metadata, so this function doublechec...
static int resourceIdForResource(const QString &resourceFileName, const QString &resourceType, const QString &storageLocation)
static bool updateMetaDataForId(const QMap< QString, QVariant > map, int id, const QString &tableName)
setMetaDataForId removes all metadata for the given id and table name, and inserts the metadata in th...
static void deleteTemporaryResources()
Delete all storages that are Unknown or Memory and all resources that are marked temporary or belong ...
static QMap< QString, QVariant > metaDataForId(int id, const QString &tableName)
metaDataForId
static bool initialize(const QString &location)
initializes the database and updates the scheme if necessary. Does not actually fill the database wit...
static bool removeResourceVersionImpl(int resourceId, int version, KisResourceStorageSP storage)
static bool addTag(const QString &resourceType, const QString storageLocation, KisTagSP tag)
static QString lastError()
lastError returns the last SQL error.
static QVector< int > resourcesForStorage(const QString &resourceType, const QString &storageLocation)
static bool getResourceIdFromFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
The function will find the resource only if it is the latest version.
static const QString resourceCacheDbFilename
filename of the database
static bool linkTagToStorage(const QString &url, const QString &resourceType, const QString &storageLocation)
static bool deleteStorage(KisResourceStorageSP storage)
Actually delete the storage and all its resources from the database (i.e., nothing is set to inactive...
static bool setResourceActive(int resourceId, bool active=false)
Make this resource active or inactive; this does not remove the resource from disk or from the databa...
static bool addMetaDataForId(const QMap< QString, QVariant > map, int id, const QString &tableName)
static QStringList storageTypes
kinds of places where resources can be stored
static bool addResources(KisResourceStorageSP storage, QString resourceType)
static bool removeResourceCompletely(int resourceId)
static bool addStorageTags(KisResourceStorageSP storage)
static QString s_lastError
static bool getResourceIdFromVersionedFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
Note that here you can put even the original filename - any filename from the versioned_resources - a...
static const QString databaseVersion
current schema version
static void synchronizeForeignKeysState()
static bool registerResourceType(const QString &resourceType)
registerResourceType registers this resource type in the database
static bool makeResourceTheCurrentVersion(int resourceId, KoResourceSP resource)
static bool synchronizeStorage(KisResourceStorageSP storage)
static bool registerStorageType(const KisResourceStorage::StorageType storageType)
registerStorageType registers this storage type in the database
static bool updateResourceTableForResourceIfNeeded(int resourceId, const QString &resourceType, KisResourceStorageSP storage)
static bool hasTag(const QString &url, const QString &resourceType)
static bool addTags(KisResourceStorageSP storage, QString resourceType)
static bool tagResource(const QString &resourceFileName, KisTagSP tag, const QString &resourceType)
static KisResourceLoaderRegistry * instance()
QString makeStorageLocationRelative(QString location) const
static void saveTags()
saveTags saves all tags to .tag files in the resource folder
static KisResourceLocator * instance()
static QString storageTypeToUntranslatedString(StorageType storageType)
static constexpr single_statement_mode_t single_statement_mode
static void log(const QString &message)
Logs with date/time.
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define warnDbMigration
Definition kis_debug.h:86
#define infoDbMigration
Definition kis_debug.h:66
KRITAVERSION_EXPORT QString versionString(bool checkGit=false)