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 }