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

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "KisResourceLocator.h"
0008 
0009 #include <QApplication>
0010 #include <QDebug>
0011 #include <QList>
0012 #include <QDir>
0013 #include <QDirIterator>
0014 #include <QFileInfo>
0015 #include <QMessageBox>
0016 #include <QVersionNumber>
0017 #include <QElapsedTimer>
0018 #include <QSqlQuery>
0019 #include <QSqlError>
0020 #include <QBuffer>
0021 
0022 #include <kconfig.h>
0023 #include <kconfiggroup.h>
0024 #include <ksharedconfig.h>
0025 #include <klocalizedstring.h>
0026 
0027 #include <KritaVersionWrapper.h>
0028 #include <KisMimeDatabase.h>
0029 #include <kis_assert.h>
0030 #include <kis_debug.h>
0031 #include <KisUsageLogger.h>
0032 
0033 #include "KoResourcePaths.h"
0034 #include "KisResourceStorage.h"
0035 #include "KisResourceCacheDb.h"
0036 #include "KisResourceLoaderRegistry.h"
0037 #include "KisMemoryStorage.h"
0038 #include "KisResourceModelProvider.h"
0039 #include <KisGlobalResourcesInterface.h>
0040 #include <KisStorageModel.h>
0041 #include <KoMD5Generator.h>
0042 #include <KoResourceLoadResult.h>
0043 #include <KisResourceThumbnailCache.h>
0044 
0045 #include "ResourceDebug.h"
0046 
0047 const QString KisResourceLocator::resourceLocationKey {"ResourceDirectory"};
0048 
0049 class KisResourceLocator::Private {
0050 public:
0051     QString resourceLocation;
0052     QMap<QString, KisResourceStorageSP> storages;
0053     QHash<QPair<QString, QString>, KoResourceSP> resourceCache;
0054     QMap<QPair<QString, QString>, KisTagSP> tagCache;
0055     QStringList errorMessages;
0056 };
0057 
0058 KisResourceLocator::KisResourceLocator(QObject *parent)
0059     : QObject(parent)
0060     , d(new Private())
0061 {
0062 }
0063 
0064 KisResourceLocator *KisResourceLocator::instance()
0065 {
0066     // Not a regular Q_GLOBAL_STATIC, because we want this deleted as
0067     // part of the app destructor.
0068     KisResourceLocator *locator = qApp->findChild<KisResourceLocator *>(QString());
0069     if (!locator) {
0070         locator = new KisResourceLocator(qApp);
0071     }
0072     return locator;
0073 }
0074 
0075 KisResourceLocator::~KisResourceLocator()
0076 {
0077 }
0078 
0079 KisResourceLocator::LocatorError KisResourceLocator::initialize(const QString &installationResourcesLocation)
0080 {
0081     InitializationStatus initializationStatus = InitializationStatus::Unknown;
0082 
0083     d->resourceLocation = KoResourcePaths::getAppDataLocation();
0084 
0085     if (!d->resourceLocation.endsWith('/')) d->resourceLocation += '/';
0086 
0087     QFileInfo fi(d->resourceLocation);
0088 
0089     if (!fi.exists()) {
0090         if (!QDir().mkpath(d->resourceLocation)) {
0091             d->errorMessages << i18n("1. Could not create the resource location at %1.", d->resourceLocation);
0092             return LocatorError::CannotCreateLocation;
0093         }
0094         initializationStatus = InitializationStatus::FirstRun;
0095     }
0096 
0097     if (!fi.isWritable()) {
0098         d->errorMessages << i18n("2. The resource location at %1 is not writable.", d->resourceLocation);
0099         return LocatorError::LocationReadOnly;
0100     }
0101 
0102     // Check whether we're updating from an older version
0103     if (initializationStatus != InitializationStatus::FirstRun) {
0104         QFile fi(d->resourceLocation + '/' + "KRITA_RESOURCE_VERSION");
0105         if (!fi.exists()) {
0106             initializationStatus = InitializationStatus::FirstUpdate;
0107         }
0108         else {
0109             fi.open(QFile::ReadOnly);
0110             QVersionNumber resource_version = QVersionNumber::fromString(QString::fromUtf8(fi.readAll()));
0111             QVersionNumber krita_version = QVersionNumber::fromString(KritaVersionWrapper::versionString());
0112             if (krita_version > resource_version) {
0113                 initializationStatus = InitializationStatus::Updating;
0114             }
0115             else {
0116                 initializationStatus = InitializationStatus::Initialized;
0117             }
0118         }
0119     }
0120 
0121     if (initializationStatus != InitializationStatus::Initialized) {
0122         KisResourceLocator::LocatorError res = firstTimeInstallation(initializationStatus, installationResourcesLocation);
0123         if (res != LocatorError::Ok) {
0124             return res;
0125         }
0126         initializationStatus = InitializationStatus::Initialized;
0127     }
0128 
0129     if (!synchronizeDb()) {
0130         return LocatorError::CannotSynchronizeDb;
0131     }
0132 
0133     return LocatorError::Ok;
0134 }
0135 
0136 QStringList KisResourceLocator::errorMessages() const
0137 {
0138     return d->errorMessages;
0139 }
0140 
0141 QString KisResourceLocator::resourceLocationBase() const
0142 {
0143     return d->resourceLocation;
0144 }
0145 
0146 bool KisResourceLocator::resourceCached(QString storageLocation, const QString &resourceType, const QString &filename) const
0147 {
0148     storageLocation = makeStorageLocationAbsolute(storageLocation);
0149     QPair<QString, QString> key = QPair<QString, QString> (storageLocation, resourceType + "/" + filename);
0150 
0151     return d->resourceCache.contains(key);
0152 }
0153 
0154 void KisResourceLocator::loadRequiredResources(KoResourceSP resource)
0155 {
0156     QList<KoResourceLoadResult> requiredResources = resource->requiredResources(KisGlobalResourcesInterface::instance());
0157 
0158     Q_FOREACH (KoResourceLoadResult res, requiredResources) {
0159         switch (res.type())
0160         {
0161         case KoResourceLoadResult::ExistingResource:
0162             KIS_SAFE_ASSERT_RECOVER_NOOP(res.resource()->resourceId() >= 0);
0163             break;
0164         case KoResourceLoadResult::EmbeddedResource: {
0165             KoResourceSignature sig = res.embeddedResource().signature();
0166             QByteArray data = res.embeddedResource().data();
0167             QBuffer buffer(&data);
0168             buffer.open(QBuffer::ReadOnly);
0169 
0170             importResource(sig.type, sig.filename, &buffer, false, "memory");
0171             break;
0172         }
0173         case KoResourceLoadResult::FailedLink:
0174             qWarning() << "Failed to load linked resource:" << res.signature();
0175             break;
0176         }
0177     }
0178 }
0179 
0180 KisTagSP KisResourceLocator::tagForUrl(const QString &tagUrl, const QString resourceType)
0181 {
0182     if (d->tagCache.contains(QPair<QString, QString>(resourceType, tagUrl))) {
0183         return d->tagCache[QPair<QString, QString>(resourceType, tagUrl)];
0184     }
0185 
0186     KisTagSP tag = tagForUrlNoCache(tagUrl, resourceType);
0187 
0188     if (tag && tag->valid()) {
0189         d->tagCache[QPair<QString, QString>(resourceType, tagUrl)] = tag;
0190     }
0191 
0192     return tag;
0193 }
0194 
0195 KisTagSP KisResourceLocator::tagForUrlNoCache(const QString &tagUrl, const QString resourceType)
0196 {
0197     QSqlQuery query;
0198     bool r = query.prepare("SELECT tags.id\n"
0199                            ",      tags.url\n"
0200                            ",      tags.active\n"
0201                            ",      tags.name\n"
0202                            ",      tags.comment\n"
0203                            ",      tags.filename\n"
0204                            ",      resource_types.name as resource_type\n"
0205                            ",      resource_types.id\n"
0206                            "FROM   tags\n"
0207                            ",      resource_types\n"
0208                            "WHERE  tags.resource_type_id = resource_types.id\n"
0209                            "AND    resource_types.name = :resource_type\n"
0210                            "AND    tags.url = :tag_url\n");
0211 
0212     if (!r) {
0213         qWarning() << "Could not prepare KisResourceLocator::tagForUrl query" << query.lastError();
0214         return KisTagSP();
0215     }
0216 
0217     query.bindValue(":resource_type", resourceType);
0218     query.bindValue(":tag_url", tagUrl);
0219 
0220     r = query.exec();
0221     if (!r) {
0222         qWarning() << "Could not execute KisResourceLocator::tagForUrl query" << query.lastError() << query.boundValues();
0223         return KisTagSP();
0224     }
0225 
0226     r = query.first();
0227     if (!r) {
0228         return KisTagSP();
0229     }
0230 
0231     KisTagSP tag(new KisTag());
0232 
0233     int tagId = query.value("tags.id").toInt();
0234     int resourceTypeId = query.value("resource_types.id").toInt();
0235 
0236     tag->setUrl(query.value("url").toString());
0237     tag->setResourceType(resourceType);
0238     tag->setId(query.value("id").toInt());
0239     tag->setActive(query.value("active").toBool());
0240     tag->setName(query.value("name").toString());
0241     tag->setComment(query.value("comment").toString());
0242     tag->setFilename(query.value("filename").toString());
0243     tag->setValid(true);
0244 
0245 
0246     QMap<QString, QString> names;
0247     QMap<QString, QString> comments;
0248 
0249     r = query.prepare("SELECT language\n"
0250                       ",      name\n"
0251                       ",      comment\n"
0252                       "FROM   tag_translations\n"
0253                       "WHERE  tag_id = :id");
0254 
0255     if (!r) {
0256         qWarning() << "Could not prepare KisResourceLocator::tagForUrl translation query" << query.lastError();
0257     }
0258 
0259     query.bindValue(":id", tag->id());
0260 
0261     if (!query.exec()) {
0262         qWarning() << "Could not execute KisResourceLocator::tagForUrl translation query" << query.lastError();
0263     }
0264 
0265     while (query.next()) {
0266         names[query.value(0).toString()] = query.value(1).toString();
0267         comments[query.value(0).toString()] = query.value(2).toString();
0268     }
0269 
0270     tag->setNames(names);
0271     tag->setComments(comments);
0272 
0273     QSqlQuery defaultResourcesQuery;
0274 
0275     if (!defaultResourcesQuery.prepare("SELECT resources.filename\n"
0276                                        "FROM   resources\n"
0277                                        ",      resource_tags\n"
0278                                        "WHERE  resource_tags.tag_id = :tag_id\n"
0279                                        "AND    resources.resource_type_id = :type_id\n"
0280                                        "AND    resource_tags.resource_id = resources.id\n"
0281                                        "AND    resource_tags.active = 1\n")) {
0282         qWarning() << "Could not prepare resource/tag query" << defaultResourcesQuery.lastError();
0283     }
0284 
0285     defaultResourcesQuery.bindValue(":tag_id", tagId);
0286     defaultResourcesQuery.bindValue(":type_id", resourceTypeId);
0287 
0288     if (!defaultResourcesQuery.exec()) {
0289         qWarning() << "Could not execute resource/tag query" << defaultResourcesQuery.lastError();
0290     }
0291 
0292     QStringList resourceFileNames;
0293 
0294     while (defaultResourcesQuery.next()) {
0295         resourceFileNames << defaultResourcesQuery.value("resources.filename").toString();
0296     }
0297 
0298     tag->setDefaultResources(resourceFileNames);
0299 
0300     return tag;
0301 }
0302 
0303 
0304 KoResourceSP KisResourceLocator::resource(QString storageLocation, const QString &resourceType, const QString &filename)
0305 {
0306     storageLocation = makeStorageLocationAbsolute(storageLocation);
0307 
0308     QPair<QString, QString> key = QPair<QString, QString> (storageLocation, resourceType + "/" + filename);
0309 
0310     KoResourceSP resource;
0311     if (d->resourceCache.contains(key)) {
0312         resource = d->resourceCache[key];
0313     }
0314     else {
0315         KisResourceStorageSP storage = d->storages[storageLocation];
0316         if (!storage) {
0317             qWarning() << "Could not find storage" << storageLocation;
0318             return 0;
0319         }
0320 
0321         resource = storage->resource(resourceType + "/" + filename);
0322 
0323         if (resource) {
0324             d->resourceCache[key] = resource;
0325             // load all the embedded resources into temporary "memory" storage
0326             loadRequiredResources(resource);
0327             resource->updateLinkedResourcesMetaData(KisGlobalResourcesInterface::instance());
0328         }
0329     }
0330 
0331     if (!resource) {
0332         qWarning() << "KoResourceSP KisResourceLocator::resource" << storageLocation << resourceType << filename << "was not found";
0333         return 0;
0334     }
0335 
0336     resource->setStorageLocation(storageLocation);
0337     Q_ASSERT(!resource->storageLocation().isEmpty());
0338 
0339     if (resource->resourceId() < 0 || resource->version() < 0) {
0340         QSqlQuery q;
0341         if (!q.prepare("SELECT resources.id\n"
0342                        ",      versioned_resources.version as version\n"
0343                        ",      versioned_resources.md5sum as md5sum\n"
0344                        ",      resources.name\n"
0345                        ",      resources.status\n"
0346                        "FROM   resources\n"
0347                        ",      storages\n"
0348                        ",      resource_types\n"
0349                        ",      versioned_resources\n"
0350                        "WHERE  storages.id = resources.storage_id\n"
0351                        "AND    storages.location = :storage_location\n"
0352                        "AND    resource_types.id = resources.resource_type_id\n"
0353                        "AND    resource_types.name = :resource_type\n"
0354                        "AND    resources.filename  = :filename\n"
0355                        "AND    versioned_resources.resource_id = resources.id\n"
0356                        "AND    versioned_resources.version = (SELECT MAX(version) FROM versioned_resources WHERE versioned_resources.resource_id = resources.id)")) {
0357             qWarning() << "Could not prepare id/version query" << q.lastError();
0358 
0359         }
0360 
0361         q.bindValue(":storage_location", makeStorageLocationRelative(storageLocation));
0362         q.bindValue(":resource_type", resourceType);
0363         q.bindValue(":filename", filename);
0364 
0365         if (!q.exec()) {
0366             qWarning() << "Could not execute id/version query" << q.lastError() << q.boundValues();
0367         }
0368 
0369         if (!q.first()) {
0370             qWarning() << "Could not find the resource in the database" << storageLocation << resourceType << filename;
0371         }
0372 
0373         resource->setResourceId(q.value(0).toInt());
0374         Q_ASSERT(resource->resourceId() >= 0);
0375 
0376         resource->setVersion(q.value(1).toInt());
0377         Q_ASSERT(resource->version() >= 0);
0378 
0379         resource->setMD5Sum(q.value(2).toString());
0380         Q_ASSERT(!resource->md5Sum().isEmpty());
0381 
0382         resource->setActive(q.value(4).toBool());
0383 
0384         // To override resources that use the filename for the name, which is versioned, and we don't want the version number in the name
0385         resource->setName(q.value(3).toString());;
0386     }
0387 
0388     if (!resource) {
0389         qWarning() << "Could not find resource" << resourceType + "/" + filename;
0390         return 0;
0391     }
0392 
0393     return resource;
0394 }
0395 
0396 KoResourceSP KisResourceLocator::resourceForId(int resourceId)
0397 {
0398     ResourceStorage rs = getResourceStorage(resourceId);
0399     KoResourceSP r = resource(rs.storageLocation, rs.resourceType, rs.resourceFileName);
0400     return r;
0401 }
0402 
0403 bool KisResourceLocator::setResourceActive(int resourceId, bool active)
0404 {
0405     // First remove the resource from the cache
0406     ResourceStorage rs = getResourceStorage(resourceId);
0407     QPair<QString, QString> key = QPair<QString, QString> (rs.storageLocation, rs.resourceType + "/" + rs.resourceFileName);
0408 
0409     d->resourceCache.remove(key);
0410     if (!active) {
0411         KisResourceThumbnailCache::instance()->remove(key);
0412     }
0413 
0414     bool result = KisResourceCacheDb::setResourceActive(resourceId, active);
0415 
0416     Q_EMIT resourceActiveStateChanged(rs.resourceType, resourceId);
0417 
0418     return result;
0419 }
0420 
0421 KoResourceSP KisResourceLocator::importResourceFromFile(const QString &resourceType, const QString &fileName, const bool allowOverwrite, const QString &storageLocation)
0422 {
0423     QFile f(fileName);
0424     if (!f.open(QFile::ReadOnly)) {
0425         qWarning() << "Could not open" << fileName << "for loading";
0426         return nullptr;
0427     }
0428 
0429     return importResource(resourceType, fileName, &f, allowOverwrite, storageLocation);
0430 }
0431 
0432 KoResourceSP KisResourceLocator::importResource(const QString &resourceType, const QString &fileName, QIODevice *device, const bool allowOverwrite, const QString &storageLocation)
0433 {
0434     KisResourceStorageSP storage = d->storages[makeStorageLocationAbsolute(storageLocation)];
0435 
0436     QByteArray resourceData = device->readAll();
0437     KoResourceSP resource;
0438 
0439     {
0440         QBuffer buf(&resourceData);
0441         buf.open(QBuffer::ReadOnly);
0442 
0443         KisResourceLoaderBase *loader = KisResourceLoaderRegistry::instance()->loader(resourceType, KisMimeDatabase::mimeTypeForFile(fileName));
0444 
0445         if (!loader) {
0446             qWarning() << "Could not import" << fileName << ": resource doesn't load.";
0447             return nullptr;
0448         }
0449 
0450         resource = loader->load(QFileInfo(fileName).fileName(), buf, KisGlobalResourcesInterface::instance());
0451     }
0452 
0453     if (!resource || !resource->valid()) {
0454         qWarning() << "Could not import" << fileName << ": resource doesn't load.";
0455         return nullptr;
0456     }
0457 
0458     const QString md5 = KoMD5Generator::generateHash(resourceData);
0459     const QString resourceUrl = resourceType + "/" + resource->filename();
0460 
0461     const KoResourceSP existingResource = storage->resource(resourceUrl);
0462 
0463     if (existingResource) {
0464         const QString existingResourceMd5Sum = storage->resourceMd5(resourceUrl);
0465 
0466         if (!allowOverwrite) {
0467             return nullptr;
0468         }
0469 
0470         if (existingResourceMd5Sum == md5 &&
0471             existingResource->filename() == resource->filename()) {
0472 
0473             /**
0474              * Make sure that this resource is the latest version of the
0475              * resource. Also, we cannot just return existingResource, because
0476              * it has uninitialized fields. It should go through the initialization
0477              * by the locator's caching system.
0478              */
0479 
0480             int existingResourceId = -1;
0481             bool r = KisResourceCacheDb::getResourceIdFromFilename(existingResource->filename(), resourceType, storageLocation, existingResourceId);
0482 
0483             if (r && existingResourceId > 0) {
0484                 return resourceForId(existingResourceId);
0485             }
0486         }
0487 
0488         qWarning() << "A resource with the same filename but a different MD5 already exists in the storage" << resourceType << fileName << storageLocation;
0489         if (storageLocation == "") {
0490             qWarning() << "Proceeding with overwriting the existing resource...";
0491             // remove all versions of the resource from the resource folder
0492             QStringList versionsLocations;
0493 
0494             // this resource has id -1, we need correct id
0495             int existingResourceId = -1;
0496             bool r = KisResourceCacheDb::getResourceIdFromVersionedFilename(existingResource->filename(), resourceType, storageLocation, existingResourceId);
0497 
0498             if (r && existingResourceId >= 0) {
0499                 if (KisResourceCacheDb::getAllVersionsLocations(existingResourceId, versionsLocations)) {
0500 
0501                     for (int i = 0; i < versionsLocations.size(); i++) {
0502                         QFileInfo fi(this->resourceLocationBase() + "/" + resourceType + "/" + versionsLocations[i]);
0503                         if (fi.exists()) {
0504                             r = QFile::remove(fi.filePath());
0505                             if (!r) {
0506                                 qWarning() << "KisResourceLocator::importResourceFromFile: Removal of " << fi.filePath()
0507                                            << "was requested, but it wasn't possible, something went wrong.";
0508                             }
0509                         } else {
0510                             qWarning() << "KisResourceLocator::importResourceFromFile: Removal of " << fi.filePath()
0511                                        << "was requested, but it doesn't exist.";
0512                         }
0513                     }
0514                 } else {
0515                     qWarning() << "KisResourceLocator::importResourceFromFile: Finding all locations for " << existingResourceId << "was requested, but it failed.";
0516                     return nullptr;
0517                 }
0518             } else {
0519                 qWarning() << "KisResourceLocator::importResourceFromFile: there is no resource file found in the location of " << storageLocation << resource->filename() << resourceType;
0520                 return nullptr;
0521             }
0522 
0523             Q_EMIT beginExternalResourceRemove(resourceType, {existingResourceId});
0524 
0525             // remove everything related to this resource from the database (remember about tags and versions!!!)
0526             r = KisResourceCacheDb::removeResourceCompletely(existingResourceId);
0527 
0528             Q_EMIT endExternalResourceRemove(resourceType);
0529 
0530             if (!r) {
0531                 qWarning() << "KisResourceLocator::importResourceFromFile: Removing resource with id " << existingResourceId << "completely from the database failed.";
0532                 return nullptr;
0533             }
0534 
0535         } else {
0536             qWarning() << "KisResourceLocator::importResourceFromFile: Overwriting of the resource was denied, aborting import.";
0537             return nullptr;
0538         }
0539     }
0540 
0541     QBuffer buf(&resourceData);
0542     buf.open(QBuffer::ReadOnly);
0543 
0544     if (storage->importResource(resourceUrl, &buf)) {
0545         resource = storage->resource(resourceUrl);
0546 
0547         if (!resource) {
0548             qWarning() << "Could not retrieve imported resource from the storage" << resourceType << fileName << storageLocation;
0549             return nullptr;
0550         }
0551 
0552         resource->setStorageLocation(storageLocation);
0553         resource->setMD5Sum(storage->resourceMd5(resourceUrl));
0554         resource->setVersion(0);
0555         resource->setDirty(false);
0556         resource->updateLinkedResourcesMetaData(KisGlobalResourcesInterface::instance());
0557 
0558         Q_EMIT beginExternalResourceImport(resourceType, 1);
0559 
0560         // Insert into the database
0561         const bool result = KisResourceCacheDb::addResource(storage,
0562                                                 storage->timeStampForResource(resourceType, resource->filename()),
0563                                                 resource,
0564                                                 resourceType);
0565 
0566         Q_EMIT endExternalResourceImport(resourceType);
0567 
0568         if (!result) {
0569             return nullptr;
0570         }
0571 
0572         // resourceCaches use absolute locations
0573         const QString absoluteStorageLocation = makeStorageLocationAbsolute(resource->storageLocation());
0574         const QPair<QString, QString> key = {absoluteStorageLocation, resourceType + "/" + resource->filename()};
0575         // Add to the cache
0576         d->resourceCache[key] = resource;
0577         KisResourceThumbnailCache::instance()->insert(key, resource->thumbnail());
0578 
0579         return resource;
0580     }
0581 
0582     return nullptr;
0583 }
0584 
0585 bool KisResourceLocator::importWillOverwriteResource(const QString &resourceType, const QString &fileName, const QString &storageLocation) const
0586 {
0587     KisResourceStorageSP storage = d->storages[makeStorageLocationAbsolute(storageLocation)];
0588 
0589     const QString resourceUrl = resourceType + "/" + QFileInfo(fileName).fileName();
0590 
0591     const KoResourceSP existingResource = storage->resource(resourceUrl);
0592 
0593     return existingResource;
0594 }
0595 
0596 bool KisResourceLocator::exportResource(KoResourceSP resource, QIODevice *device)
0597 {
0598     if (!resource || !resource->valid() || resource->resourceId() < 0) return false;
0599 
0600     const QString resourceUrl = resource->resourceType().first + "/" + resource->filename();
0601     KisResourceStorageSP storage = d->storages[makeStorageLocationAbsolute(resource->storageLocation())];
0602     return storage->exportResource(resourceUrl, device);
0603 }
0604 
0605 bool KisResourceLocator::addResource(const QString &resourceType, const KoResourceSP resource, const QString &storageLocation)
0606 {
0607     if (!resource || !resource->valid()) return false;
0608 
0609     KisResourceStorageSP storage = d->storages[makeStorageLocationAbsolute(storageLocation)];
0610     Q_ASSERT(storage);
0611 
0612     //If we have gotten this far and the resource still doesn't have a filename to save to, we should generate one.
0613     if (resource->filename().isEmpty()) {
0614         resource->setFilename(resource->name().split(" ").join("_") + resource->defaultFileExtension());
0615     }
0616 
0617     if (resource->version() != 0) { // Can happen with cloned resources
0618         resource->setVersion(0);
0619     }
0620 
0621     // Save the resource to the storage storage
0622     if (!storage->addResource(resource)) {
0623         qWarning() << "Could not add resource" << resource->filename() << "to the storage" << storageLocation;
0624         return false;
0625     }
0626 
0627     resource->setStorageLocation(storageLocation);
0628     resource->setMD5Sum(storage->resourceMd5(resourceType + "/" + resource->filename()));
0629     resource->setDirty(false);
0630     resource->updateLinkedResourcesMetaData(KisGlobalResourcesInterface::instance());
0631 
0632     d->resourceCache[QPair<QString, QString>(storageLocation, resourceType + "/" + resource->filename())] = resource;
0633 
0634     /// And to the database.
0635     ///
0636     /// The metadata will be set by KisResourceCacheDb, which is
0637     /// not very consistent with KisResourceLocator::updateResource(),
0638     /// but works :)
0639     const bool result = KisResourceCacheDb::addResource(storage,
0640                                                         storage->timeStampForResource(resourceType, resource->filename()),
0641                                                         resource,
0642                                                         resourceType);
0643     return result;
0644 }
0645 
0646 bool KisResourceLocator::updateResource(const QString &resourceType, const KoResourceSP resource)
0647 {
0648     QString storageLocation = makeStorageLocationAbsolute(resource->storageLocation());
0649 
0650     Q_ASSERT(d->storages.contains(storageLocation));
0651 
0652     if (resource->resourceId() < 0) {
0653         return addResource(resourceType, resource);
0654     }
0655 
0656     KisResourceStorageSP storage = d->storages[storageLocation];
0657 
0658     if (!storage->supportsVersioning()) return false;
0659 
0660     // remove older version
0661     KisResourceThumbnailCache::instance()->remove(storageLocation, resourceType, resource->filename());
0662 
0663     resource->updateThumbnail();
0664     resource->setVersion(resource->version() + 1);
0665     resource->setActive(true);
0666 
0667     if (!storage->saveAsNewVersion(resource)) {
0668         qWarning() << "Failed to save the new version of " << resource->name() << "to storage" << storageLocation;
0669         return false;
0670     }
0671 
0672     resource->setMD5Sum(storage->resourceMd5(resourceType + "/" + resource->filename()));
0673     resource->setDirty(false);
0674     resource->updateLinkedResourcesMetaData(KisGlobalResourcesInterface::instance());
0675 
0676     // The version needs already to have been incremented
0677     if (!KisResourceCacheDb::addResourceVersion(resource->resourceId(), QDateTime::currentDateTime(), storage, resource)) {
0678         qWarning() << "Failed to add a new version of the resource to the database" << resource->name();
0679         return false;
0680     }
0681 
0682     if (!setMetaDataForResource(resource->resourceId(), resource->metadata())) {
0683         qWarning() << "Failed to update resource metadata" << resource;
0684         return false;
0685     }
0686 
0687     // Update the resource in the cache
0688     QPair<QString, QString> key = QPair<QString, QString> (storageLocation, resourceType + "/" + resource->filename());
0689     d->resourceCache[key] = resource;
0690     KisResourceThumbnailCache::instance()->insert(key, resource->thumbnail());
0691 
0692     return true;
0693 }
0694 
0695 bool KisResourceLocator::reloadResource(const QString &resourceType, const KoResourceSP resource)
0696 {
0697     // This resource isn't in the database yet, so we cannot reload it
0698     if (resource->resourceId() < 0) return false;
0699 
0700     QString storageLocation = makeStorageLocationAbsolute(resource->storageLocation());
0701     Q_ASSERT(d->storages.contains(storageLocation));
0702 
0703     KisResourceStorageSP storage = d->storages[storageLocation];
0704 
0705     if (!storage->loadVersionedResource(resource)) {
0706         qWarning() << "Failed to reload the resource" << resource->name() << "from storage" << storageLocation;
0707         return false;
0708     }
0709 
0710     resource->setMD5Sum(storage->resourceMd5(resourceType + "/" + resource->filename()));
0711     resource->setDirty(false);
0712     resource->updateLinkedResourcesMetaData(KisGlobalResourcesInterface::instance());
0713 
0714     // We haven't changed the version of the resource, so the cache must be still valid
0715     QPair<QString, QString> key = QPair<QString, QString> (storageLocation, resourceType + "/" + resource->filename());
0716     Q_ASSERT(d->resourceCache[key] == resource);
0717 
0718     return true;
0719 }
0720 
0721 QMap<QString, QVariant> KisResourceLocator::metaDataForResource(int id) const
0722 {
0723     return KisResourceCacheDb::metaDataForId(id, "resources");
0724 }
0725 
0726 bool KisResourceLocator::setMetaDataForResource(int id, QMap<QString, QVariant> map) const
0727 {
0728     return KisResourceCacheDb::updateMetaDataForId(map, id, "resources");
0729 }
0730 
0731 QMap<QString, QVariant> KisResourceLocator::metaDataForStorage(const QString &storageLocation) const
0732 {
0733     QMap<QString, QVariant> metadata;
0734     if (!d->storages.contains(makeStorageLocationAbsolute(storageLocation))) {
0735         qWarning() << storageLocation << "not in" << d->storages.keys();
0736         return metadata;
0737     }
0738 
0739     KisResourceStorageSP st = d->storages[makeStorageLocationAbsolute(storageLocation)];
0740 
0741     if (d->storages[makeStorageLocationAbsolute(storageLocation)].isNull()) {
0742         return metadata;
0743     }
0744 
0745     Q_FOREACH(const QString key, st->metaDataKeys()) {
0746         metadata[key] = st->metaData(key);
0747     }
0748     return metadata;
0749 }
0750 
0751 void KisResourceLocator::setMetaDataForStorage(const QString &storageLocation, QMap<QString, QVariant> map) const
0752 {
0753     Q_ASSERT(d->storages.contains(storageLocation));
0754     Q_FOREACH(const QString &key, map.keys()) {
0755         d->storages[storageLocation]->setMetaData(key, map[key]);
0756     }
0757 }
0758 
0759 void KisResourceLocator::purge(const QString &storageLocation)
0760 {
0761     Q_FOREACH(const auto key, d->resourceCache.keys()) {
0762         if (key.first == storageLocation) {
0763             d->resourceCache.remove(key);
0764             KisResourceThumbnailCache::instance()->remove(key);
0765         }
0766     }
0767 }
0768 
0769 bool KisResourceLocator::addStorage(const QString &storageLocation, KisResourceStorageSP storage)
0770 {
0771     if (d->storages.contains(storageLocation)) {
0772         if (!removeStorage(storageLocation)) {
0773             qWarning() << "could not remove" << storageLocation;
0774             return false;
0775         }
0776     }
0777 
0778     QVector<std::pair<QString, int>> addedResources;
0779     Q_FOREACH(const QString &type, KisResourceLoaderRegistry::instance()->resourceTypes()) {
0780         int numAddedResources = 0;
0781 
0782         QSharedPointer<KisResourceStorage::ResourceIterator> it = storage->resources(type);
0783         while (it->hasNext()) {
0784             it->next();
0785             numAddedResources++;
0786         }
0787 
0788         if (numAddedResources > 0) {
0789             addedResources << std::make_pair(type, numAddedResources);
0790         }
0791     }
0792 
0793     Q_FOREACH (const auto &typedResources, addedResources) {
0794         Q_EMIT beginExternalResourceImport(typedResources.first, typedResources.second);
0795     }
0796 
0797     d->storages[storageLocation] = storage;
0798     if (!KisResourceCacheDb::addStorage(storage, false)) {
0799         d->errorMessages.append(i18n("Could not add %1 to the database", storage->location()));
0800         qWarning() << d->errorMessages;
0801         return false;
0802     }
0803 
0804     if (!KisResourceCacheDb::addStorageTags(storage)) {
0805         d->errorMessages.append(QString("Could not add tags for storage %1 to the cache database").arg(storage->location()));
0806         qWarning() << d->errorMessages;
0807         return false;
0808     }
0809 
0810     Q_FOREACH (const auto &typedResources, addedResources) {
0811         Q_EMIT endExternalResourceImport(typedResources.first);
0812     }
0813 
0814     Q_EMIT storageAdded(makeStorageLocationRelative(storage->location()));
0815     return true;
0816 }
0817 
0818 bool KisResourceLocator::removeStorage(const QString &storageLocation)
0819 {
0820     // Cloned documents have a document storage, but that isn't in the locator.
0821     if (!d->storages.contains(storageLocation)) {
0822         return true;
0823     }
0824 
0825     QVector<std::pair<QString, QVector<int>>> removedResources;
0826 
0827     Q_FOREACH(const QString &type, KisResourceLoaderRegistry::instance()->resourceTypes()) {
0828         const QVector<int> resources = KisResourceCacheDb::resourcesForStorage(type, storageLocation);
0829         if (!resources.isEmpty()) {
0830             removedResources << std::make_pair(type, resources);
0831         }
0832     }
0833 
0834     Q_FOREACH (const auto &typedResources, removedResources) {
0835         Q_EMIT beginExternalResourceRemove(typedResources.first, typedResources.second);
0836     }
0837 
0838     purge(storageLocation);
0839 
0840     KisResourceStorageSP storage = d->storages.take(storageLocation);
0841 
0842     if (!KisResourceCacheDb::deleteStorage(storage)) {
0843         d->errorMessages.append(i18n("Could not remove storage %1 from the database", storage->location()));
0844         qWarning() << d->errorMessages;
0845         return false;
0846     }
0847 
0848     Q_FOREACH (const auto &typedResources, removedResources) {
0849         Q_EMIT endExternalResourceRemove(typedResources.first);
0850     }
0851 
0852     Q_EMIT storageRemoved(makeStorageLocationRelative(storage->location()));
0853 
0854     return true;
0855 }
0856 
0857 bool KisResourceLocator::hasStorage(const QString &document)
0858 {
0859     return d->storages.contains(document);
0860 }
0861 
0862 void KisResourceLocator::saveTags()
0863 {
0864     QSqlQuery query;
0865 
0866     if (!query.prepare("SELECT tags.url \n"
0867                        ",      resource_types.name \n"
0868                        "FROM   tags\n"
0869                        ",      resource_types\n"
0870                        "WHERE  tags.resource_type_id = resource_types.id\n"))
0871     {
0872         qWarning() << "Could not prepare save tags query" << query.lastError();
0873         return;
0874     }
0875 
0876     if (!query.exec()) {
0877         qWarning() << "Could not execute save tags query" << query.lastError();
0878         return;
0879     }
0880 
0881     // this needs to use ResourcePaths because it is sometimes called during initialization
0882     // (when the database versions don't match up and tags need to be saved)
0883     QString resourceLocation = KoResourcePaths::getAppDataLocation() + "/";
0884 
0885     while (query.next()) {
0886         // Save tag...
0887         KisTagSP tag = tagForUrlNoCache(query.value("tags.url").toString(),
0888                                  query.value("resource_types.name").toString());
0889 
0890         if (!tag || !tag->valid()) {
0891             continue;
0892         }
0893 
0894 
0895         QString filename = tag->filename();
0896         if (filename.isEmpty() || QFileInfo(filename).suffix().isEmpty()) {
0897             filename = tag->url() + ".tag";
0898         }
0899 
0900 
0901         if (QFileInfo(filename).suffix() != "tag" && QFileInfo(filename).suffix() != "TAG") {
0902             // it's either .abr file, or maybe a .bundle
0903             // or something else, but not a tag file
0904             dbgResources << "Skipping saving tag " << tag->name(false) << filename << tag->resourceType();
0905             continue;
0906         }
0907 
0908         filename.remove(resourceLocation);
0909 
0910         QFile f(resourceLocation + "/" + tag->resourceType() + '/' + filename);
0911 
0912         if (!f.open(QFile::WriteOnly)) {
0913             qWarning () << "Could not open tag file for writing" << f.fileName();
0914             continue;
0915         }
0916 
0917         QBuffer buf;
0918         buf.open(QIODevice::WriteOnly);;
0919 
0920         if (!tag->save(buf)) {
0921             qWarning() << "Could not save tag to" << f.fileName();
0922             buf.close();
0923             f.close();
0924             continue;
0925         }
0926 
0927         f.write(buf.data());
0928         f.flush();
0929 
0930         f.close();
0931     }
0932 }
0933 
0934 void KisResourceLocator::purgeTag(const QString tagUrl, const QString resourceType)
0935 {
0936     d->tagCache.remove(QPair<QString, QString>(resourceType, tagUrl));
0937 }
0938 
0939 QString KisResourceLocator::filePathForResource(KoResourceSP resource)
0940 {
0941     const QString storageLocation = makeStorageLocationAbsolute(resource->storageLocation());
0942     KisResourceStorageSP storage = d->storages[storageLocation];
0943     if (!storage) {
0944         qWarning() << "Could not find storage" << storageLocation;
0945         return QString();
0946     }
0947 
0948     const QString resourceUrl = resource->resourceType().first + "/" + resource->filename();
0949 
0950     return storage->resourceFilePath(resourceUrl);
0951 }
0952 
0953 KisResourceLocator::LocatorError KisResourceLocator::firstTimeInstallation(InitializationStatus initializationStatus, const QString &installationResourcesLocation)
0954 {
0955     emit progressMessage(i18n("Krita is running for the first time. Initialization will take some time."));
0956     Q_UNUSED(initializationStatus);
0957 
0958     Q_FOREACH(const QString &folder, KisResourceLoaderRegistry::instance()->resourceTypes()) {
0959         QDir dir(d->resourceLocation + '/' + folder + '/');
0960         if (!dir.exists()) {
0961             if (!QDir().mkpath(d->resourceLocation + '/' + folder + '/')) {
0962                 d->errorMessages << i18n("3. Could not create the resource location at %1.", dir.path());
0963                 return LocatorError::CannotCreateLocation;
0964             }
0965         }
0966     }
0967 
0968     Q_FOREACH(const QString &folder, KisResourceLoaderRegistry::instance()->resourceTypes()) {
0969         QDir dir(installationResourcesLocation + '/' + folder + '/');
0970         if (dir.exists()) {
0971             Q_FOREACH(const QString &entry, dir.entryList(QDir::Files | QDir::Readable)) {
0972                 QFile f(dir.canonicalPath() + '/'+ entry);
0973                 if (!QFileInfo(d->resourceLocation + '/' + folder + '/' + entry).exists()) {
0974                     if (!f.copy(d->resourceLocation + '/' + folder + '/' + entry)) {
0975                         d->errorMessages << i18n("Could not copy resource %1 to %2", f.fileName(), d->resourceLocation + '/' + folder + '/' + entry);
0976                     }
0977                 }
0978             }
0979         }
0980     }
0981 
0982     // And add bundles and adobe libraries
0983     QStringList filters = QStringList() << "*.bundle" << "*.abr" << "*.asl";
0984     QDirIterator iter(installationResourcesLocation, filters, QDir::Files, QDirIterator::Subdirectories);
0985     while (iter.hasNext()) {
0986         iter.next();
0987         emit progressMessage(i18n("Installing the resources from bundle %1.", iter.filePath()));
0988         QFile f(iter.filePath());
0989         Q_ASSERT(f.exists());
0990         if (!f.copy(d->resourceLocation + '/' + iter.fileName())) {
0991             d->errorMessages << i18n("Could not copy resource %1 to %2", f.fileName(), d->resourceLocation);
0992         }
0993     }
0994 
0995     QFile f(d->resourceLocation + '/' + "KRITA_RESOURCE_VERSION");
0996     f.open(QFile::WriteOnly);
0997     f.write(KritaVersionWrapper::versionString().toUtf8());
0998     f.close();
0999 
1000     if (!initializeDb()) {
1001         return LocatorError::CannotInitializeDb;
1002     }
1003 
1004     return LocatorError::Ok;
1005 }
1006 
1007 bool KisResourceLocator::initializeDb()
1008 {
1009     emit progressMessage(i18n("Initializing the resources."));
1010     d->errorMessages.clear();
1011     findStorages();
1012 
1013     Q_FOREACH(auto loader, KisResourceLoaderRegistry::instance()->values()) {
1014         KisResourceCacheDb::registerResourceType(loader->resourceType());
1015     }
1016 
1017     Q_FOREACH(KisResourceStorageSP storage, d->storages) {
1018         if (!KisResourceCacheDb::addStorage(storage, (storage->type() == KisResourceStorage::StorageType::Folder ? false : true))) {
1019             d->errorMessages.append(QString("Could not add storage %1 to the cache database").arg(storage->location()));
1020         }
1021     }
1022 
1023     Q_FOREACH(KisResourceStorageSP storage, d->storages) {
1024         if (!KisResourceCacheDb::addStorageTags(storage)) {
1025             d->errorMessages.append(QString("Could not add tags for storage %1 to the cache database").arg(storage->location()));
1026         }
1027     }
1028     return (d->errorMessages.isEmpty());
1029 }
1030 
1031 void KisResourceLocator::findStorages()
1032 {
1033     d->storages.clear();
1034     d->resourceCache.clear();
1035 
1036     // Add the folder
1037     KisResourceStorageSP storage = QSharedPointer<KisResourceStorage>::create(d->resourceLocation);
1038     Q_ASSERT(storage->location() == d->resourceLocation);
1039     d->storages[d->resourceLocation] = storage;
1040 
1041     // Add the memory storage
1042     d->storages["memory"] = QSharedPointer<KisResourceStorage>::create("memory");
1043     d->storages["memory"]->setMetaData(KisResourceStorage::s_meta_name, i18n("Temporary Resources"));
1044 
1045     // And add bundles and adobe libraries
1046     QStringList filters = QStringList() << "*.bundle" << "*.abr" << "*.asl";
1047     QDirIterator iter(d->resourceLocation, filters, QDir::Files, QDirIterator::Subdirectories);
1048     while (iter.hasNext()) {
1049         iter.next();
1050         KisResourceStorageSP storage = QSharedPointer<KisResourceStorage>::create(iter.filePath());
1051         if (!storage->valid()) {
1052             // we still add the storage to the list and try to read whatever possible
1053             qWarning() << "KisResourceLocator::findStorages: the storage is invalid" << storage->location();
1054         }
1055         d->storages[storage->location()] = storage;
1056     }
1057 }
1058 
1059 QList<KisResourceStorageSP> KisResourceLocator::storages() const
1060 {
1061     return d->storages.values();
1062 }
1063 
1064 KisResourceStorageSP KisResourceLocator::storageByLocation(const QString &location) const
1065 {
1066     if (!d->storages.contains(location)) {
1067         qWarning() << "No" << location << "storage defined:" << d->storages.keys();
1068         return 0;
1069     }
1070     KisResourceStorageSP storage = d->storages[location];
1071     if (!storage || !storage->valid()) {
1072         qWarning() << "Could not retrieve the" << location << "storage object or the object is not valid";
1073         return 0;
1074     }
1075 
1076     return storage;
1077 }
1078 
1079 KisResourceStorageSP KisResourceLocator::folderStorage() const
1080 {
1081     return storageByLocation(d->resourceLocation);
1082 }
1083 
1084 KisResourceStorageSP KisResourceLocator::memoryStorage() const
1085 {
1086     return storageByLocation("memory");
1087 }
1088 
1089 KisResourceLocator::ResourceStorage KisResourceLocator::getResourceStorage(int resourceId) const
1090 {
1091     ResourceStorage rs;
1092 
1093     QSqlQuery q;
1094     bool r = q.prepare("SELECT storages.location\n"
1095                        ",      resource_types.name as resource_type\n"
1096                        ",      resources.filename\n"
1097                        "FROM   resources\n"
1098                        ",      storages\n"
1099                        ",      resource_types\n"
1100                        "WHERE  resources.id = :resource_id\n"
1101                        "AND    resources.storage_id = storages.id\n"
1102                        "AND    resource_types.id = resources.resource_type_id");
1103     if (!r) {
1104         qWarning() << "KisResourceLocator::removeResource: could not prepare query." << q.lastError();
1105         return rs;
1106     }
1107 
1108 
1109     q.bindValue(":resource_id", resourceId);
1110 
1111     r = q.exec();
1112     if (!r) {
1113         qWarning() << "KisResourceLocator::removeResource: could not execute query." << q.lastError();
1114         return rs;
1115     }
1116 
1117     q.first();
1118 
1119     QString storageLocation = q.value("location").toString();
1120     QString resourceType= q.value("resource_type").toString();
1121     QString resourceFilename = q.value("filename").toString();
1122 
1123     rs.storageLocation = makeStorageLocationAbsolute(storageLocation);
1124     rs.resourceType = resourceType;
1125     rs.resourceFileName = resourceFilename;
1126 
1127     return rs;
1128 }
1129 
1130 QString KisResourceLocator::makeStorageLocationAbsolute(QString storageLocation) const
1131 {
1132 //    debugResource << "makeStorageLocationAbsolute" << storageLocation;
1133 
1134     if (storageLocation.isEmpty()) {
1135         return resourceLocationBase();
1136     }
1137 
1138     if (QFileInfo(storageLocation).isRelative() && (storageLocation.endsWith(".bundle", Qt::CaseInsensitive)
1139                                              || storageLocation.endsWith(".asl", Qt::CaseInsensitive)
1140                                              || storageLocation.endsWith(".abr", Qt::CaseInsensitive))) {
1141         if (resourceLocationBase().endsWith('/') || resourceLocationBase().endsWith("\\")) {
1142             storageLocation = resourceLocationBase() + storageLocation;
1143         }
1144         else {
1145             storageLocation = resourceLocationBase() + '/' + storageLocation;
1146         }
1147     }
1148 
1149 //    debugResource  << "\t" << storageLocation;
1150     return storageLocation;
1151 }
1152 
1153 bool KisResourceLocator::synchronizeDb()
1154 {
1155     d->errorMessages.clear();
1156 
1157     // Add resource types that have been added since first-time installation.
1158     Q_FOREACH(auto loader, KisResourceLoaderRegistry::instance()->values()) {
1159         KisResourceCacheDb::registerResourceType(loader->resourceType());
1160     }
1161 
1162 
1163     findStorages();
1164     Q_FOREACH(const KisResourceStorageSP storage, d->storages) {
1165         if (!KisResourceCacheDb::synchronizeStorage(storage)) {
1166             d->errorMessages.append(i18n("Could not synchronize %1 with the database", storage->location()));
1167         }
1168     }
1169 
1170     Q_FOREACH(const KisResourceStorageSP storage, d->storages) {
1171         if (!KisResourceCacheDb::addStorageTags(storage)) {
1172             d->errorMessages.append(i18n("Could not synchronize %1 with the database", storage->location()));
1173         }
1174     }
1175 
1176     // now remove the storages that no longer exists
1177     KisStorageModel model;
1178 
1179     QList<QString> storagesToRemove;
1180     for (int i = 0; i < model.rowCount(); i++) {
1181         QModelIndex idx = model.index(i, 0);
1182         QString location = model.data(idx, Qt::UserRole + KisStorageModel::Location).toString();
1183         storagesToRemove << location;
1184     }
1185 
1186     for (int i = 0; i < storagesToRemove.size(); i++) {
1187         QString location = storagesToRemove[i];
1188         if (!d->storages.contains(this->makeStorageLocationAbsolute(location))) {
1189             if (!KisResourceCacheDb::deleteStorage(location)) {
1190                 d->errorMessages.append(i18n("Could not remove storage %1 from the database", this->makeStorageLocationAbsolute(location)));
1191                 qWarning() << d->errorMessages;
1192                 return false;
1193             }
1194             emit storageRemoved(this->makeStorageLocationAbsolute(location));
1195         }
1196     }
1197 
1198 
1199     d->errorMessages <<
1200         KisResourceLoaderRegistry::instance()->executeAllFixups();
1201 
1202     d->resourceCache.clear();
1203     return d->errorMessages.isEmpty();
1204 }
1205 
1206 
1207 QString KisResourceLocator::makeStorageLocationRelative(QString location) const
1208 {
1209 //    debugResource << "makeStorageLocationRelative" << location << "locationbase" << resourceLocationBase();
1210     return location.remove(resourceLocationBase());
1211 }