KIO

previewjob.cpp
1// -*- c++ -*-
2/*
3 This file is part of the KDE libraries
4 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
6 SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "previewjob.h"
12#include "filecopyjob.h"
13#include "kiogui_debug.h"
14#include "standardthumbnailjob_p.h"
15#include "statjob.h"
16
17#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) && !defined(Q_OS_HAIKU)
18#define WITH_SHM 1
19#else
20#define WITH_SHM 0
21#endif
22
23#if WITH_SHM
24#include <sys/ipc.h>
25#include <sys/shm.h>
26#endif
27
28#include <algorithm>
29#include <limits>
30
31#include <QCryptographicHash>
32#include <QDir>
33#include <QDirIterator>
34#include <QFile>
35#include <QImage>
36#include <QJsonArray>
37#include <QJsonDocument>
38#include <QMimeDatabase>
39#include <QObject>
40#include <QPixmap>
41#include <QRegularExpression>
42#include <QSaveFile>
43#include <QStandardPaths>
44#include <QTemporaryDir>
45#include <QTemporaryFile>
46#include <QTimer>
47
48#include <KConfigGroup>
49#include <KFileUtils>
50#include <KLocalizedString>
51#include <KMountPoint>
52#include <KPluginMetaData>
53#include <KProtocolInfo>
54#include <KService>
55#include <KSharedConfig>
56#include <Solid/Device>
57#include <Solid/StorageAccess>
58
59#include "job_p.h"
60
61namespace
62{
63static qreal s_defaultDevicePixelRatio = 1.0;
64}
65
66namespace KIO
67{
68struct PreviewItem;
69}
70using namespace KIO;
71
72struct KIO::PreviewItem {
73 KFileItem item;
74 KPluginMetaData plugin;
75 bool standardThumbnailer = false;
76};
77
78class KIO::PreviewJobPrivate : public KIO::JobPrivate
79{
80public:
81 PreviewJobPrivate(const KFileItemList &items, const QSize &size)
82 : initialItems(items)
83 , width(size.width())
84 , height(size.height())
85 , cacheSize(0)
86 , bScale(true)
87 , bSave(true)
88 , ignoreMaximumSize(false)
89 , sequenceIndex(0)
90 , succeeded(false)
91 , maximumLocalSize(0)
92 , maximumRemoteSize(0)
93 , enableRemoteFolderThumbnail(false)
94 , shmid(-1)
95 , shmaddr(nullptr)
96 {
97 // https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
98 thumbRoot = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
99 }
100
101 enum {
102 STATE_STATORIG, // if the thumbnail exists
103 STATE_GETORIG, // if we create it
104 STATE_CREATETHUMB, // thumbnail:/ worker
105 STATE_DEVICE_INFO, // additional state check to get needed device ids
106 } state;
107
108 KFileItemList initialItems;
109 QStringList enabledPlugins;
110 // Our todo list :)
111 // We remove the first item at every step, so use std::list
112 std::list<PreviewItem> items;
113 // The current item
114 PreviewItem currentItem;
115 // The modification time of that URL
116 QDateTime tOrig;
117 // Path to thumbnail cache for the current size
118 QString thumbPath;
119 // Original URL of current item in RFC2396 format
120 // (file:///path/to/a%20file instead of file:/path/to/a file)
121 QByteArray origName;
122 // Thumbnail file name for current item
123 QString thumbName;
124 // Size of thumbnail
125 int width;
126 int height;
127 // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled)
128 short cacheSize;
129 // Whether the thumbnail should be scaled
130 bool bScale;
131 // Whether we should save the thumbnail
132 bool bSave;
133 bool ignoreMaximumSize;
134 int sequenceIndex;
135 bool succeeded;
136 // If the file to create a thumb for was a temp file, this is its name
137 QString tempName;
138 KIO::filesize_t maximumLocalSize;
139 KIO::filesize_t maximumRemoteSize;
140 // Manage preview for locally mounted remote directories
141 bool enableRemoteFolderThumbnail;
142 // Shared memory segment Id. The segment is allocated to a size
143 // of extent x extent x 4 (32 bit image) on first need.
144 int shmid;
145 // And the data area
146 uchar *shmaddr;
147 // Size of the shm segment
148 size_t shmsize;
149 // Root of thumbnail cache
150 QString thumbRoot;
151 // Metadata returned from the KIO thumbnail worker
152 QMap<QString, QString> thumbnailWorkerMetaData;
153 qreal devicePixelRatio = s_defaultDevicePixelRatio;
154 static const int idUnknown = -1;
155 // Id of a device storing currently processed file
156 int currentDeviceId = 0;
157 // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on.
158 QMap<QString, int> deviceIdMap;
159 enum CachePolicy {
160 Prevent,
161 Allow,
162 Unknown
163 } currentDeviceCachePolicy = Unknown;
164 // the path of a unique temporary directory
165 QString m_tempDirPath;
166
167 void getOrCreateThumbnail();
168 bool statResultThumbnail();
169 void createThumbnail(const QString &);
170 void cleanupTempFile();
171 void determineNextFile();
172 void emitPreview(const QImage &thumb);
173
174 void startPreview();
175 void slotThumbData(KIO::Job *, const QByteArray &);
176 void slotStandardThumbData(KIO::Job *, const QImage &);
177 // Checks if thumbnail is on encrypted partition different than thumbRoot
178 CachePolicy canBeCached(const QString &path);
179 int getDeviceId(const QString &path);
180 void saveThumbnailData(QImage &thumb);
181
182 Q_DECLARE_PUBLIC(PreviewJob)
183
184 struct StandardThumbnailerData {
185 QString exec;
186 QStringList mimetypes;
187 };
188
189 static QList<KPluginMetaData> loadAvailablePlugins()
190 {
191 static QList<KPluginMetaData> jsonMetaDataPlugins;
192 if (jsonMetaDataPlugins.isEmpty()) {
193 jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/thumbcreator"));
194 for (const auto &thumbnailer : standardThumbnailers().asKeyValueRange()) {
195 // Check if our own plugins support the mimetype. If so, we use the plugin instead
196 // and ignore the standard thumbnailer
197 auto handledMimes = thumbnailer.second.mimetypes;
198 for (const auto &plugin : std::as_const(jsonMetaDataPlugins)) {
199 for (const auto &mime : handledMimes) {
200 if (plugin.mimeTypes().contains(mime)) {
201 handledMimes.removeOne(mime);
202 }
203 }
204 }
205 if (handledMimes.isEmpty()) {
206 continue;
207 }
208
209 QMimeDatabase db;
210 // We only need the first mimetype since the names/comments are often shared between multiple types
211 auto mime = db.mimeTypeForName(handledMimes.first());
212 auto name = mime.name().isEmpty() ? handledMimes.first() : mime.name();
213 if (!mime.comment().isEmpty()) {
214 name = mime.comment();
215 }
216 if (name.isEmpty()) {
217 continue;
218 }
219 // the plugin metadata
220 QJsonObject kplugin;
221 kplugin[QStringLiteral("MimeTypes")] = QJsonValue::fromVariant(handledMimes);
222 kplugin[QStringLiteral("Name")] = name;
223 kplugin[QStringLiteral("Description")] = QStringLiteral("standardthumbnailer");
224
225 QJsonObject root;
226 root[QStringLiteral("CacheThumbnail")] = true;
227 root[QStringLiteral("KPlugin")] = kplugin;
228
229 KPluginMetaData standardThumbnailerPlugin(root, thumbnailer.first);
230 jsonMetaDataPlugins.append(standardThumbnailerPlugin);
231 }
232 }
233 return jsonMetaDataPlugins;
234 }
235
236 static QMap<QString, StandardThumbnailerData> standardThumbnailers()
237 {
238 // mimetype, exec
239 static QMap<QString, StandardThumbnailerData> standardThumbs;
240 if (standardThumbs.empty()) {
241 QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("thumbnailers/"), QStandardPaths::LocateDirectory);
242 const auto thumbnailerPaths = KFileUtils::findAllUniqueFiles(dirs, QStringList{QStringLiteral("*.thumbnailer")});
243 for (const QString &thumbnailerPath : thumbnailerPaths) {
244 const KConfigGroup thumbnailerConfig(KSharedConfig::openConfig(thumbnailerPath), QStringLiteral("Thumbnailer Entry"));
245 StandardThumbnailerData data;
246 QString thumbnailerName = QFileInfo(thumbnailerPath).baseName();
247 QStringList mimetypes = thumbnailerConfig.readEntry("MimeType", QString{}).split(QStringLiteral(";"));
248 mimetypes.removeAll(QLatin1String(""));
249 QString exec = thumbnailerConfig.readEntry("Exec", QString{});
250 if (!exec.isEmpty() && !mimetypes.isEmpty()) {
251 data.exec = exec;
252 data.mimetypes = mimetypes;
253 standardThumbs.insert(thumbnailerName, data);
254 }
255 }
256 }
257 return standardThumbs;
258 }
259
260private:
261 QDir createTemporaryDir();
262};
263
264void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
265{
266 s_defaultDevicePixelRatio = defaultDevicePixelRatio;
267}
268
269PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
270 : KIO::Job(*new PreviewJobPrivate(items, size))
271{
273
274 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
275 if (enabledPlugins) {
276 d->enabledPlugins = *enabledPlugins;
277 } else {
278 d->enabledPlugins =
279 globalConfig.readEntry("Plugins",
280 QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
281 }
282
283 // Return to event loop first, determineNextFile() might delete this;
284 QTimer::singleShot(0, this, [d]() {
285 d->startPreview();
286 });
287}
288
289PreviewJob::~PreviewJob()
290{
292 if (!d->m_tempDirPath.isEmpty()) {
293 QDir tempDir(d->m_tempDirPath);
294 tempDir.removeRecursively();
295 }
296#if WITH_SHM
297 if (d->shmaddr) {
298 shmdt((char *)d->shmaddr);
299 shmctl(d->shmid, IPC_RMID, nullptr);
300 }
301#endif
302}
303
305{
307 switch (type) {
308 case Unscaled:
309 d->bScale = false;
310 d->bSave = false;
311 break;
312 case Scaled:
313 d->bScale = true;
314 d->bSave = false;
315 break;
316 case ScaledAndCached:
317 d->bScale = true;
318 d->bSave = true;
319 break;
320 default:
321 break;
322 }
323}
324
326{
327 Q_D(const PreviewJob);
328 if (d->bScale) {
329 return d->bSave ? ScaledAndCached : Scaled;
330 }
331 return Unscaled;
332}
333
334void PreviewJobPrivate::startPreview()
335{
336 Q_Q(PreviewJob);
337 // Load the list of plugins to determine which MIME types are supported
338 const QList<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
340
341 // Using thumbnailer plugin
342 for (const KPluginMetaData &plugin : plugins) {
343 bool pluginIsEnabled = enabledPlugins.contains(plugin.pluginId());
344 const auto mimeTypes = plugin.mimeTypes();
345 for (const QString &mimeType : mimeTypes) {
346 if (pluginIsEnabled) {
347 mimeMap.insert(mimeType, plugin);
348 }
349 }
350 }
351
352 // Look for images and store the items in our todo list :)
353 bool bNeedCache = false;
354 for (const auto &fileItem : std::as_const(initialItems)) {
355 PreviewItem item;
356 item.item = fileItem;
357 item.standardThumbnailer = false;
358
359 const QString mimeType = item.item.mimetype();
360 KPluginMetaData plugin;
361
362 auto pluginIt = mimeMap.constFind(mimeType);
363 if (pluginIt == mimeMap.constEnd()) {
364 // check MIME type inheritance, resolve aliases
365 QMimeDatabase db;
366 const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
367 if (mimeInfo.isValid()) {
368 const QStringList parentMimeTypes = mimeInfo.allAncestors();
369 for (const QString &parentMimeType : parentMimeTypes) {
370 pluginIt = mimeMap.constFind(parentMimeType);
371 if (pluginIt != mimeMap.constEnd()) {
372 break;
373 }
374 }
375 }
376
377 if (pluginIt == mimeMap.constEnd()) {
378 // Check the wildcards last, see BUG 453480
379 QString groupMimeType = mimeType;
380 const int slashIdx = groupMimeType.indexOf(QLatin1Char('/'));
381 if (slashIdx != -1) {
382 // Replace everything after '/' with '*'
383 groupMimeType.truncate(slashIdx + 1);
384 groupMimeType += QLatin1Char('*');
385 }
386 pluginIt = mimeMap.constFind(groupMimeType);
387 }
388 }
389
390 if (pluginIt != mimeMap.constEnd()) {
391 plugin = *pluginIt;
392 }
393
394 if (plugin.isValid()) {
395 item.standardThumbnailer = plugin.description() == QStringLiteral("standardthumbnailer");
396 item.plugin = plugin;
397 items.push_back(item);
398
399 if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
400 const QUrl url = fileItem.targetUrl();
401 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
402 bNeedCache = true;
403 }
404 }
405 } else {
406 Q_EMIT q->failed(fileItem);
407 }
408 }
409
410 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
411 maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
412 maximumRemoteSize = cg.readEntry<KIO::filesize_t>("MaximumRemoteSize", 0);
413 enableRemoteFolderThumbnail = cg.readEntry("EnableRemoteFolderThumbnail", false);
414
415 if (bNeedCache) {
416 const int longer = std::max(width, height);
417 if (longer <= 128) {
418 cacheSize = 128;
419 } else if (longer <= 256) {
420 cacheSize = 256;
421 } else if (longer <= 512) {
422 cacheSize = 512;
423 } else {
424 cacheSize = 1024;
425 }
426
427 struct CachePool {
428 QString path;
429 int minSize;
430 };
431
432 const static auto pools = {
433 CachePool{QStringLiteral("normal/"), 128},
434 CachePool{QStringLiteral("large/"), 256},
435 CachePool{QStringLiteral("x-large/"), 512},
436 CachePool{QStringLiteral("xx-large/"), 1024},
437 };
438
439 QString thumbDir;
440 int wants = devicePixelRatio * cacheSize;
441 for (const auto &p : pools) {
442 if (p.minSize < wants) {
443 continue;
444 } else {
445 thumbDir = p.path;
446 break;
447 }
448 }
449 thumbPath = thumbRoot + thumbDir;
450
451 if (!QDir(thumbPath).exists() && !QDir(thumbRoot).mkdir(thumbDir, QFile::ReadUser | QFile::WriteUser | QFile::ExeUser)) { // 0700
452 qCWarning(KIO_GUI) << "couldn't create thumbnail dir " << thumbPath;
453 }
454 } else {
455 bSave = false;
456 }
457
458 initialItems.clear();
459 determineNextFile();
460}
461
463{
465
466 auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
467 return url == pItem.item.url();
468 });
469 if (it != d->items.cend()) {
470 d->items.erase(it);
471 }
472
473 if (d->currentItem.item.url() == url) {
474 KJob *job = subjobs().first();
475 job->kill();
476 removeSubjob(job);
477 d->determineNextFile();
478 }
479}
480
482{
483 d_func()->sequenceIndex = index;
484}
485
487{
488 return d_func()->sequenceIndex;
489}
490
492{
493 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
494}
495
497{
498 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
499}
500
502{
503 d_func()->devicePixelRatio = dpr;
504}
505
507{
508 d_func()->ignoreMaximumSize = ignoreSize;
509}
510
511void PreviewJobPrivate::cleanupTempFile()
512{
513 if (!tempName.isEmpty()) {
514 Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
515 QFile::remove(tempName);
516 tempName.clear();
517 }
518}
519
520void PreviewJobPrivate::determineNextFile()
521{
522 Q_Q(PreviewJob);
523 if (!currentItem.item.isNull()) {
524 if (!succeeded) {
525 Q_EMIT q->failed(currentItem.item);
526 }
527 }
528 // No more items ?
529 if (items.empty()) {
530 q->emitResult();
531 return;
532 } else {
533 // First, stat the orig file
534 state = PreviewJobPrivate::STATE_STATORIG;
535 currentItem = items.front();
536 items.pop_front();
537 succeeded = false;
538 KIO::Job *job = KIO::stat(currentItem.item.targetUrl(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
539 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
540 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
541 q->addSubjob(job);
542 }
543}
544
545void PreviewJob::slotResult(KJob *job)
546{
548
549 removeSubjob(job);
550 Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
551 switch (d->state) {
552 case PreviewJobPrivate::STATE_STATORIG: {
553 if (job->error()) { // that's no good news...
554 // Drop this one and move on to the next one
555 d->determineNextFile();
556 return;
557 }
558 const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
559 d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
561
562 const KIO::filesize_t size = static_cast<KIO::filesize_t>(statResult.numberValue(KIO::UDSEntry::UDS_SIZE, 0));
563 if (size == 0) {
564 qCDebug(KIO_GUI) << "PreviewJob: skipping an empty file, migth be a broken symlink" << d->currentItem.item.url();
565 d->determineNextFile();
566 return;
567 }
568
569 bool skipCurrentItem = false;
570 const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
571 if ((itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) && !d->currentItem.item.isSlow()) {
572 skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
573 } else {
574 // For remote items the "IgnoreMaximumSize" plugin property is not respected
575 // Also we need to check if remote (but locally mounted) folder preview is enabled
576 skipCurrentItem = (!d->ignoreMaximumSize && size > d->maximumRemoteSize) || (d->currentItem.item.isDir() && !d->enableRemoteFolderThumbnail);
577 }
578 if (skipCurrentItem) {
579 d->determineNextFile();
580 return;
581 }
582
583 bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
584 if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
585 // This preview will not be cached, no need to look for a saved thumbnail
586 // Just create it, and be done
587 d->getOrCreateThumbnail();
588 return;
589 }
590
591 if (d->statResultThumbnail()) {
592 d->succeeded = true;
593 d->determineNextFile();
594 return;
595 }
596
597 d->getOrCreateThumbnail();
598 return;
599 }
600 case PreviewJobPrivate::STATE_DEVICE_INFO: {
601 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
602 int id;
603 QString path = statJob->url().toLocalFile();
604 if (job->error()) {
605 // We set id to 0 to know we tried getting it
606 qCWarning(KIO_GUI) << "Cannot read information about filesystem under path" << path;
607 id = 0;
608 } else {
610 }
611 d->deviceIdMap[path] = id;
612 d->createThumbnail(d->currentItem.item.localPath());
613 return;
614 }
615 case PreviewJobPrivate::STATE_GETORIG: {
616 if (job->error()) {
617 d->cleanupTempFile();
618 d->determineNextFile();
619 return;
620 }
621
622 d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
623 return;
624 }
625 case PreviewJobPrivate::STATE_CREATETHUMB: {
626 d->cleanupTempFile();
627 d->determineNextFile();
628 return;
629 }
630 }
631}
632
633bool PreviewJobPrivate::statResultThumbnail()
634{
635 if (thumbPath.isEmpty()) {
636 return false;
637 }
638
639 bool isLocal;
640 const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
641 if (isLocal) {
642 const QFileInfo localFile(url.toLocalFile());
643 const QString canonicalPath = localFile.canonicalFilePath();
645 if (origName.isEmpty()) {
646 qCDebug(KIO_GUI) << "Failed to convert" << url << "to canonical path, possibly a broken symlink";
647 return false;
648 }
649 } else {
650 // Don't include the password if any
651 origName = currentItem.item.targetUrl().toEncoded(QUrl::RemovePassword);
652 }
653
654 QCryptographicHash md5(QCryptographicHash::Md5);
655 md5.addData(origName);
656 thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
657
658 QImage thumb;
659 QFile thumbFile(thumbPath + thumbName);
660 if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
661 return false;
662 }
663
664 if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
665 || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
666 return false;
667 }
668
669 const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
670 if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
671 // Thumb::Size is not required, but if it is set it should match
672 return false;
673 }
674
675 // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
676 // When a thumbnail is DPR-invariant, use the DPR passed in the request.
677 thumb.setDevicePixelRatio(devicePixelRatio);
678
679 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
680
681 if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
682 // Check if the version matches
683 // The software string should read "KDE Thumbnail Generator pluginName (vX)"
684 QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
685 if (softwareString.isEmpty()) {
686 // The thumbnail has been created with an older version, recreating
687 return false;
688 }
689 int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
690 if (versionIndex < 0) {
691 return false;
692 }
693
694 QString cachedVersion = softwareString.remove(0, versionIndex + 2);
695 cachedVersion.chop(1);
696 uint thumbnailerMajor = thumbnailerVersion.toInt();
697 uint cachedMajor = cachedVersion.toInt();
698 if (thumbnailerMajor > cachedMajor) {
699 return false;
700 }
701 }
702
703 // Found it, use it
704 emitPreview(thumb);
705 return true;
706}
707
708void PreviewJobPrivate::getOrCreateThumbnail()
709{
710 Q_Q(PreviewJob);
711 // We still need to load the orig file ! (This is getting tedious) :)
712 const KFileItem &item = currentItem.item;
713 const QString localPath = item.localPath();
714 if (!localPath.isEmpty()) {
715 createThumbnail(localPath);
716 return;
717 }
718
719 if (item.isDir()) {
720 // Skip remote dirs (bug 208625)
721 cleanupTempFile();
722 determineNextFile();
723 return;
724 }
725 // No plugin support access to this remote content, copy the file
726 // to the local machine, then create the thumbnail
727 state = PreviewJobPrivate::STATE_GETORIG;
728 QTemporaryFile localFile;
729
730 // Some thumbnailers, like libkdcraw, depend on the file extension being
731 // correct
732 const QString extension = item.suffix();
733 if (!extension.isEmpty()) {
734 localFile.setFileTemplate(QStringLiteral("%1.%2").arg(localFile.fileTemplate(), extension));
735 }
736
737 localFile.setAutoRemove(false);
738 localFile.open();
739 tempName = localFile.fileName();
740 const QUrl currentURL = item.mostLocalUrl();
741 KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
742 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
743 q->addSubjob(job);
744}
745
746PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
747{
748 // If checked file is directory on a different filesystem than its parent, we need to check it separately
749 int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
750 // special case for root folders
751 const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex);
752
753 int parentId = getDeviceId(parentDirPath);
754 if (parentId == idUnknown) {
755 return CachePolicy::Unknown;
756 }
757
758 bool isDifferentSystem = !parentId || parentId != currentDeviceId;
759 if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
760 return currentDeviceCachePolicy;
761 }
762 int checkedId;
763 QString checkedPath;
764 if (isDifferentSystem) {
765 checkedId = currentDeviceId;
766 checkedPath = path;
767 } else {
768 checkedId = getDeviceId(parentDirPath);
769 checkedPath = parentDirPath;
770 if (checkedId == idUnknown) {
771 return CachePolicy::Unknown;
772 }
773 }
774 // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
775 int thumbRootId = getDeviceId(thumbRoot);
776 if (thumbRootId == idUnknown) {
777 return CachePolicy::Unknown;
778 }
779 bool shouldAllow = checkedId && checkedId == thumbRootId;
780 if (!shouldAllow) {
781 Solid::Device device = Solid::Device::storageAccessFromPath(checkedPath);
782 if (device.isValid()) {
783 // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location.
784 // Or, if the checked device is unencrypted, allow thumbnailing.
785 if (device.as<Solid::StorageAccess>()->isEncrypted()) {
786 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot);
787 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted();
788 } else {
789 shouldAllow = true;
790 }
791 }
792 }
793 if (!isDifferentSystem) {
794 currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
795 }
796 return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
797}
798
799int PreviewJobPrivate::getDeviceId(const QString &path)
800{
801 Q_Q(PreviewJob);
802 auto iter = deviceIdMap.find(path);
803 if (iter != deviceIdMap.end()) {
804 return iter.value();
805 }
806 QUrl url = QUrl::fromLocalFile(path);
807 if (!url.isValid()) {
808 qCWarning(KIO_GUI) << "Could not get device id for file preview, Invalid url" << path;
809 return 0;
810 }
811 state = PreviewJobPrivate::STATE_DEVICE_INFO;
812 KIO::Job *job = KIO::stat(url, StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
813 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
814 q->addSubjob(job);
815
816 return idUnknown;
817}
818
819QDir KIO::PreviewJobPrivate::createTemporaryDir()
820{
821 if (m_tempDirPath.isEmpty()) {
822 auto tempDir = QTemporaryDir();
823 Q_ASSERT(tempDir.isValid());
824
825 tempDir.setAutoRemove(false);
826 // restrict read access to current User
827 QFile::setPermissions(tempDir.path(), QFile::Permission::ReadOwner | QFile::Permission::WriteOwner | QFile::Permission::ExeOwner);
828
829 m_tempDirPath = tempDir.path();
830 }
831
832 return QDir(m_tempDirPath);
833}
834
835void PreviewJobPrivate::createThumbnail(const QString &pixPath)
836{
837 Q_Q(PreviewJob);
838
839 QFileInfo info(pixPath);
840 Q_ASSERT_X(info.isAbsolute(), "PreviewJobPrivate::createThumbnail", qPrintable(QLatin1String("path is not absolute: ") + info.path()));
841
842 state = PreviewJobPrivate::STATE_CREATETHUMB;
843
844 bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
845
846 bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
847 CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
848
849 if (cachePolicy == CachePolicy::Unknown) {
850 // If Unknown is returned, creating thumbnail should be called again by slotResult
851 return;
852 }
853
854 if (currentItem.standardThumbnailer) {
855 // Using /usr/share/thumbnailers
856 QString exec;
857 for (const auto &thumbnailer : standardThumbnailers().asKeyValueRange()) {
858 for (const auto &mimetype : std::as_const(thumbnailer.second.mimetypes)) {
859 if (currentItem.plugin.supportsMimeType(mimetype)) {
860 exec = thumbnailer.second.exec;
861 }
862 }
863 }
864 if (exec.isEmpty()) {
865 qCWarning(KIO_GUI) << "The exec entry for standard thumbnailer " << currentItem.plugin.name() << " was empty!";
866 return;
867 }
868 auto tempDir = createTemporaryDir();
869 if (pixPath.startsWith(tempDir.path())) {
870 // don't generate thumbnails for images already in temporary directory
871 return;
872 }
873
874 KIO::StandardThumbnailJob *job = new KIO::StandardThumbnailJob(exec, width * devicePixelRatio, pixPath, tempDir.path());
875 q->addSubjob(job);
876 q->connect(job, &KIO::StandardThumbnailJob::data, q, [=, this](KIO::Job *job, const QImage &thumb) {
877 slotStandardThumbData(job, thumb);
878 });
879 job->start();
880 return;
881 }
882
883 // Using thumbnailer plugin
884 QUrl thumbURL;
885 thumbURL.setScheme(QStringLiteral("thumbnail"));
886 thumbURL.setPath(pixPath);
887 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
888 q->addSubjob(job);
889 q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
890 slotThumbData(job, data);
891 });
892 int thumb_width = width;
893 int thumb_height = height;
894 if (save) {
895 thumb_width = thumb_height = cacheSize;
896 }
897
898 job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
899 job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
900 job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
901 job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
902 job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
903 job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
904 job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
905 if (sequenceIndex) {
906 job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
907 }
908
909#if WITH_SHM
910 size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
911 if (shmid == -1 || shmsize < requiredSize) {
912 if (shmaddr) {
913 // clean previous shared memory segment
914 shmdt((char *)shmaddr);
915 shmaddr = nullptr;
916 shmctl(shmid, IPC_RMID, nullptr);
917 shmid = -1;
918 }
919 if (requiredSize > 0) {
920 shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
921 if (shmid != -1) {
922 shmsize = requiredSize;
923 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
924 if (shmaddr == (uchar *)-1) {
925 shmctl(shmid, IPC_RMID, nullptr);
926 shmaddr = nullptr;
927 shmid = -1;
928 }
929 }
930 }
931 }
932 if (shmid != -1) {
933 job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
934 }
935#endif
936}
937
938void PreviewJobPrivate::slotStandardThumbData(KIO::Job *job, const QImage &thumbData)
939{
940 thumbnailWorkerMetaData = job->metaData();
941
942 if (thumbData.isNull()) {
943 // let succeeded in false state
944 // failed will get called in determineNextFile()
945 return;
946 }
947
948 QImage thumb = thumbData;
949 saveThumbnailData(thumb);
950
951 emitPreview(thumb);
952 succeeded = true;
953}
954
955void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
956{
957 QImage thumb;
958 // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp
959 QDataStream str(data);
960
961#if WITH_SHM
962 if (shmaddr != nullptr) {
963 int width;
964 int height;
965 QImage::Format format;
966 qreal imgDevicePixelRatio;
967 // TODO KF6: add a version number as first parameter
968 str >> width >> height >> format >> imgDevicePixelRatio;
969 thumb = QImage(shmaddr, width, height, format).copy();
970 thumb.setDevicePixelRatio(imgDevicePixelRatio);
971 }
972#endif
973
974 if (thumb.isNull()) {
975 // fallback a raw QImage
976 str >> thumb;
977 }
978
979 slotStandardThumbData(job, thumb);
980}
981
982void PreviewJobPrivate::saveThumbnailData(QImage &thumb)
983{
984 const bool save = bSave && !sequenceIndex && currentDeviceCachePolicy == CachePolicy::Allow
985 && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
986 && (!currentItem.item.targetUrl().isLocalFile() || !currentItem.item.targetUrl().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
987
988 if (save) {
989 thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
990 thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
991 thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
992 thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
993 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
994 QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
995 if (!thumbnailerVersion.isEmpty()) {
996 signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
997 }
998 thumb.setText(QStringLiteral("Software"), signature);
999 QSaveFile saveFile(thumbPath + thumbName);
1000 if (saveFile.open(QIODevice::WriteOnly)) {
1001 if (thumb.save(&saveFile, "PNG")) {
1002 saveFile.commit();
1003 }
1004 }
1005 }
1006}
1007
1008void PreviewJobPrivate::emitPreview(const QImage &thumb)
1009{
1010 Q_Q(PreviewJob);
1011 QPixmap pix;
1012 const qreal ratio = thumb.devicePixelRatio();
1013 if (thumb.width() > width * ratio || thumb.height() > height * ratio) {
1014 pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation));
1015 } else {
1016 pix = QPixmap::fromImage(thumb);
1017 }
1018 pix.setDevicePixelRatio(ratio);
1019 Q_EMIT q->gotPreview(currentItem.item, pix);
1020}
1021
1023{
1024 return PreviewJobPrivate::loadAvailablePlugins();
1025}
1026
1028{
1030 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1031 for (const KPluginMetaData &plugin : plugins) {
1032 result << plugin.pluginId();
1033 }
1034 return result;
1035}
1036
1038{
1039 const QStringList exclusionList = QStringList() << QStringLiteral("textthumbnail");
1040
1042 for (const QString &plugin : exclusionList) {
1043 defaultPlugins.removeAll(plugin);
1044 }
1045
1046 return defaultPlugins;
1047}
1048
1050{
1052 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1053 for (const KPluginMetaData &plugin : plugins) {
1054 result += plugin.mimeTypes();
1055 }
1056 return result;
1057}
1058
1059PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
1060{
1061 return new PreviewJob(items, size, enabledPlugins);
1062}
1063
1064#include "moc_previewjob.cpp"
bool hasSubjobs() const
const QList< KJob * > & subjobs() const
QString readEntry(const char *key, const char *aDefault=nullptr) const
List of KFileItems, which adds a few helper methods to QList<KFileItem>.
Definition kfileitem.h:632
QUrl mostLocalUrl(bool *local=nullptr) const
Tries to return a local URL for this file item if possible.
QString suffix() const
Returns the file extension Similar to QFileInfo::suffix except it takes into account UDS_DISPLAY_NAME...
bool removeSubjob(KJob *job) override
Mark a sub job as being done.
Definition job.cpp:80
MetaData metaData() const
Get meta data received from the worker.
Definition job.cpp:205
void addMetaData(const QString &key, const QString &value)
Add key/value pair to the meta data that is sent to the worker.
Definition job.cpp:221
KIO Job to get a thumbnail picture.
Definition previewjob.h:31
int sequenceIndex() const
Returns the currently set sequence index.
void setScaleType(ScaleType type)
Sets the scale type for the generated preview.
void setDevicePixelRatio(qreal dpr)
Request preview to use the device pixel ratio dpr.
void removeItem(const QUrl &url)
Removes an item from preview processing.
static QStringList defaultPlugins()
Returns a list of plugins that should be enabled by default, which is all plugins Minus the plugins s...
static QStringList supportedMimeTypes()
Returns a list of all supported MIME types.
static QStringList availablePlugins()
Returns a list of all available preview plugins.
float sequenceIndexWraparoundPoint() const
Returns the index at which the thumbs of a ThumbSequenceCreator start wrapping around ("looping").
ScaleType
Specifies the type of scaling that is applied to the generated preview.
Definition previewjob.h:39
@ Unscaled
The original size of the preview will be returned.
Definition previewjob.h:44
@ Scaled
The preview will be scaled to the size specified when constructing the PreviewJob.
Definition previewjob.h:49
@ ScaledAndCached
The preview will be scaled to the size specified when constructing the PreviewJob.
Definition previewjob.h:55
PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins=nullptr)
bool handlesSequences() const
Determines whether the ThumbCreator in use is a ThumbSequenceCreator.
void setSequenceIndex(int index)
Sets the sequence index given to the thumb creators.
ScaleType scaleType() const
void setIgnoreMaximumSize(bool ignoreSize=true)
If ignoreSize is true, then the preview is always generated regardless of the settings.
static void setDefaultDevicePixelRatio(qreal devicePixelRatio)
Sets a default device Pixel Ratio used for Previews.
static QList< KPluginMetaData > availableThumbnailerPlugins()
Returns all plugins that are considered when a preview is generated The result is internally cached,...
const QUrl & url() const
Returns the SimpleJob's URL.
Definition simplejob.cpp:70
const UDSEntry & statResult() const
Result of the stat operation.
Definition statjob.cpp:80
void data(KIO::Job *job, const QByteArray &data)
Data from the worker has arrived.
long long numberValue(uint field, long long defaultValue=0) const
Definition udsentry.cpp:370
@ UDS_MODIFICATION_TIME
The last time the file was modified. Required time format: seconds since UNIX epoch.
Definition udsentry.h:234
@ UDS_SIZE
Size of the file.
Definition udsentry.h:203
@ UDS_DEVICE_ID
Device number for this file, used to detect hardlinks.
Definition udsentry.h:298
int error() const
void result(KJob *job)
KJob(QObject *parent=nullptr)
bool kill(KJob::KillVerbosity verbosity=KJob::Quietly)
QString pluginId() const
QStringList mimeTypes() const
bool value(QStringView key, bool defaultValue) const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
bool isValid() const
QString description() const
static QString protocolClass(const QString &protocol)
Returns the protocol class for the specified protocol.
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
static Device storageAccessFromPath(const QString &path)
bool isValid() const
DevIface * as()
KCALUTILS_EXPORT QString mimeType()
KCOREADDONS_EXPORT QStringList findAllUniqueFiles(const QStringList &dirs, const QStringList &nameFilters={})
A namespace for KIO globals.
KIOCORE_EXPORT MkdirJob * mkdir(const QUrl &url, int permissions=-1)
Creates a single directory.
Definition mkdirjob.cpp:110
KIOGUI_EXPORT PreviewJob * filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins=nullptr)
Creates a PreviewJob to generate a preview image for the given items.
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
Find all details for one file or directory.
Definition statjob.cpp:203
KIOCORE_EXPORT QString number(KIO::filesize_t size)
Converts a size to a string representation Not unlike QString::number(...)
Definition global.cpp:55
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
Get (means: read).
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
Find MIME type for one file or directory.
KIOCORE_EXPORT FileCopyJob * file_copy(const QUrl &src, const QUrl &dest, int permissions=-1, JobFlags flags=DefaultFlags)
Copy a single file.
@ HideProgressInfo
Hide progress information dialog, i.e. don't show a GUI.
Definition job_base.h:251
@ Overwrite
When set, automatically overwrite the destination if it exists already.
Definition job_base.h:267
qulonglong filesize_t
64-bit file size
Definition global.h:35
@ StatDefaultDetails
Default StatDetail flag when creating a StatJob.
Definition global.h:275
@ StatInode
dev, inode
Definition global.h:265
QString path(const QString &relativePath)
QString name(StandardAction id)
KGuiItem save()
QDateTime fromSecsSinceEpoch(qint64 secs)
bool remove()
virtual bool setPermissions(Permissions permissions) override
QImage copy(const QRect &rectangle) const const
qreal devicePixelRatio() const const
int height() const const
bool isNull() const const
bool load(QIODevice *device, const char *format)
bool save(QIODevice *device, const char *format, int quality) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
void setDevicePixelRatio(qreal scaleFactor)
void setText(const QString &key, const QString &text)
QString text(const QString &key) const const
int width() const const
QJsonValue fromVariant(const QVariant &variant)
void append(QList< T > &&value)
T & first()
bool isEmpty() const const
qsizetype removeAll(const AT &t)
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
bool empty() const const
iterator insert(const Key &key, const T &value)
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool isValid() const const
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
void setDevicePixelRatio(qreal scaleFactor)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
void chop(qsizetype n)
void clear()
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
qulonglong toULongLong(bool *ok, int base) const const
QString trimmed() const const
void truncate(qsizetype position)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
KeepAspectRatio
SmoothTransformation
virtual QString fileName() const const override
QString fileTemplate() const const
void setAutoRemove(bool b)
void setFileTemplate(const QString &name)
FullyEncoded
RemoveFilename
QUrl adjusted(FormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool isLocalFile() const const
bool isValid() const const
QString scheme() const const
void setPath(const QString &path, ParsingMode mode)
void setScheme(const QString &scheme)
QByteArray toEncoded(FormattingOptions options) const const
QString toLocalFile() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:52:08 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.