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

0001 /*
0002  * SPDX-FileCopyrightText: 2019 Boudewijn Rempt <boud@valdyas.org>
0003  * SPDX-FileCopyrightText: 2023 L. E. Segovia <amy@amyspark.me>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 #include "KisStorageModel.h"
0008 
0009 #include <QtSql>
0010 #include <QElapsedTimer>
0011 #include <KisResourceLocator.h>
0012 #include <KoResourcePaths.h>
0013 #include <KisResourceModelProvider.h>
0014 #include <KisResourceThumbnailCache.h>
0015 #include <QFileInfo>
0016 #include <kis_assert.h>
0017 
0018 #include <kconfig.h>
0019 #include <kconfiggroup.h>
0020 #include <ksharedconfig.h>
0021 
0022 Q_GLOBAL_STATIC(KisStorageModel, s_instance)
0023 
0024 struct KisStorageModel::Private {
0025     int cachedRowCount {-1};
0026     QList<QString> storages;
0027 };
0028 
0029 KisStorageModel::KisStorageModel(QObject *parent)
0030     : QAbstractTableModel(parent)
0031     , d(new Private())
0032 {
0033     connect(KisResourceLocator::instance(), SIGNAL(storageAdded(const QString&)), this, SLOT(addStorage(const QString&)));
0034     connect(KisResourceLocator::instance(), SIGNAL(storageRemoved(const QString&)), this, SLOT(removeStorage(const QString&)));
0035 
0036     QSqlQuery query;
0037 
0038     bool r = query.prepare("SELECT location\n"
0039                            "FROM   storages\n"
0040                            "ORDER BY id");
0041     if (!r) {
0042         qWarning() << "Could not prepare KisStorageModel query" << query.lastError();
0043     }
0044 
0045     r = query.exec();
0046 
0047     if (!r) {
0048         qWarning() << "Could not execute KisStorageModel query" << query.lastError();
0049     }
0050 
0051     while (query.next()) {
0052         d->storages << query.value(0).toString();
0053     }
0054 }
0055 
0056 KisStorageModel *KisStorageModel::instance()
0057 {
0058     return s_instance;
0059 }
0060 
0061 KisStorageModel::~KisStorageModel()
0062 {
0063 }
0064 
0065 int KisStorageModel::rowCount(const QModelIndex &parent) const
0066 {
0067     if (parent.isValid()) {
0068         return 0;
0069     }
0070     return d->storages.size();
0071 
0072 }
0073 
0074 int KisStorageModel::columnCount(const QModelIndex &parent) const
0075 {
0076     if (parent.isValid()) {
0077         return 0;
0078     }
0079 
0080     return (int)MetaData;
0081 }
0082 
0083 QImage KisStorageModel::getThumbnailFromQuery(const QSqlQuery &query)
0084 {
0085     const QString storageLocation = query.value("location").toString();
0086     const QString storageType = query.value("storage_type").toString();
0087     const QString storageIdAsString = query.value("id").toString();
0088 
0089     QImage img = KisResourceThumbnailCache::instance()->originalImage(storageLocation, storageType, storageIdAsString);
0090     if (!img.isNull()) {
0091         return img;
0092     } else {
0093         const int storageId = query.value("id").toInt();
0094         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(storageId >= 0, img);
0095 
0096         bool result = false;
0097         QSqlQuery thumbQuery;
0098         result = thumbQuery.prepare("SELECT thumbnail FROM storages WHERE id = :id");
0099         if (!result) {
0100             qWarning() << "Failed to prepare query for thumbnail of" << storageId << thumbQuery.lastError();
0101             return img;
0102         }
0103 
0104         thumbQuery.bindValue(":id", storageId);
0105 
0106         result = thumbQuery.exec();
0107 
0108         if (!result) {
0109             qWarning() << "Failed to execute query for thumbnail of" << storageId << thumbQuery.lastError();
0110             return img;
0111         }
0112 
0113         if (!thumbQuery.next()) {
0114             qWarning() << "Failed to find thumbnail of" << storageId;
0115             return img;
0116         }
0117 
0118         QByteArray ba = thumbQuery.value("thumbnail").toByteArray();
0119         QBuffer buf(&ba);
0120         buf.open(QBuffer::ReadOnly);
0121         img.load(&buf, "PNG");
0122         KisResourceThumbnailCache::instance()->insert(storageLocation, storageType, storageIdAsString, img);
0123         return img;
0124     }
0125 }
0126 
0127 QVariant KisStorageModel::data(const QModelIndex &index, int role) const
0128 {
0129     QVariant v;
0130 
0131     if (!index.isValid()) return v;
0132     if (index.row() > rowCount()) return v;
0133     if (index.column() > (int)MetaData) return v;
0134 
0135     if (role == Qt::FontRole) {
0136         return QFont();
0137     }
0138 
0139     QString location = d->storages.at(index.row());
0140 
0141     QSqlQuery query;
0142 
0143     bool r = query.prepare(
0144         "SELECT storages.id as id\n"
0145         ",      storage_types.name as storage_type\n"
0146         ",      location\n"
0147         ",      timestamp\n"
0148         ",      pre_installed\n"
0149         ",      active\n"
0150         "FROM   storages\n"
0151         ",      storage_types\n"
0152         "WHERE  storages.storage_type_id = storage_types.id\n"
0153         "AND    location = :location");
0154 
0155     if (!r) {
0156         qWarning() << "Could not prepare KisStorageModel data query" << query.lastError();
0157         return v;
0158     }
0159 
0160     query.bindValue(":location", location);
0161 
0162     r = query.exec();
0163 
0164     if (!r) {
0165         qWarning() << "Could not execute KisStorageModel data query" << query.lastError() << query.boundValues();
0166         return v;
0167     }
0168 
0169     if (!query.first()) {
0170         qWarning() << "KisStorageModel data query did not return anything";
0171         return v;
0172     }
0173 
0174     if ((role == Qt::DisplayRole || role == Qt::EditRole) && index.column() == Active) {
0175         return query.value("active");
0176     } else {
0177         switch (role) {
0178         case Qt::DisplayRole:
0179         {
0180             switch(index.column()) {
0181             case Id:
0182                 return query.value("id");
0183             case StorageType:
0184                 return query.value("storage_type");
0185             case Location:
0186                 return query.value("location");
0187             case TimeStamp:
0188                 return QDateTime::fromSecsSinceEpoch(query.value("timestamp").value<int>()).toString();
0189             case PreInstalled:
0190                 return query.value("pre_installed");
0191             case Active:
0192                 return query.value("active");
0193             case Thumbnail:
0194             {
0195                 return getThumbnailFromQuery(query);
0196             }
0197             case DisplayName:
0198             {
0199                 QMap<QString, QVariant> r = KisResourceLocator::instance()->metaDataForStorage(query.value("location").toString());
0200                 QVariant name = query.value("location");
0201                 if (r.contains(KisResourceStorage::s_meta_name) && !r[KisResourceStorage::s_meta_name].isNull()) {
0202                     name = r[KisResourceStorage::s_meta_name];
0203                 }
0204                 else if (r.contains(KisResourceStorage::s_meta_title) && !r[KisResourceStorage::s_meta_title].isNull()) {
0205                     name = r[KisResourceStorage::s_meta_title];
0206                 }
0207                 return name;
0208             }
0209             case Qt::UserRole + MetaData:
0210             {
0211                 QMap<QString, QVariant> r = KisResourceLocator::instance()->metaDataForStorage(query.value("location").toString());
0212                 return r;
0213             }
0214             default:
0215                 return v;
0216             }
0217         }
0218         case Qt::CheckStateRole: {
0219             switch (index.column()) {
0220             case PreInstalled:
0221                 if (query.value("pre_installed").toInt() == 0) {
0222                     return Qt::Unchecked;
0223                 } else {
0224                     return Qt::Checked;
0225                 }
0226             case Active:
0227                 if (query.value("active").toInt() == 0) {
0228                     return Qt::Unchecked;
0229                 } else {
0230                     return Qt::Checked;
0231                 }
0232             default:
0233                 return {};
0234             }
0235         }
0236         case Qt::DecorationRole: {
0237             if (index.column() == Thumbnail) {
0238                 return getThumbnailFromQuery(query);
0239             }
0240             return {};
0241         }
0242         case Qt::UserRole + Id:
0243             return query.value("id");
0244         case Qt::UserRole + DisplayName:
0245         {
0246             QMap<QString, QVariant> r = KisResourceLocator::instance()->metaDataForStorage(query.value("location").toString());
0247             QVariant name = query.value("location");
0248             if (r.contains(KisResourceStorage::s_meta_name) && !r[KisResourceStorage::s_meta_name].isNull()) {
0249                 name = r[KisResourceStorage::s_meta_name];
0250             }
0251             else if (r.contains(KisResourceStorage::s_meta_title) && !r[KisResourceStorage::s_meta_title].isNull()) {
0252                 name = r[KisResourceStorage::s_meta_title];
0253             }
0254             return name;
0255         }
0256         case Qt::UserRole + StorageType:
0257             return query.value("storage_type");
0258         case Qt::UserRole + Location:
0259             return query.value("location");
0260         case Qt::UserRole + TimeStamp:
0261             return query.value("timestamp");
0262         case Qt::UserRole + PreInstalled:
0263             return query.value("pre_installed");
0264         case Qt::UserRole + Active:
0265             return query.value("active");
0266         case Qt::UserRole + Thumbnail:
0267             return getThumbnailFromQuery(query);
0268         case Qt::UserRole + MetaData:
0269         {
0270             QMap<QString, QVariant> r = KisResourceLocator::instance()->metaDataForStorage(query.value("location").toString());
0271             return r;
0272         }
0273 
0274         default:
0275             ;
0276         }
0277     }
0278 
0279     return v;
0280 }
0281 
0282 bool KisStorageModel::setData(const QModelIndex &index, const QVariant &value, int role)
0283 {
0284     if (index.isValid()) {
0285 
0286         if (role == Qt::CheckStateRole) {
0287             QSqlQuery query;
0288             bool r = query.prepare("UPDATE storages\n"
0289                                    "SET    active = :active\n"
0290                                    "WHERE  id = :id\n");
0291             query.bindValue(":active", value);
0292             query.bindValue(":id", index.data(Qt::UserRole + Id));
0293 
0294             if (!r) {
0295                 qWarning() << "Could not prepare KisStorageModel update query" << query.lastError();
0296                 return false;
0297             }
0298 
0299             r = query.exec();
0300 
0301             if (!r) {
0302                 qWarning() << "Could not execute KisStorageModel update query" << query.lastError();
0303                 return false;
0304             }
0305 
0306         }
0307 
0308         emit dataChanged(index, index, {role});
0309 
0310         if (value.toBool()) {
0311             emit storageEnabled(data(index, Qt::UserRole + Location).toString());
0312         }
0313         else {
0314             emit storageDisabled(data(index, Qt::UserRole + Location).toString());
0315         }
0316 
0317     }
0318     return true;
0319 }
0320 
0321 Qt::ItemFlags KisStorageModel::flags(const QModelIndex &index) const
0322 {
0323     if (!index.isValid()) {
0324         return Qt::NoItemFlags;
0325     }
0326     return QAbstractTableModel::flags(index) | Qt::ItemIsEditable | Qt::ItemNeverHasChildren;
0327 }
0328 
0329 KisResourceStorageSP KisStorageModel::storageForIndex(const QModelIndex &index) const
0330 {
0331 
0332     if (!index.isValid()) return 0;
0333     if (index.row() > rowCount()) return 0;
0334     if (index.column() > (int)MetaData) return 0;
0335 
0336     QString location = d->storages.at(index.row());
0337 
0338     return KisResourceLocator::instance()->storageByLocation(KisResourceLocator::instance()->makeStorageLocationAbsolute(location));
0339 }
0340 
0341 KisResourceStorageSP KisStorageModel::storageForId(const int storageId) const
0342 {
0343     QSqlQuery query;
0344 
0345     bool r = query.prepare("SELECT location\n"
0346                            "FROM   storages\n"
0347                            "WHERE  storages.id = :storageId");
0348 
0349     if (!r) {
0350         qWarning() << "Could not prepare KisStorageModel data query" << query.lastError();
0351         return 0;
0352     }
0353 
0354     query.bindValue(":storageId", storageId);
0355 
0356     r = query.exec();
0357 
0358     if (!r) {
0359         qWarning() << "Could not execute KisStorageModel data query" << query.lastError() << query.boundValues();
0360         return 0;
0361     }
0362 
0363     if (!query.first()) {
0364         qWarning() << "KisStorageModel data query did not return anything";
0365         return 0;
0366     }
0367 
0368     return KisResourceLocator::instance()->storageByLocation(KisResourceLocator::instance()->makeStorageLocationAbsolute(query.value("location").toString()));
0369 }
0370 
0371 QString findUnusedName(QString location, QString filename)
0372 {
0373     // the Save Incremental Version incrementation in KisViewManager is way too complex for this task
0374     // and in that case there is a specific file to increment, while here we need to find just
0375     // an unused filename
0376     QFileInfo info = QFileInfo(location + "/" + filename);
0377     if (!info.exists()) {
0378         return filename;
0379     }
0380 
0381     QString extension = info.suffix();
0382     QString filenameNoExtension = filename.left(filename.length() - extension.length());
0383 
0384 
0385     QDir dir = QDir(location);
0386     QStringList similarEntries = dir.entryList(QStringList() << filenameNoExtension + "*");
0387 
0388     QList<int> versions;
0389     int maxVersionUsed = -1;
0390     for (int i = 0; i < similarEntries.count(); i++) {
0391         QString entry = similarEntries[i];
0392         //QFileInfo fi = QFileInfo(entry);
0393         if (!entry.endsWith(extension)) {
0394             continue;
0395         }
0396         QString versionStr = entry.right(entry.length() - filenameNoExtension.length()); // strip the common part
0397         versionStr = versionStr.left(versionStr.length() - extension.length());
0398         if (!versionStr.startsWith("_")) {
0399             continue;
0400         }
0401         versionStr = versionStr.right(versionStr.length() - 1); // strip '_'
0402         // now the part left should be a number
0403         bool ok;
0404         int version = versionStr.toInt(&ok);
0405         if (!ok) {
0406             continue;
0407         }
0408         if (version > maxVersionUsed) {
0409             maxVersionUsed = version;
0410         }
0411     }
0412 
0413     int versionToUse = maxVersionUsed > -1 ? maxVersionUsed + 1 : 1;
0414     int versionStringLength = 3;
0415     QString baseNewVersion = QString::number(versionToUse);
0416     while (baseNewVersion.length() < versionStringLength) {
0417         baseNewVersion.prepend("0");
0418     }
0419 
0420     QString newFilename = filenameNoExtension + "_" + QString::number(versionToUse) + extension;
0421     bool success = !QFileInfo(location + "/" + newFilename).exists();
0422 
0423     if (!success) {
0424         qCritical() << "The new filename for the bundle does exist." << newFilename;
0425     }
0426 
0427     return newFilename;
0428 
0429 }
0430 
0431 bool KisStorageModel::importStorage(QString filename, StorageImportOption importOption) const
0432 {
0433     // 1. Copy the bundle/storage to the resource folder
0434     QFileInfo oldFileInfo(filename);
0435     QString newDir = KoResourcePaths::getAppDataLocation();
0436     QString newName = oldFileInfo.fileName();
0437     QString newLocation = newDir + '/' + newName;
0438 
0439     QFileInfo newFileInfo(newLocation);
0440     if (newFileInfo.exists()) {
0441         if (importOption == Overwrite) {
0442             //QFile::remove(newLocation);
0443             return false;
0444         } else if (importOption == Rename) {
0445             newName = findUnusedName(newDir, newName);
0446             newLocation = newDir + '/' + newName;
0447             newFileInfo = QFileInfo(newLocation);
0448         } else { // importOption == None
0449             return false;
0450         }
0451     }
0452     QFile::copy(filename, newLocation);
0453 
0454     // 2. Add the bundle as a storage/update database
0455     KisResourceStorageSP storage = QSharedPointer<KisResourceStorage>::create(newLocation);
0456     KIS_ASSERT(!storage.isNull());
0457     if (storage.isNull()) { return false; }
0458     if (!KisResourceLocator::instance()->addStorage(newLocation, storage)) {
0459         qWarning() << "Could not add bundle to the storages" << newLocation;
0460         return false;
0461     }
0462     return true;
0463 }
0464 
0465 QVariant KisStorageModel::headerData(int section, Qt::Orientation orientation, int role) const
0466 {
0467     QVariant v = QVariant();
0468     if (role != Qt::DisplayRole) {
0469         return v;
0470     }
0471     if (orientation == Qt::Horizontal) {
0472         switch(section) {
0473         case Id:
0474             return i18n("Id");
0475         case StorageType:
0476             return i18n("Type");
0477         case Location:
0478             return i18n("Location");
0479         case TimeStamp:
0480             return i18n("Creation Date");
0481         case PreInstalled:
0482             return i18n("Preinstalled");
0483         case Active:
0484             return i18n("Active");
0485         case Thumbnail:
0486             return i18n("Thumbnail");
0487         case DisplayName:
0488             return i18n("Name");
0489         default:
0490             v = QString::number(section);
0491         }
0492         return v;
0493     }
0494     return QAbstractTableModel::headerData(section, orientation, role);
0495 }
0496 
0497 void KisStorageModel::addStorage(const QString &location)
0498 {
0499     beginInsertRows(QModelIndex(), rowCount(), rowCount());
0500     d->storages.append(location);
0501     endInsertRows();
0502 }
0503 
0504 void KisStorageModel::removeStorage(const QString &location)
0505 {
0506     int row = d->storages.indexOf(QFileInfo(location).fileName());
0507     beginRemoveRows(QModelIndex(), row, row);
0508     d->storages.removeAt(row);
0509     endRemoveRows();
0510 }
0511 
0512