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

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