File indexing completed on 2024-12-01 12:36:42
0001 // -*- c++ -*- 0002 /* 0003 This file is part of the KDE libraries 0004 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org> 0006 SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "previewjob.h" 0012 #include "kio_widgets_debug.h" 0013 0014 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) 0015 #define WITH_SHM 1 0016 #else 0017 #define WITH_SHM 0 0018 #endif 0019 0020 #if WITH_SHM 0021 #include <sys/ipc.h> 0022 #include <sys/shm.h> 0023 #endif 0024 0025 #include <limits> 0026 #include <set> 0027 0028 #include <QDir> 0029 #include <QFile> 0030 #include <QImage> 0031 #include <QPixmap> 0032 #include <QRegularExpression> 0033 #include <QSaveFile> 0034 #include <QTemporaryFile> 0035 #include <QTimer> 0036 0037 #include <QCryptographicHash> 0038 0039 #include <KConfigGroup> 0040 #include <KMountPoint> 0041 #include <KPluginInfo> 0042 #include <KPluginMetaData> 0043 #include <KService> 0044 #include <KServiceTypeTrader> 0045 #include <KSharedConfig> 0046 #include <QMimeDatabase> 0047 #include <QStandardPaths> 0048 #include <Solid/Device> 0049 #include <Solid/StorageAccess> 0050 #include <kprotocolinfo.h> 0051 0052 #include <algorithm> 0053 #include <cmath> 0054 0055 #include "job_p.h" 0056 0057 namespace 0058 { 0059 static int s_defaultDevicePixelRatio = 1; 0060 } 0061 0062 namespace KIO 0063 { 0064 struct PreviewItem; 0065 } 0066 using namespace KIO; 0067 0068 struct KIO::PreviewItem { 0069 KFileItem item; 0070 KPluginMetaData plugin; 0071 }; 0072 0073 class KIO::PreviewJobPrivate : public KIO::JobPrivate 0074 { 0075 public: 0076 PreviewJobPrivate(const KFileItemList &items, const QSize &size) 0077 : initialItems(items) 0078 , width(size.width()) 0079 , height(size.height()) 0080 , cacheSize(0) 0081 , bScale(true) 0082 , bSave(true) 0083 , ignoreMaximumSize(false) 0084 , sequenceIndex(0) 0085 , succeeded(false) 0086 , maximumLocalSize(0) 0087 , maximumRemoteSize(0) 0088 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0089 , iconSize(0) 0090 , iconAlpha(70) 0091 #endif 0092 , shmid(-1) 0093 , shmaddr(nullptr) 0094 { 0095 // https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY 0096 thumbRoot = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/"); 0097 } 0098 0099 enum { 0100 STATE_STATORIG, // if the thumbnail exists 0101 STATE_GETORIG, // if we create it 0102 STATE_CREATETHUMB, // thumbnail:/ worker 0103 STATE_DEVICE_INFO, // additional state check to get needed device ids 0104 } state; 0105 0106 KFileItemList initialItems; 0107 QStringList enabledPlugins; 0108 // Some plugins support remote URLs, <protocol, mimetypes> 0109 QHash<QString, QStringList> m_remoteProtocolPlugins; 0110 // Our todo list :) 0111 // We remove the first item at every step, so use std::list 0112 std::list<PreviewItem> items; 0113 // The current item 0114 PreviewItem currentItem; 0115 // The modification time of that URL 0116 QDateTime tOrig; 0117 // Path to thumbnail cache for the current size 0118 QString thumbPath; 0119 // Original URL of current item in RFC2396 format 0120 // (file:///path/to/a%20file instead of file:/path/to/a file) 0121 QByteArray origName; 0122 // Thumbnail file name for current item 0123 QString thumbName; 0124 // Size of thumbnail 0125 int width; 0126 int height; 0127 // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled) 0128 short cacheSize; 0129 // Whether the thumbnail should be scaled 0130 bool bScale; 0131 // Whether we should save the thumbnail 0132 bool bSave; 0133 bool ignoreMaximumSize; 0134 int sequenceIndex; 0135 bool succeeded; 0136 // If the file to create a thumb for was a temp file, this is its name 0137 QString tempName; 0138 KIO::filesize_t maximumLocalSize; 0139 KIO::filesize_t maximumRemoteSize; 0140 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0141 // the size for the icon overlay 0142 int iconSize; 0143 // the transparency of the blended MIME type icon 0144 int iconAlpha; 0145 #endif 0146 // Shared memory segment Id. The segment is allocated to a size 0147 // of extent x extent x 4 (32 bit image) on first need. 0148 int shmid; 0149 // And the data area 0150 uchar *shmaddr; 0151 // Size of the shm segment 0152 size_t shmsize; 0153 // Root of thumbnail cache 0154 QString thumbRoot; 0155 // Metadata returned from the KIO thumbnail worker 0156 QMap<QString, QString> thumbnailWorkerMetaData; 0157 int devicePixelRatio = s_defaultDevicePixelRatio; 0158 static const int idUnknown = -1; 0159 // Id of a device storing currently processed file 0160 int currentDeviceId = 0; 0161 // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on. 0162 QMap<QString, int> deviceIdMap; 0163 enum CachePolicy { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown; 0164 0165 void getOrCreateThumbnail(); 0166 bool statResultThumbnail(); 0167 void createThumbnail(const QString &); 0168 void cleanupTempFile(); 0169 void determineNextFile(); 0170 void emitPreview(const QImage &thumb); 0171 0172 void startPreview(); 0173 void slotThumbData(KIO::Job *, const QByteArray &); 0174 // Checks if thumbnail is on encrypted partition different than thumbRoot 0175 CachePolicy canBeCached(const QString &path); 0176 int getDeviceId(const QString &path); 0177 0178 Q_DECLARE_PUBLIC(PreviewJob) 0179 0180 static QVector<KPluginMetaData> loadAvailablePlugins() 0181 { 0182 static QVector<KPluginMetaData> jsonMetaDataPlugins; 0183 if (!jsonMetaDataPlugins.isEmpty()) { 0184 return jsonMetaDataPlugins; 0185 } 0186 jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/thumbcreator")); 0187 std::set<QString> pluginIds; 0188 for (const KPluginMetaData &data : std::as_const(jsonMetaDataPlugins)) { 0189 pluginIds.insert(data.pluginId()); 0190 } 0191 #if KSERVICE_ENABLE_DEPRECATED_SINCE(5, 88) 0192 QT_WARNING_PUSH 0193 QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations") 0194 QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") 0195 const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator")); 0196 for (const auto &plugin : plugins) { 0197 if (KPluginInfo info(plugin); info.isValid()) { 0198 if (auto [it, inserted] = pluginIds.insert(plugin->desktopEntryName()); inserted) { 0199 jsonMetaDataPlugins << info.toMetaData(); 0200 } 0201 } else { 0202 // Hack for directory thumbnailer: It has a hardcoded plugin id in the KIO worker and not any C++ plugin 0203 // Consequently we just use the base name as the plugin file for our KPluginMetaData object 0204 const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + plugin->entryPath()); 0205 KPluginMetaData tmpData = KPluginMetaData::fromDesktopFile(path); 0206 jsonMetaDataPlugins << KPluginMetaData(tmpData.rawData(), QFileInfo(path).baseName(), path); 0207 } 0208 } 0209 QT_WARNING_POP 0210 #else 0211 #pragma message("TODO: directory thumbnailer needs a non-desktop file solution ") 0212 #endif 0213 return jsonMetaDataPlugins; 0214 } 0215 }; 0216 0217 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86) 0218 void PreviewJob::setDefaultDevicePixelRatio(int defaultDevicePixelRatio) 0219 { 0220 s_defaultDevicePixelRatio = defaultDevicePixelRatio; 0221 } 0222 #endif 0223 0224 void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio) 0225 { 0226 s_defaultDevicePixelRatio = std::ceil(defaultDevicePixelRatio); 0227 } 0228 0229 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7) 0230 PreviewJob::PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) 0231 : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width))) 0232 { 0233 Q_D(PreviewJob); 0234 d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins(); 0235 d->iconSize = iconSize; 0236 d->iconAlpha = iconAlpha; 0237 d->bScale = scale; 0238 d->bSave = save && scale; 0239 0240 // Return to event loop first, determineNextFile() might delete this; 0241 QTimer::singleShot(0, this, [d]() { 0242 d->startPreview(); 0243 }); 0244 } 0245 #endif 0246 0247 PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) 0248 : KIO::Job(*new PreviewJobPrivate(items, size)) 0249 { 0250 Q_D(PreviewJob); 0251 0252 if (enabledPlugins) { 0253 d->enabledPlugins = *enabledPlugins; 0254 } else { 0255 const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); 0256 d->enabledPlugins = 0257 globalConfig.readEntry("Plugins", 0258 QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")}); 0259 } 0260 0261 // Return to event loop first, determineNextFile() might delete this; 0262 QTimer::singleShot(0, this, [d]() { 0263 d->startPreview(); 0264 }); 0265 } 0266 0267 PreviewJob::~PreviewJob() 0268 { 0269 #if WITH_SHM 0270 Q_D(PreviewJob); 0271 if (d->shmaddr) { 0272 shmdt((char *)d->shmaddr); 0273 shmctl(d->shmid, IPC_RMID, nullptr); 0274 } 0275 #endif 0276 } 0277 0278 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0279 void PreviewJob::setOverlayIconSize(int size) 0280 { 0281 Q_D(PreviewJob); 0282 d->iconSize = size; 0283 } 0284 #endif 0285 0286 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0287 int PreviewJob::overlayIconSize() const 0288 { 0289 Q_D(const PreviewJob); 0290 return d->iconSize; 0291 } 0292 #endif 0293 0294 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0295 void PreviewJob::setOverlayIconAlpha(int alpha) 0296 { 0297 Q_D(PreviewJob); 0298 d->iconAlpha = qBound(0, alpha, 255); 0299 } 0300 #endif 0301 0302 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0303 int PreviewJob::overlayIconAlpha() const 0304 { 0305 Q_D(const PreviewJob); 0306 return d->iconAlpha; 0307 } 0308 #endif 0309 0310 void PreviewJob::setScaleType(ScaleType type) 0311 { 0312 Q_D(PreviewJob); 0313 switch (type) { 0314 case Unscaled: 0315 d->bScale = false; 0316 d->bSave = false; 0317 break; 0318 case Scaled: 0319 d->bScale = true; 0320 d->bSave = false; 0321 break; 0322 case ScaledAndCached: 0323 d->bScale = true; 0324 d->bSave = true; 0325 break; 0326 default: 0327 break; 0328 } 0329 } 0330 0331 PreviewJob::ScaleType PreviewJob::scaleType() const 0332 { 0333 Q_D(const PreviewJob); 0334 if (d->bScale) { 0335 return d->bSave ? ScaledAndCached : Scaled; 0336 } 0337 return Unscaled; 0338 } 0339 0340 void PreviewJobPrivate::startPreview() 0341 { 0342 Q_Q(PreviewJob); 0343 // Load the list of plugins to determine which MIME types are supported 0344 const QVector<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins(); 0345 QMap<QString, KPluginMetaData> mimeMap; 0346 QHash<QString, QHash<QString, KPluginMetaData>> protocolMap; 0347 0348 for (const KPluginMetaData &plugin : plugins) { 0349 QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols"), QStringList()); 0350 const QString p = plugin.value(QStringLiteral("X-KDE-Protocol")); 0351 if (!p.isEmpty()) { 0352 protocols.append(p); 0353 } 0354 for (const QString &protocol : std::as_const(protocols)) { 0355 // Add supported MIME type for this protocol 0356 QStringList &_ms = m_remoteProtocolPlugins[protocol]; 0357 const auto mimeTypes = plugin.mimeTypes(); 0358 for (const QString &_m : mimeTypes) { 0359 protocolMap[protocol].insert(_m, plugin); 0360 if (!_ms.contains(_m)) { 0361 _ms.append(_m); 0362 } 0363 } 0364 } 0365 if (enabledPlugins.contains(plugin.pluginId())) { 0366 const auto mimeTypes = plugin.mimeTypes(); 0367 for (const QString &mimeType : mimeTypes) { 0368 mimeMap.insert(mimeType, plugin); 0369 } 0370 } 0371 } 0372 0373 // Look for images and store the items in our todo list :) 0374 bool bNeedCache = false; 0375 for (const auto &fileItem : std::as_const(initialItems)) { 0376 PreviewItem item; 0377 item.item = fileItem; 0378 0379 const QString mimeType = item.item.mimetype(); 0380 KPluginMetaData plugin; 0381 0382 // look for protocol-specific thumbnail plugins first 0383 auto it = protocolMap.constFind(item.item.url().scheme()); 0384 if (it != protocolMap.constEnd()) { 0385 plugin = it.value().value(mimeType); 0386 } 0387 0388 if (!plugin.isValid()) { 0389 auto pluginIt = mimeMap.constFind(mimeType); 0390 if (pluginIt == mimeMap.constEnd()) { 0391 // check MIME type inheritance, resolve aliases 0392 QMimeDatabase db; 0393 const QMimeType mimeInfo = db.mimeTypeForName(mimeType); 0394 if (mimeInfo.isValid()) { 0395 const QStringList parentMimeTypes = mimeInfo.allAncestors(); 0396 for (const QString &parentMimeType : parentMimeTypes) { 0397 pluginIt = mimeMap.constFind(parentMimeType); 0398 if (pluginIt != mimeMap.constEnd()) { 0399 break; 0400 } 0401 } 0402 } 0403 0404 if (pluginIt == mimeMap.constEnd()) { 0405 // Check the wildcards last, see BUG 453480 0406 QString groupMimeType = mimeType; 0407 const int slashIdx = groupMimeType.indexOf(QLatin1Char('/')); 0408 if (slashIdx != -1) { 0409 // Replace everything after '/' with '*' 0410 groupMimeType.truncate(slashIdx + 1); 0411 groupMimeType += QLatin1Char('*'); 0412 } 0413 pluginIt = mimeMap.constFind(groupMimeType); 0414 } 0415 } 0416 0417 if (pluginIt != mimeMap.constEnd()) { 0418 plugin = *pluginIt; 0419 } 0420 } 0421 0422 if (plugin.isValid()) { 0423 item.plugin = plugin; 0424 items.push_back(item); 0425 if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) { 0426 const QUrl url = fileItem.url(); 0427 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) { 0428 bNeedCache = true; 0429 } 0430 } 0431 } else { 0432 Q_EMIT q->failed(fileItem); 0433 } 0434 } 0435 0436 KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings"); 0437 maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max()); 0438 maximumRemoteSize = cg.readEntry<KIO::filesize_t>("MaximumRemoteSize", 0); 0439 0440 if (bNeedCache) { 0441 const int longer = std::max(width, height); 0442 if (longer <= 128) { 0443 cacheSize = 128; 0444 } else if (longer <= 256) { 0445 cacheSize = 256; 0446 } else if (longer <= 512) { 0447 cacheSize = 512; 0448 } else { 0449 cacheSize = 1024; 0450 } 0451 0452 struct CachePool { 0453 QString path; 0454 int minSize; 0455 }; 0456 0457 const static auto pools = { 0458 CachePool{QStringLiteral("/normal/"), 128}, 0459 CachePool{QStringLiteral("/large/"), 256}, 0460 CachePool{QStringLiteral("/x-large/"), 512}, 0461 CachePool{QStringLiteral("/xx-large/"), 1024}, 0462 }; 0463 0464 QString thumbDir; 0465 int wants = devicePixelRatio * cacheSize; 0466 for (const auto &p : pools) { 0467 if (p.minSize < wants) { 0468 continue; 0469 } else { 0470 thumbDir = p.path; 0471 break; 0472 } 0473 } 0474 thumbPath = thumbRoot + thumbDir; 0475 0476 if (!QDir(thumbPath).exists()) { 0477 if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions) 0478 QFile f(thumbPath); 0479 f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700 0480 } 0481 } 0482 } else { 0483 bSave = false; 0484 } 0485 0486 initialItems.clear(); 0487 determineNextFile(); 0488 } 0489 0490 void PreviewJob::removeItem(const QUrl &url) 0491 { 0492 Q_D(PreviewJob); 0493 0494 auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) { 0495 return url == pItem.item.url(); 0496 }); 0497 if (it != d->items.cend()) { 0498 d->items.erase(it); 0499 } 0500 0501 if (d->currentItem.item.url() == url) { 0502 KJob *job = subjobs().first(); 0503 job->kill(); 0504 removeSubjob(job); 0505 d->determineNextFile(); 0506 } 0507 } 0508 0509 void KIO::PreviewJob::setSequenceIndex(int index) 0510 { 0511 d_func()->sequenceIndex = index; 0512 } 0513 0514 int KIO::PreviewJob::sequenceIndex() const 0515 { 0516 return d_func()->sequenceIndex; 0517 } 0518 0519 float KIO::PreviewJob::sequenceIndexWraparoundPoint() const 0520 { 0521 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat(); 0522 } 0523 0524 bool KIO::PreviewJob::handlesSequences() const 0525 { 0526 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1"); 0527 } 0528 0529 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86) 0530 void KIO::PreviewJob::setDevicePixelRatio(int dpr) 0531 { 0532 d_func()->devicePixelRatio = dpr; 0533 } 0534 #endif 0535 0536 void KIO::PreviewJob::setDevicePixelRatio(qreal dpr) 0537 { 0538 d_func()->devicePixelRatio = std::ceil(dpr); 0539 } 0540 0541 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) 0542 { 0543 d_func()->ignoreMaximumSize = ignoreSize; 0544 } 0545 0546 void PreviewJobPrivate::cleanupTempFile() 0547 { 0548 if (!tempName.isEmpty()) { 0549 Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink()); 0550 QFile::remove(tempName); 0551 tempName.clear(); 0552 } 0553 } 0554 0555 void PreviewJobPrivate::determineNextFile() 0556 { 0557 Q_Q(PreviewJob); 0558 if (!currentItem.item.isNull()) { 0559 if (!succeeded) { 0560 Q_EMIT q->failed(currentItem.item); 0561 } 0562 } 0563 // No more items ? 0564 if (items.empty()) { 0565 q->emitResult(); 0566 return; 0567 } else { 0568 // First, stat the orig file 0569 state = PreviewJobPrivate::STATE_STATORIG; 0570 currentItem = items.front(); 0571 items.pop_front(); 0572 succeeded = false; 0573 KIO::Job *job = KIO::statDetails(currentItem.item.url(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo); 0574 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1")); 0575 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); 0576 q->addSubjob(job); 0577 } 0578 } 0579 0580 void PreviewJob::slotResult(KJob *job) 0581 { 0582 Q_D(PreviewJob); 0583 0584 removeSubjob(job); 0585 Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ... 0586 switch (d->state) { 0587 case PreviewJobPrivate::STATE_STATORIG: { 0588 if (job->error()) { // that's no good news... 0589 // Drop this one and move on to the next one 0590 d->determineNextFile(); 0591 return; 0592 } 0593 const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult(); 0594 d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0); 0595 d->tOrig = QDateTime::fromSecsSinceEpoch(statResult.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0)); 0596 0597 bool skipCurrentItem = false; 0598 const KIO::filesize_t size = (KIO::filesize_t)statResult.numberValue(KIO::UDSEntry::UDS_SIZE, 0); 0599 const QUrl itemUrl = d->currentItem.item.mostLocalUrl(); 0600 0601 if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) { 0602 skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false); 0603 } else { 0604 // For remote items the "IgnoreMaximumSize" plugin property is not respected 0605 skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize; 0606 0607 // Remote directories are not supported, don't try to do a file_copy on them 0608 if (!skipCurrentItem) { 0609 // TODO update item.mimeType from the UDS entry, in case it wasn't set initially 0610 // But we don't use the MIME type anymore, we just use isDir(). 0611 if (d->currentItem.item.isDir()) { 0612 skipCurrentItem = true; 0613 } 0614 } 0615 } 0616 if (skipCurrentItem) { 0617 d->determineNextFile(); 0618 return; 0619 } 0620 0621 bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false); 0622 if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) { 0623 // This preview will not be cached, no need to look for a saved thumbnail 0624 // Just create it, and be done 0625 d->getOrCreateThumbnail(); 0626 return; 0627 } 0628 0629 if (d->statResultThumbnail()) { 0630 return; 0631 } 0632 0633 d->getOrCreateThumbnail(); 0634 return; 0635 } 0636 case PreviewJobPrivate::STATE_DEVICE_INFO: { 0637 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job); 0638 int id; 0639 QString path = statJob->url().toLocalFile(); 0640 if (job->error()) { 0641 // We set id to 0 to know we tried getting it 0642 qCWarning(KIO_WIDGETS) << "Cannot read information about filesystem under path" << path; 0643 id = 0; 0644 } else { 0645 id = statJob->statResult().numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0); 0646 } 0647 d->deviceIdMap[path] = id; 0648 d->createThumbnail(d->currentItem.item.localPath()); 0649 return; 0650 } 0651 case PreviewJobPrivate::STATE_GETORIG: { 0652 if (job->error()) { 0653 d->cleanupTempFile(); 0654 d->determineNextFile(); 0655 return; 0656 } 0657 0658 d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile()); 0659 return; 0660 } 0661 case PreviewJobPrivate::STATE_CREATETHUMB: { 0662 d->cleanupTempFile(); 0663 d->determineNextFile(); 0664 return; 0665 } 0666 } 0667 } 0668 0669 bool PreviewJobPrivate::statResultThumbnail() 0670 { 0671 if (thumbPath.isEmpty()) { 0672 return false; 0673 } 0674 0675 bool isLocal; 0676 const QUrl url = currentItem.item.mostLocalUrl(&isLocal); 0677 if (isLocal) { 0678 const QFileInfo localFile(url.toLocalFile()); 0679 const QString canonicalPath = localFile.canonicalFilePath(); 0680 origName = QUrl::fromLocalFile(canonicalPath).toEncoded(QUrl::RemovePassword | QUrl::FullyEncoded); 0681 if (origName.isEmpty()) { 0682 qCWarning(KIO_WIDGETS) << "Failed to convert" << url << "to canonical path"; 0683 return false; 0684 } 0685 } else { 0686 // Don't include the password if any 0687 origName = url.toEncoded(QUrl::RemovePassword); 0688 } 0689 0690 QCryptographicHash md5(QCryptographicHash::Md5); 0691 md5.addData(origName); 0692 thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png"); 0693 0694 QImage thumb; 0695 QFile thumbFile(thumbPath + thumbName); 0696 if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) { 0697 return false; 0698 } 0699 0700 if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName) 0701 || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) { 0702 return false; 0703 } 0704 0705 const QString origSize = thumb.text(QStringLiteral("Thumb::Size")); 0706 if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) { 0707 // Thumb::Size is not required, but if it is set it should match 0708 return false; 0709 } 0710 0711 // The DPR of the loaded thumbnail is unspecified (and typically irrelevant). 0712 // When a thumbnail is DPR-invariant, use the DPR passed in the request. 0713 thumb.setDevicePixelRatio(devicePixelRatio); 0714 0715 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion")); 0716 0717 if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) { 0718 // Check if the version matches 0719 // The software string should read "KDE Thumbnail Generator pluginName (vX)" 0720 QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed(); 0721 if (softwareString.isEmpty()) { 0722 // The thumbnail has been created with an older version, recreating 0723 return false; 0724 } 0725 int versionIndex = softwareString.lastIndexOf(QLatin1String("(v")); 0726 if (versionIndex < 0) { 0727 return false; 0728 } 0729 0730 QString cachedVersion = softwareString.remove(0, versionIndex + 2); 0731 cachedVersion.chop(1); 0732 uint thumbnailerMajor = thumbnailerVersion.toInt(); 0733 uint cachedMajor = cachedVersion.toInt(); 0734 if (thumbnailerMajor > cachedMajor) { 0735 return false; 0736 } 0737 } 0738 0739 // Found it, use it 0740 emitPreview(thumb); 0741 succeeded = true; 0742 determineNextFile(); 0743 return true; 0744 } 0745 0746 void PreviewJobPrivate::getOrCreateThumbnail() 0747 { 0748 Q_Q(PreviewJob); 0749 // We still need to load the orig file ! (This is getting tedious) :) 0750 const KFileItem &item = currentItem.item; 0751 const QString localPath = item.localPath(); 0752 if (!localPath.isEmpty()) { 0753 createThumbnail(localPath); 0754 } else { 0755 const QUrl fileUrl = item.url(); 0756 // heuristics for remote URL support 0757 bool supportsProtocol = false; 0758 if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) { 0759 // There's a plugin supporting this protocol and MIME type 0760 supportsProtocol = true; 0761 } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) { 0762 // Assume KIO understands any URL, ThumbCreator workers who have 0763 // X-KDE-Protocols=KIO will get fed the remote URL directly. 0764 supportsProtocol = true; 0765 } 0766 0767 if (supportsProtocol) { 0768 createThumbnail(fileUrl.toString()); 0769 return; 0770 } 0771 if (item.isDir()) { 0772 // Skip remote dirs (bug 208625) 0773 cleanupTempFile(); 0774 determineNextFile(); 0775 return; 0776 } 0777 // No plugin support access to this remote content, copy the file 0778 // to the local machine, then create the thumbnail 0779 state = PreviewJobPrivate::STATE_GETORIG; 0780 QTemporaryFile localFile; 0781 localFile.setAutoRemove(false); 0782 localFile.open(); 0783 tempName = localFile.fileName(); 0784 const QUrl currentURL = item.mostLocalUrl(); 0785 KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */); 0786 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1")); 0787 q->addSubjob(job); 0788 } 0789 } 0790 0791 PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path) 0792 { 0793 // If checked file is directory on a different filesystem than its parent, we need to check it separately 0794 int separatorIndex = path.lastIndexOf(QLatin1Char('/')); 0795 // special case for root folders 0796 const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex); 0797 0798 int parentId = getDeviceId(parentDirPath); 0799 if (parentId == idUnknown) { 0800 return CachePolicy::Unknown; 0801 } 0802 0803 bool isDifferentSystem = !parentId || parentId != currentDeviceId; 0804 if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) { 0805 return currentDeviceCachePolicy; 0806 } 0807 int checkedId; 0808 QString checkedPath; 0809 if (isDifferentSystem) { 0810 checkedId = currentDeviceId; 0811 checkedPath = path; 0812 } else { 0813 checkedId = getDeviceId(parentDirPath); 0814 checkedPath = parentDirPath; 0815 if (checkedId == idUnknown) { 0816 return CachePolicy::Unknown; 0817 } 0818 } 0819 // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot 0820 int thumbRootId = getDeviceId(thumbRoot); 0821 if (thumbRootId == idUnknown) { 0822 return CachePolicy::Unknown; 0823 } 0824 bool shouldAllow = checkedId && checkedId == thumbRootId; 0825 if (!shouldAllow) { 0826 Solid::Device device = Solid::Device::storageAccessFromPath(checkedPath); 0827 if (device.isValid()) { 0828 // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location. 0829 // Or, if the checked device is unencrypted, allow thumbnailing. 0830 if (device.as<Solid::StorageAccess>()->isEncrypted()) { 0831 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot); 0832 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted(); 0833 } else { 0834 shouldAllow = true; 0835 } 0836 } 0837 } 0838 if (!isDifferentSystem) { 0839 currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent; 0840 } 0841 return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent; 0842 } 0843 0844 int PreviewJobPrivate::getDeviceId(const QString &path) 0845 { 0846 Q_Q(PreviewJob); 0847 auto iter = deviceIdMap.find(path); 0848 if (iter != deviceIdMap.end()) { 0849 return iter.value(); 0850 } 0851 QUrl url = QUrl::fromLocalFile(path); 0852 if (!url.isValid()) { 0853 qCWarning(KIO_WIDGETS) << "Could not get device id for file preview, Invalid url" << path; 0854 return 0; 0855 } 0856 state = PreviewJobPrivate::STATE_DEVICE_INFO; 0857 KIO::Job *job = KIO::statDetails(url, StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo); 0858 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); 0859 q->addSubjob(job); 0860 0861 return idUnknown; 0862 } 0863 0864 void PreviewJobPrivate::createThumbnail(const QString &pixPath) 0865 { 0866 Q_Q(PreviewJob); 0867 state = PreviewJobPrivate::STATE_CREATETHUMB; 0868 QUrl thumbURL; 0869 thumbURL.setScheme(QStringLiteral("thumbnail")); 0870 thumbURL.setPath(pixPath); 0871 0872 bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex; 0873 0874 bool isRemoteProtocol = currentItem.item.localPath().isEmpty(); 0875 CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath); 0876 0877 if (cachePolicy == CachePolicy::Unknown) { 0878 // If Unknown is returned, creating thumbnail should be called again by slotResult 0879 return; 0880 } 0881 0882 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo); 0883 q->addSubjob(job); 0884 q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) { 0885 slotThumbData(job, data); 0886 }); 0887 0888 int thumb_width = width; 0889 int thumb_height = height; 0890 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0891 int thumb_iconSize = iconSize; 0892 #endif 0893 if (save) { 0894 thumb_width = thumb_height = cacheSize; 0895 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0896 thumb_iconSize = 64; 0897 #endif 0898 } 0899 0900 job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype()); 0901 job->addMetaData(QStringLiteral("width"), QString::number(thumb_width)); 0902 job->addMetaData(QStringLiteral("height"), QString::number(thumb_height)); 0903 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102) 0904 job->addMetaData(QStringLiteral("iconSize"), QString::number(thumb_iconSize)); 0905 job->addMetaData(QStringLiteral("iconAlpha"), QString::number(iconAlpha)); 0906 #endif 0907 job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName()); 0908 job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(','))); 0909 job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio)); 0910 job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow)); 0911 if (sequenceIndex) { 0912 job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex)); 0913 } 0914 0915 #if WITH_SHM 0916 size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4; 0917 if (shmid == -1 || shmsize < requiredSize) { 0918 if (shmaddr) { 0919 // clean previous shared memory segment 0920 shmdt((char *)shmaddr); 0921 shmaddr = nullptr; 0922 shmctl(shmid, IPC_RMID, nullptr); 0923 shmid = -1; 0924 } 0925 if (requiredSize > 0) { 0926 shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600); 0927 if (shmid != -1) { 0928 shmsize = requiredSize; 0929 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY)); 0930 if (shmaddr == (uchar *)-1) { 0931 shmctl(shmid, IPC_RMID, nullptr); 0932 shmaddr = nullptr; 0933 shmid = -1; 0934 } 0935 } 0936 } 0937 } 0938 if (shmid != -1) { 0939 job->addMetaData(QStringLiteral("shmid"), QString::number(shmid)); 0940 } 0941 #endif 0942 } 0943 0944 void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data) 0945 { 0946 thumbnailWorkerMetaData = job->metaData(); 0947 /* clang-format off */ 0948 const bool save = bSave 0949 && !sequenceIndex 0950 && currentDeviceCachePolicy == CachePolicy::Allow 0951 && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) 0952 && (!currentItem.item.url().isLocalFile() 0953 || !currentItem.item.url().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)); 0954 /* clang-format on */ 0955 0956 QImage thumb; 0957 #if WITH_SHM 0958 if (shmaddr) { 0959 // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp 0960 QDataStream str(data); 0961 int width; 0962 int height; 0963 quint8 iFormat; 0964 int imgDevicePixelRatio = 1; 0965 // TODO KF6: add a version number as first parameter 0966 str >> width >> height >> iFormat; 0967 if (iFormat & 0x80) { 0968 // HACK to deduce if imgDevicePixelRatio is present 0969 iFormat &= 0x7f; 0970 str >> imgDevicePixelRatio; 0971 } 0972 QImage::Format format = static_cast<QImage::Format>(iFormat); 0973 thumb = QImage(shmaddr, width, height, format).copy(); 0974 thumb.setDevicePixelRatio(imgDevicePixelRatio); 0975 } else { 0976 thumb.loadFromData(data); 0977 } 0978 #else 0979 thumb.loadFromData(data); 0980 #endif 0981 0982 if (thumb.isNull()) { 0983 QDataStream s(data); 0984 s >> thumb; 0985 } 0986 0987 if (save) { 0988 thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName)); 0989 thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch())); 0990 thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size())); 0991 thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype()); 0992 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion")); 0993 QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name(); 0994 if (!thumbnailerVersion.isEmpty()) { 0995 signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')')); 0996 } 0997 thumb.setText(QStringLiteral("Software"), signature); 0998 QSaveFile saveFile(thumbPath + thumbName); 0999 if (saveFile.open(QIODevice::WriteOnly)) { 1000 if (thumb.save(&saveFile, "PNG")) { 1001 saveFile.commit(); 1002 } 1003 } 1004 } 1005 emitPreview(thumb); 1006 succeeded = true; 1007 } 1008 1009 void PreviewJobPrivate::emitPreview(const QImage &thumb) 1010 { 1011 Q_Q(PreviewJob); 1012 QPixmap pix; 1013 const qreal ratio = thumb.devicePixelRatio(); 1014 if (thumb.width() > width * ratio || thumb.height() > height * ratio) { 1015 pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation)); 1016 } else { 1017 pix = QPixmap::fromImage(thumb); 1018 } 1019 pix.setDevicePixelRatio(ratio); 1020 Q_EMIT q->gotPreview(currentItem.item, pix); 1021 } 1022 1023 QVector<KPluginMetaData> PreviewJob::availableThumbnailerPlugins() 1024 { 1025 return PreviewJobPrivate::loadAvailablePlugins(); 1026 } 1027 1028 QStringList PreviewJob::availablePlugins() 1029 { 1030 QStringList result; 1031 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins(); 1032 for (const KPluginMetaData &plugin : plugins) { 1033 result << plugin.pluginId(); 1034 } 1035 return result; 1036 } 1037 1038 QStringList PreviewJob::defaultPlugins() 1039 { 1040 const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail"); 1041 1042 QStringList defaultPlugins = availablePlugins(); 1043 for (const QString &plugin : blacklist) { 1044 defaultPlugins.removeAll(plugin); 1045 } 1046 1047 return defaultPlugins; 1048 } 1049 1050 QStringList PreviewJob::supportedMimeTypes() 1051 { 1052 QStringList result; 1053 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins(); 1054 for (const KPluginMetaData &plugin : plugins) { 1055 result += plugin.mimeTypes(); 1056 } 1057 return result; 1058 } 1059 1060 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7) 1061 PreviewJob * 1062 KIO::filePreview(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) 1063 { 1064 return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); 1065 } 1066 #endif 1067 1068 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7) 1069 PreviewJob * 1070 KIO::filePreview(const QList<QUrl> &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins) 1071 { 1072 KFileItemList fileItems; 1073 fileItems.reserve(items.size()); 1074 for (const QUrl &url : items) { 1075 Q_ASSERT(url.isValid()); // please call us with valid urls only 1076 fileItems.append(KFileItem(url)); 1077 } 1078 return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins); 1079 } 1080 #endif 1081 1082 PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) 1083 { 1084 return new PreviewJob(items, size, enabledPlugins); 1085 } 1086 1087 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 5) 1088 KIO::filesize_t PreviewJob::maximumFileSize() 1089 { 1090 KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings"); 1091 return cg.readEntry("MaximumSize", 5 * 1024 * 1024LL /* 5MB */); 1092 } 1093 #endif 1094 1095 #include "moc_previewjob.cpp"