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