File indexing completed on 2024-05-12 15:59:51

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
0003  * SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "KisResourceStorage.h"
0009 
0010 #include <QApplication>
0011 #include <QDebug>
0012 #include <QFileInfo>
0013 #include <QUuid>
0014 #include <QtMath>
0015 #include <QRegularExpression>
0016 
0017 #include <cmath>
0018 #include <quazip.h>
0019 #include <boost/optional.hpp>
0020 
0021 #include <kis_debug.h>
0022 #include <kis_pointer_utils.h>
0023 
0024 
0025 #include "KisFolderStorage.h"
0026 #include "KisBundleStorage.h"
0027 #include "KisMemoryStorage.h"
0028 
0029 
0030 const QString KisResourceStorage::s_meta_generator("meta:generator");
0031 const QString KisResourceStorage::s_meta_author("dc:author");
0032 const QString KisResourceStorage::s_meta_title("dc:title");
0033 const QString KisResourceStorage::s_meta_description("dc:description");
0034 const QString KisResourceStorage::s_meta_initial_creator("meta:initial-creator");
0035 const QString KisResourceStorage::s_meta_creator("cd:creator");
0036 const QString KisResourceStorage::s_meta_creation_date("meta:creation-date");
0037 const QString KisResourceStorage::s_meta_dc_date("meta:dc-date");
0038 const QString KisResourceStorage::s_meta_user_defined("meta:meta-userdefined");
0039 const QString KisResourceStorage::s_meta_name("meta:name");
0040 const QString KisResourceStorage::s_meta_value("meta:value");
0041 const QString KisResourceStorage::s_meta_version("meta:bundle-version");
0042 const QString KisResourceStorage::s_meta_email("meta:email");
0043 const QString KisResourceStorage::s_meta_license("meta:license");
0044 const QString KisResourceStorage::s_meta_website("meta:website");
0045 
0046 Q_GLOBAL_STATIC(KisStoragePluginRegistry, s_instance);
0047 
0048 KisStoragePluginRegistry::KisStoragePluginRegistry()
0049 {
0050     m_storageFactoryMap[KisResourceStorage::StorageType::Folder] = new KisStoragePluginFactory<KisFolderStorage>();
0051     m_storageFactoryMap[KisResourceStorage::StorageType::Memory] = new KisStoragePluginFactory<KisMemoryStorage>();
0052     m_storageFactoryMap[KisResourceStorage::StorageType::Bundle] = new KisStoragePluginFactory<KisBundleStorage>();
0053 }
0054 
0055 KisStoragePluginRegistry::~KisStoragePluginRegistry()
0056 {
0057     qDeleteAll(m_storageFactoryMap.values());
0058 }
0059 
0060 void KisStoragePluginRegistry::addStoragePluginFactory(KisResourceStorage::StorageType storageType, KisStoragePluginFactoryBase *factory)
0061 {
0062     m_storageFactoryMap[storageType] = factory;
0063 }
0064 
0065 KisStoragePluginRegistry *KisStoragePluginRegistry::instance()
0066 {
0067     return s_instance;
0068 }
0069 
0070 class KisResourceStorage::Private {
0071 public:
0072     QString name;
0073     QString location;
0074     bool valid {false};
0075     KisResourceStorage::StorageType storageType {KisResourceStorage::StorageType::Unknown};
0076     QSharedPointer<KisStoragePlugin> storagePlugin;
0077     int storageId {-1};
0078 };
0079 
0080 KisResourceStorage::KisResourceStorage(const QString &location)
0081     : d(new Private())
0082 {
0083     d->location = location;
0084     d->name = QFileInfo(d->location).fileName();
0085     QFileInfo fi(d->location);
0086     if (fi.isDir()) {
0087         d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::Folder]->create(location));
0088         d->storageType = StorageType::Folder;
0089         d->valid = fi.isWritable();
0090     }
0091     else if (d->name.endsWith(".bundle", Qt::CaseInsensitive)) {
0092             d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::Bundle]->create(location));
0093             d->storageType = StorageType::Bundle;
0094             // XXX: should we also check whether there's a valid metadata entry? Or is this enough?
0095             d->valid = (fi.isReadable() && QuaZip(d->location).open(QuaZip::mdUnzip));
0096     } else if (d->name.endsWith(".abr", Qt::CaseInsensitive)) {
0097             d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::AdobeBrushLibrary]->create(location));
0098             d->storageType = StorageType::AdobeBrushLibrary;
0099             d->valid = fi.isReadable();
0100     } else if (d->name.endsWith(".asl", Qt::CaseInsensitive)) {
0101             d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::AdobeStyleLibrary]->create(location));
0102             d->storageType = StorageType::AdobeStyleLibrary;
0103             d->valid = d->storagePlugin->isValid();
0104     } else if (d->location == "memory" || !QUuid::fromString(d->location).isNull() || (!d->location.isEmpty() && !fi.exists())) {
0105         d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::Memory]->create(location));
0106         d->name = location;
0107         d->storageType = StorageType::Memory;
0108         d->valid = true;
0109     } else {
0110         // we create a fake memeory storage to make sure methods like `timestamp()` still work
0111         d->storagePlugin.reset(KisStoragePluginRegistry::instance()->m_storageFactoryMap[StorageType::Memory]->create(location));
0112         d->valid = false;
0113     }
0114 }
0115 
0116 KisResourceStorage::~KisResourceStorage()
0117 {
0118 }
0119 
0120 KisResourceStorage::KisResourceStorage(const KisResourceStorage &rhs)
0121     : d(new Private)
0122 {
0123     *this = rhs;
0124 }
0125 
0126 KisResourceStorage &KisResourceStorage::operator=(const KisResourceStorage &rhs)
0127 {
0128     if (this != &rhs) {
0129         d->name = rhs.d->name;
0130         d->location = rhs.d->location;
0131         d->storageType = rhs.d->storageType;
0132         if (d->storageType == StorageType::Memory) {
0133             d->storagePlugin = QSharedPointer<KisMemoryStorage>(new KisMemoryStorage(*dynamic_cast<KisMemoryStorage*>(rhs.d->storagePlugin.data())));
0134         }
0135         else {
0136             d->storagePlugin = rhs.d->storagePlugin;
0137         }
0138         d->valid = false;
0139     }
0140     return *this;
0141 }
0142 
0143 KisResourceStorageSP KisResourceStorage::clone() const
0144 {
0145     return KisResourceStorageSP(new KisResourceStorage(*this));
0146 }
0147 
0148 QString KisResourceStorage::name() const
0149 {
0150     return d->name;
0151 }
0152 
0153 QString KisResourceStorage::location() const
0154 {
0155     return d->location;
0156 }
0157 
0158 KisResourceStorage::StorageType KisResourceStorage::type() const
0159 {
0160     return d->storageType;
0161 }
0162 
0163 QImage KisResourceStorage::thumbnail() const
0164 {
0165     return d->storagePlugin->thumbnail();
0166 }
0167 
0168 QDateTime KisResourceStorage::timestamp() const
0169 {
0170     return d->storagePlugin->timestamp();
0171 }
0172 
0173 QDateTime KisResourceStorage::timeStampForResource(const QString &resourceType, const QString &filename) const
0174 {
0175     QFileInfo li(d->location);
0176     if (li.suffix().toLower() == "bundle") {
0177         QFileInfo bf(d->location + "_modified/" + resourceType + "/" + filename);
0178         if (bf.exists()) {
0179             return bf.lastModified();
0180         }
0181     } else if (QFileInfo(d->location + "/" + resourceType + "/" + filename).exists()) {
0182         return QFileInfo(d->location + "/" + resourceType + "/" + filename).lastModified();
0183     }
0184     return this->timestamp();
0185 }
0186 
0187 KisResourceStorage::ResourceItem KisResourceStorage::resourceItem(const QString &url)
0188 {
0189     return d->storagePlugin->resourceItem(url);
0190 }
0191 
0192 KoResourceSP KisResourceStorage::resource(const QString &url)
0193 {
0194     return d->storagePlugin->resource(url);
0195 }
0196 
0197 QString KisResourceStorage::resourceMd5(const QString &url)
0198 {
0199     return d->storagePlugin->resourceMd5(url);
0200 }
0201 
0202 QString KisResourceStorage::resourceFilePath(const QString &url)
0203 {
0204     return d->storagePlugin->resourceFilePath(url);
0205 }
0206 
0207 QSharedPointer<KisResourceStorage::ResourceIterator> KisResourceStorage::resources(const QString &resourceType) const
0208 {
0209     return d->storagePlugin->resources(resourceType);
0210 }
0211 
0212 QSharedPointer<KisResourceStorage::TagIterator> KisResourceStorage::tags(const QString &resourceType) const
0213 {
0214     return d->storagePlugin->tags(resourceType);
0215 }
0216 
0217 bool KisResourceStorage::saveAsNewVersion(KoResourceSP resource)
0218 {
0219     return d->storagePlugin->saveAsNewVersion(resource->resourceType().first, resource);
0220 }
0221 
0222 bool KisResourceStorage::addResource(KoResourceSP resource)
0223 {
0224     return d->storagePlugin->addResource(resource->resourceType().first, resource);
0225 }
0226 
0227 bool KisResourceStorage::importResource(const QString &url, QIODevice *device)
0228 {
0229     return d->storagePlugin->importResource(url, device);
0230 }
0231 
0232 bool KisResourceStorage::exportResource(const QString &url, QIODevice *device)
0233 {
0234     return d->storagePlugin->exportResource(url, device);
0235 }
0236 
0237 bool KisResourceStorage::supportsVersioning() const
0238 {
0239     return d->storagePlugin->supportsVersioning();
0240 }
0241 
0242 bool KisResourceStorage::loadVersionedResource(KoResourceSP resource)
0243 {
0244     return d->storagePlugin->loadVersionedResource(resource);
0245 }
0246 
0247 void KisResourceStorage::setMetaData(const QString &key, const QVariant &value)
0248 {
0249     d->storagePlugin->setMetaData(key, value);
0250 }
0251 
0252 bool KisResourceStorage::valid() const
0253 {
0254     return d->valid;
0255 }
0256 
0257 QStringList KisResourceStorage::metaDataKeys() const
0258 {
0259     return d->storagePlugin->metaDataKeys();
0260 }
0261 
0262 QVariant KisResourceStorage::metaData(const QString &key) const
0263 {
0264     return d->storagePlugin->metaData(key);
0265 }
0266 
0267 void KisResourceStorage::setStorageId(int storageId)
0268 {
0269     d->storageId = storageId;
0270 }
0271 
0272 int KisResourceStorage::storageId()
0273 {
0274     return d->storageId;
0275 }
0276 
0277 struct VersionedFileParts
0278 {
0279     QString basename;
0280     int version = 0;
0281     QString suffix;
0282 };
0283 
0284 boost::optional<VersionedFileParts> guessFilenameParts(const QString &filename)
0285 {
0286     QRegularExpression exp("^(.*)\\.(\\d\\d*)\\.(.+)$");
0287 
0288     QRegularExpressionMatch res = exp.match(filename);
0289 
0290     if (res.hasMatch()) {
0291         return VersionedFileParts({res.captured(1), res.captured(2).toInt(), res.captured(3)});
0292     }
0293 
0294     return boost::none;
0295 }
0296 
0297 VersionedFileParts guessFileNamePartsLazy(const QString &filename, int minVersion)
0298 {
0299     boost::optional<VersionedFileParts> guess = guessFilenameParts(filename);
0300     if (guess) {
0301         guess->version = qMax(guess->version, minVersion);
0302     } else {
0303         QFileInfo info(filename);
0304         guess = VersionedFileParts();
0305         guess->basename = info.baseName();
0306         guess->version = minVersion;
0307         guess->suffix = info.completeSuffix();
0308     }
0309 
0310     return *guess;
0311 }
0312 
0313 QString KisStorageVersioningHelper::chooseUniqueName(KoResourceSP resource,
0314                                                      int minVersion,
0315                                                      std::function<bool(QString)> checkExists)
0316 {
0317     int version = qMax(resource->version(), minVersion);
0318 
0319     VersionedFileParts parts = guessFileNamePartsLazy(resource->filename(), version);
0320     version = parts.version;
0321 
0322     QString newFilename;
0323 
0324     while (1) {
0325         int numPlaceholders = 4;
0326 
0327         if (version > 9999) {
0328             numPlaceholders = qFloor(std::log10(version)) + 1;
0329         }
0330 
0331         QString versionString = QString("%1").arg(version, numPlaceholders, 10, QChar('0'));
0332 
0333         // XXX: Temporary, until I've fixed the tests
0334         if (versionString == "0000" && qApp->applicationName() == "krita") {
0335             newFilename = resource->filename();
0336         }
0337         else {
0338             newFilename = parts.basename +
0339                     "."
0340                     + versionString
0341                     + "."
0342                     + parts.suffix;
0343         }
0344         if (checkExists(newFilename)) {
0345             version++;
0346             if (version == std::numeric_limits<int>::max()) {
0347                 return QString();
0348             }
0349             continue;
0350         }
0351 
0352         break;
0353     }
0354 
0355     return newFilename;
0356 }
0357 
0358 void KisStorageVersioningHelper::detectFileVersions(QVector<VersionedResourceEntry> &allFiles)
0359 {
0360     for (auto it = allFiles.begin(); it != allFiles.end(); ++it) {
0361         VersionedFileParts parts = guessFileNamePartsLazy(it->filename, -1);
0362         it->guessedKey = parts.basename + parts.suffix;
0363         it->guessedVersion = parts.version;
0364     }
0365 
0366     std::sort(allFiles.begin(), allFiles.end(), VersionedResourceEntry::KeyVersionLess());
0367 
0368     boost::optional<QString> lastResourceKey;
0369     int availableVersion = 0;
0370     for (auto it = allFiles.begin(); it != allFiles.end(); ++it) {
0371         if (!lastResourceKey || *lastResourceKey != it->guessedKey) {
0372             availableVersion = 0;
0373             lastResourceKey = it->guessedKey;
0374         }
0375 
0376         if (it->guessedVersion < availableVersion) {
0377             it->guessedVersion = availableVersion;
0378         }
0379 
0380         availableVersion = it->guessedVersion + 1;
0381     }
0382 }
0383 
0384 bool KisStorageVersioningHelper::addVersionedResource(const QString &saveLocation,
0385                                                       KoResourceSP resource,
0386                                                       int minVersion)
0387 {
0388     int version = qMax(resource->version(), minVersion);
0389 
0390     VersionedFileParts parts = guessFileNamePartsLazy(resource->filename(), version);
0391     version = parts.version;
0392 
0393     QString newFilename =
0394         chooseUniqueName(resource, minVersion,
0395                          [saveLocation] (const QString &filename) {
0396                              return QFileInfo(saveLocation + "/" + filename).exists();
0397                          });
0398 
0399     if (newFilename.isEmpty()) return false;
0400 
0401     QFile file(saveLocation + "/" + newFilename);
0402     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!file.exists(), false);
0403 
0404     if (!file.open(QFile::WriteOnly)) {
0405         qWarning() << "Could not open resource file for writing" << newFilename;
0406         return false;
0407     }
0408 
0409     if (!resource->saveToDevice(&file)) {
0410         qWarning() << "Could not save resource file" << newFilename;
0411         return false;
0412     }
0413 
0414     resource->setFilename(newFilename);
0415     file.close();
0416 
0417     if (!resource->thumbnailPath().isEmpty()) {
0418         // hack for MyPaint brush presets thumbnails
0419         // note: for all versions of the preset, it will try to save in the same place
0420         if (!QFileInfo(saveLocation + "/" + resource->thumbnailPath()).exists()) {
0421             QImage thumbnail = resource->thumbnail();
0422             thumbnail.save(saveLocation + "/" + resource->thumbnailPath());
0423         }
0424     }
0425 
0426     return true;
0427 }
0428 
0429 
0430 QSharedPointer<KisResourceStorage::ResourceIterator> KisResourceStorage::ResourceIterator::versions() const
0431 {
0432     struct DumbIterator : public ResourceIterator
0433     {
0434     public:
0435         DumbIterator(const ResourceIterator *parent)
0436             : m_parent(parent)
0437         {
0438         }
0439 
0440         bool hasNext() const override {
0441             return !m_isStarted;
0442         }
0443 
0444         void next() override {
0445             KIS_SAFE_ASSERT_RECOVER_NOOP(!m_isStarted);
0446             m_isStarted = true;
0447         }
0448         QString url() const override
0449         {
0450             return m_parent->url();
0451         }
0452 
0453         QString type() const override
0454         {
0455             return m_parent->type();
0456         }
0457 
0458         QDateTime lastModified() const override
0459         {
0460             return m_parent->lastModified();
0461         }
0462 
0463         int guessedVersion() const override
0464         {
0465             return m_parent->guessedVersion();
0466         }
0467 
0468         QSharedPointer<KisResourceStorage::ResourceIterator> versions() const override
0469         {
0470             return toQShared(new DumbIterator(m_parent));
0471         }
0472 
0473     protected:
0474         KoResourceSP resourceImpl() const override
0475         {
0476             return m_parent->resource();
0477         }
0478 
0479     private:
0480         bool m_isStarted = false;
0481         const ResourceIterator *m_parent;
0482     };
0483 
0484     return QSharedPointer<KisResourceStorage::ResourceIterator>(new DumbIterator(this));
0485 }
0486 
0487 KoResourceSP KisResourceStorage::ResourceIterator::resource() const
0488 {
0489     if (m_cachedResource && m_cachedResourceUrl == url()) {
0490         return m_cachedResource;
0491     }
0492 
0493     m_cachedResource = resourceImpl();
0494     m_cachedResourceUrl = url();
0495 
0496     return m_cachedResource;
0497 }
0498 
0499 KisVersionedStorageIterator::KisVersionedStorageIterator(const QVector<VersionedResourceEntry> &entries, KisStoragePlugin *_q)
0500     : q(_q)
0501     , m_entries(entries)
0502     , m_begin(m_entries.constBegin())
0503     , m_end(m_entries.constEnd())
0504 
0505 {
0506     //    ENTER_FUNCTION() << ppVar(std::distance(m_begin, m_end));
0507     //    for (auto it = m_begin; it != m_end; ++it) {
0508     //        qDebug() << ppVar(it->filename) << ppVar(it->guessedVersion);
0509     //    }
0510 }
0511 
0512 KisVersionedStorageIterator::KisVersionedStorageIterator(const QVector<VersionedResourceEntry> &entries,
0513                                                          QVector<VersionedResourceEntry>::const_iterator begin,
0514                                                          QVector<VersionedResourceEntry>::const_iterator end,
0515                                                          KisStoragePlugin *_q)
0516     : q(_q)
0517     , m_entries(entries)
0518     , m_begin(begin)
0519     , m_end(end)
0520 {
0521 //    ENTER_FUNCTION() << ppVar(std::distance(m_begin, m_end));
0522 //    for (auto it = m_begin; it != m_end; ++it) {
0523 //        qDebug() << ppVar(it->filename) << ppVar(it->guessedVersion);
0524 //    }
0525 }
0526 
0527 bool KisVersionedStorageIterator::hasNext() const
0528 {
0529     return (!m_isStarted && m_begin != m_end) ||
0530             (m_isStarted && std::next(m_it) != m_end);
0531 }
0532 
0533 void KisVersionedStorageIterator::next()
0534 {
0535 
0536     if (!m_isStarted) {
0537         m_isStarted = true;
0538         m_it = m_begin;
0539     } else {
0540         ++m_it;
0541     }
0542 
0543     KIS_SAFE_ASSERT_RECOVER_RETURN(m_it != m_end);
0544 
0545     auto nextChunk = std::upper_bound(m_it, m_end, *m_it, VersionedResourceEntry::KeyLess());
0546     m_chunkStart = m_it;
0547     m_it = std::prev(nextChunk);
0548 }
0549 
0550 QString KisVersionedStorageIterator::url() const
0551 {
0552     return m_it->resourceType + "/" + m_it->filename;
0553 }
0554 
0555 QString KisVersionedStorageIterator::type() const
0556 {
0557     return m_it->resourceType;
0558 }
0559 
0560 QDateTime KisVersionedStorageIterator::lastModified() const
0561 {
0562     return m_it->lastModified;
0563 }
0564 
0565 KoResourceSP KisVersionedStorageIterator::resourceImpl() const
0566 {
0567     return q->resource(url());
0568 }
0569 
0570 int KisVersionedStorageIterator::guessedVersion() const
0571 {
0572     return m_it->guessedVersion;
0573 }
0574 
0575 QSharedPointer<KisResourceStorage::ResourceIterator> KisVersionedStorageIterator::versions() const
0576 {
0577     struct VersionsIterator : public KisVersionedStorageIterator
0578     {
0579         VersionsIterator(const QVector<VersionedResourceEntry> &entries,
0580                          QVector<VersionedResourceEntry>::const_iterator begin,
0581                          QVector<VersionedResourceEntry>::const_iterator end,
0582                          KisStoragePlugin *_q)
0583             : KisVersionedStorageIterator(entries, begin, end, _q)
0584         {
0585         }
0586 
0587         void next() override {
0588             if (!m_isStarted) {
0589                 m_isStarted = true;
0590                 m_it = m_begin;
0591             } else {
0592                 ++m_it;
0593             }
0594         }
0595 
0596         QSharedPointer<KisResourceStorage::ResourceIterator> versions() const override{
0597             return toQShared(new VersionsIterator(m_entries, m_it, std::next(m_it), q));
0598         }
0599     };
0600 
0601     return toQShared(new VersionsIterator(m_entries, m_chunkStart, std::next(m_it), q));
0602 }