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 }