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)
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
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 { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown;
160
161 void getOrCreateThumbnail();
162 bool statResultThumbnail();
163 void createThumbnail(const QString &);
164 void cleanupTempFile();
165 void determineNextFile();
166 void emitPreview(const QImage &thumb);
167
168 void startPreview();
169 void slotThumbData(KIO::Job *, const QByteArray &);
170 void slotStandardThumbData(KIO::Job *, const QImage &);
171 // Checks if thumbnail is on encrypted partition different than thumbRoot
172 CachePolicy canBeCached(const QString &path);
173 int getDeviceId(const QString &path);
174 void saveThumbnailData(QImage &thumb);
175
176 Q_DECLARE_PUBLIC(PreviewJob)
177
178 struct StandardThumbnailerData {
179 QString exec;
180 QStringList mimetypes;
181 };
182
183 static QList<KPluginMetaData> loadAvailablePlugins()
184 {
185 static QList<KPluginMetaData> jsonMetaDataPlugins;
186 if (jsonMetaDataPlugins.isEmpty()) {
187 jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/thumbcreator"));
188 for (const auto &thumbnailer : standardThumbnailers().asKeyValueRange()) {
189 // Check if our own plugins support the mimetype. If so, we use the plugin instead
190 // and ignore the standard thumbnailer
191 auto handledMimes = thumbnailer.second.mimetypes;
192 for (const auto &plugin : std::as_const(jsonMetaDataPlugins)) {
193 for (const auto &mime : handledMimes) {
194 if (plugin.mimeTypes().contains(mime)) {
195 handledMimes.removeOne(mime);
196 }
197 }
198 }
199 if (handledMimes.isEmpty()) {
200 continue;
201 }
202
203 QMimeDatabase db;
204 // We only need the first mimetype since the names/comments are often shared between multiple types
205 auto mime = db.mimeTypeForName(handledMimes.first());
206 auto name = mime.name().isEmpty() ? handledMimes.first() : mime.name();
207 if (!mime.comment().isEmpty()) {
208 name = mime.comment();
209 }
210 if (name.isEmpty()) {
211 continue;
212 }
213 // the plugin metadata
214 QJsonObject kplugin;
215 kplugin[QStringLiteral("MimeTypes")] = QJsonValue::fromVariant(handledMimes);
216 kplugin[QStringLiteral("Name")] = name;
217 kplugin[QStringLiteral("Description")] = QStringLiteral("standardthumbnailer");
218
219 QJsonObject root;
220 root[QStringLiteral("CacheThumbnail")] = true;
221 root[QStringLiteral("KPlugin")] = kplugin;
222
223 KPluginMetaData standardThumbnailerPlugin(root, thumbnailer.first);
224 jsonMetaDataPlugins.append(standardThumbnailerPlugin);
225 }
226 }
227 return jsonMetaDataPlugins;
228 }
229
230 static QMap<QString, StandardThumbnailerData> standardThumbnailers()
231 {
232 // mimetype, exec
233 static QMap<QString, StandardThumbnailerData> standardThumbs;
234 if (standardThumbs.empty()) {
236 const auto thumbnailerPaths = KFileUtils::findAllUniqueFiles(dirs, QStringList{QStringLiteral("*.thumbnailer")});
237 for (const QString &thumbnailerPath : thumbnailerPaths) {
238 const KConfigGroup thumbnailerConfig(KSharedConfig::openConfig(thumbnailerPath), QStringLiteral("Thumbnailer Entry"));
239 StandardThumbnailerData data;
240 QString thumbnailerName = QFileInfo(thumbnailerPath).baseName();
241 QStringList mimetypes = thumbnailerConfig.readEntry("MimeType", QString{}).split(QStringLiteral(";"));
242 mimetypes.removeAll(QLatin1String(""));
243 QString exec = thumbnailerConfig.readEntry("Exec", QString{});
244 if (!exec.isEmpty() && !mimetypes.isEmpty()) {
245 data.exec = exec;
246 data.mimetypes = mimetypes;
247 standardThumbs.insert(thumbnailerName, data);
248 }
249 }
250 }
251 return standardThumbs;
252 }
253};
254
255void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
256{
257 s_defaultDevicePixelRatio = defaultDevicePixelRatio;
258}
259
260PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
261 : KIO::Job(*new PreviewJobPrivate(items, size))
262{
264
265 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
266 if (enabledPlugins) {
267 d->enabledPlugins = *enabledPlugins;
268 } else {
269 d->enabledPlugins =
270 globalConfig.readEntry("Plugins",
271 QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
272 }
273
274 // Return to event loop first, determineNextFile() might delete this;
275 QTimer::singleShot(0, this, [d]() {
276 d->startPreview();
277 });
278}
279
280PreviewJob::~PreviewJob()
281{
282#if WITH_SHM
284 if (d->shmaddr) {
285 shmdt((char *)d->shmaddr);
286 shmctl(d->shmid, IPC_RMID, nullptr);
287 }
288#endif
289}
290
292{
294 switch (type) {
295 case Unscaled:
296 d->bScale = false;
297 d->bSave = false;
298 break;
299 case Scaled:
300 d->bScale = true;
301 d->bSave = false;
302 break;
303 case ScaledAndCached:
304 d->bScale = true;
305 d->bSave = true;
306 break;
307 default:
308 break;
309 }
310}
311
313{
314 Q_D(const PreviewJob);
315 if (d->bScale) {
316 return d->bSave ? ScaledAndCached : Scaled;
317 }
318 return Unscaled;
319}
320
321void PreviewJobPrivate::startPreview()
322{
323 Q_Q(PreviewJob);
324 // Load the list of plugins to determine which MIME types are supported
325 const QList<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
327
328 // Using thumbnailer plugin
329 for (const KPluginMetaData &plugin : plugins) {
330 bool pluginIsEnabled = enabledPlugins.contains(plugin.pluginId());
331 const auto mimeTypes = plugin.mimeTypes();
332 for (const QString &mimeType : mimeTypes) {
333 if (pluginIsEnabled) {
334 mimeMap.insert(mimeType, plugin);
335 }
336 }
337 }
338
339 // Look for images and store the items in our todo list :)
340 bool bNeedCache = false;
341 for (const auto &fileItem : std::as_const(initialItems)) {
342 PreviewItem item;
343 item.item = fileItem;
344 item.standardThumbnailer = false;
345
346 const QString mimeType = item.item.mimetype();
347 KPluginMetaData plugin;
348
349 auto pluginIt = mimeMap.constFind(mimeType);
350 if (pluginIt == mimeMap.constEnd()) {
351 // check MIME type inheritance, resolve aliases
352 QMimeDatabase db;
353 const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
354 if (mimeInfo.isValid()) {
355 const QStringList parentMimeTypes = mimeInfo.allAncestors();
356 for (const QString &parentMimeType : parentMimeTypes) {
357 pluginIt = mimeMap.constFind(parentMimeType);
358 if (pluginIt != mimeMap.constEnd()) {
359 break;
360 }
361 }
362 }
363
364 if (pluginIt == mimeMap.constEnd()) {
365 // Check the wildcards last, see BUG 453480
366 QString groupMimeType = mimeType;
367 const int slashIdx = groupMimeType.indexOf(QLatin1Char('/'));
368 if (slashIdx != -1) {
369 // Replace everything after '/' with '*'
370 groupMimeType.truncate(slashIdx + 1);
371 groupMimeType += QLatin1Char('*');
372 }
373 pluginIt = mimeMap.constFind(groupMimeType);
374 }
375 }
376
377 if (pluginIt != mimeMap.constEnd()) {
378 plugin = *pluginIt;
379 }
380
381 if (plugin.isValid()) {
382 item.standardThumbnailer = plugin.description() == QStringLiteral("standardthumbnailer");
383 item.plugin = plugin;
384 items.push_back(item);
385
386 if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
387 const QUrl url = fileItem.targetUrl();
388 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
389 bNeedCache = true;
390 }
391 }
392 } else {
393 Q_EMIT q->failed(fileItem);
394 }
395 }
396
397 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
398 maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
399 maximumRemoteSize = cg.readEntry<KIO::filesize_t>("MaximumRemoteSize", 0);
400 enableRemoteFolderThumbnail = cg.readEntry("EnableRemoteFolderThumbnail", false);
401
402 if (bNeedCache) {
403 const int longer = std::max(width, height);
404 if (longer <= 128) {
405 cacheSize = 128;
406 } else if (longer <= 256) {
407 cacheSize = 256;
408 } else if (longer <= 512) {
409 cacheSize = 512;
410 } else {
411 cacheSize = 1024;
412 }
413
414 struct CachePool {
416 int minSize;
417 };
418
419 const static auto pools = {
420 CachePool{QStringLiteral("normal/"), 128},
421 CachePool{QStringLiteral("large/"), 256},
422 CachePool{QStringLiteral("x-large/"), 512},
423 CachePool{QStringLiteral("xx-large/"), 1024},
424 };
425
426 QString thumbDir;
427 int wants = devicePixelRatio * cacheSize;
428 for (const auto &p : pools) {
429 if (p.minSize < wants) {
430 continue;
431 } else {
432 thumbDir = p.path;
433 break;
434 }
435 }
436 thumbPath = thumbRoot + thumbDir;
437
438 if (!QDir(thumbPath).exists() && !QDir(thumbRoot).mkdir(thumbDir, QFile::ReadUser | QFile::WriteUser | QFile::ExeUser)) { // 0700
439 qCWarning(KIO_GUI) << "couldn't create thumbnail dir " << thumbPath;
440 }
441 } else {
442 bSave = false;
443 }
444
445 initialItems.clear();
446 determineNextFile();
447}
448
450{
452
453 auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
454 return url == pItem.item.url();
455 });
456 if (it != d->items.cend()) {
457 d->items.erase(it);
458 }
459
460 if (d->currentItem.item.url() == url) {
461 KJob *job = subjobs().first();
462 job->kill();
463 removeSubjob(job);
464 d->determineNextFile();
465 }
466}
467
469{
470 d_func()->sequenceIndex = index;
471}
472
474{
475 return d_func()->sequenceIndex;
476}
477
479{
480 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
481}
482
484{
485 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
486}
487
489{
490 d_func()->devicePixelRatio = dpr;
491}
492
494{
495 d_func()->ignoreMaximumSize = ignoreSize;
496}
497
498void PreviewJobPrivate::cleanupTempFile()
499{
500 if (!tempName.isEmpty()) {
501 Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
502 QFile::remove(tempName);
503 tempName.clear();
504 }
505}
506
507void PreviewJobPrivate::determineNextFile()
508{
509 Q_Q(PreviewJob);
510 if (!currentItem.item.isNull()) {
511 if (!succeeded) {
512 Q_EMIT q->failed(currentItem.item);
513 }
514 }
515 // No more items ?
516 if (items.empty()) {
517 q->emitResult();
518 return;
519 } else {
520 // First, stat the orig file
521 state = PreviewJobPrivate::STATE_STATORIG;
522 currentItem = items.front();
523 items.pop_front();
524 succeeded = false;
525 KIO::Job *job = KIO::stat(currentItem.item.targetUrl(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
526 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
527 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
528 q->addSubjob(job);
529 }
530}
531
532void PreviewJob::slotResult(KJob *job)
533{
535
536 removeSubjob(job);
537 Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
538 switch (d->state) {
539 case PreviewJobPrivate::STATE_STATORIG: {
540 if (job->error()) { // that's no good news...
541 // Drop this one and move on to the next one
542 d->determineNextFile();
543 return;
544 }
545 const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
546 d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
548
549 bool skipCurrentItem = false;
551 const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
552
553 if ((itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) && !d->currentItem.item.isSlow()) {
554 skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
555 } else {
556 // For remote items the "IgnoreMaximumSize" plugin property is not respected
557 // Also we need to check if remote (but locally mounted) folder preview is enabled
558 skipCurrentItem = (!d->ignoreMaximumSize && size > d->maximumRemoteSize) || (d->currentItem.item.isDir() && !d->enableRemoteFolderThumbnail);
559 }
560 if (skipCurrentItem) {
561 d->determineNextFile();
562 return;
563 }
564
565 bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
566 if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
567 // This preview will not be cached, no need to look for a saved thumbnail
568 // Just create it, and be done
569 d->getOrCreateThumbnail();
570 return;
571 }
572
573 if (d->statResultThumbnail()) {
574 d->succeeded = true;
575 d->determineNextFile();
576 return;
577 }
578
579 d->getOrCreateThumbnail();
580 return;
581 }
582 case PreviewJobPrivate::STATE_DEVICE_INFO: {
583 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
584 int id;
585 QString path = statJob->url().toLocalFile();
586 if (job->error()) {
587 // We set id to 0 to know we tried getting it
588 qCWarning(KIO_GUI) << "Cannot read information about filesystem under path" << path;
589 id = 0;
590 } else {
592 }
593 d->deviceIdMap[path] = id;
594 d->createThumbnail(d->currentItem.item.localPath());
595 return;
596 }
597 case PreviewJobPrivate::STATE_GETORIG: {
598 if (job->error()) {
599 d->cleanupTempFile();
600 d->determineNextFile();
601 return;
602 }
603
604 d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
605 return;
606 }
607 case PreviewJobPrivate::STATE_CREATETHUMB: {
608 d->cleanupTempFile();
609 d->determineNextFile();
610 return;
611 }
612 }
613}
614
615bool PreviewJobPrivate::statResultThumbnail()
616{
617 if (thumbPath.isEmpty()) {
618 return false;
619 }
620
621 bool isLocal;
622 const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
623 if (isLocal) {
624 const QFileInfo localFile(url.toLocalFile());
625 const QString canonicalPath = localFile.canonicalFilePath();
627 if (origName.isEmpty()) {
628 qCWarning(KIO_GUI) << "Failed to convert" << url << "to canonical path";
629 return false;
630 }
631 } else {
632 // Don't include the password if any
633 origName = currentItem.item.targetUrl().toEncoded(QUrl::RemovePassword);
634 }
635
637 md5.addData(origName);
638 thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
639
640 QImage thumb;
641 QFile thumbFile(thumbPath + thumbName);
642 if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
643 return false;
644 }
645
646 if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
647 || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
648 return false;
649 }
650
651 const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
652 if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
653 // Thumb::Size is not required, but if it is set it should match
654 return false;
655 }
656
657 // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
658 // When a thumbnail is DPR-invariant, use the DPR passed in the request.
659 thumb.setDevicePixelRatio(devicePixelRatio);
660
661 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
662
663 if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
664 // Check if the version matches
665 // The software string should read "KDE Thumbnail Generator pluginName (vX)"
666 QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
667 if (softwareString.isEmpty()) {
668 // The thumbnail has been created with an older version, recreating
669 return false;
670 }
671 int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
672 if (versionIndex < 0) {
673 return false;
674 }
675
676 QString cachedVersion = softwareString.remove(0, versionIndex + 2);
677 cachedVersion.chop(1);
678 uint thumbnailerMajor = thumbnailerVersion.toInt();
679 uint cachedMajor = cachedVersion.toInt();
680 if (thumbnailerMajor > cachedMajor) {
681 return false;
682 }
683 }
684
685 // Found it, use it
686 emitPreview(thumb);
687 return true;
688}
689
690void PreviewJobPrivate::getOrCreateThumbnail()
691{
692 Q_Q(PreviewJob);
693 // We still need to load the orig file ! (This is getting tedious) :)
694 const KFileItem &item = currentItem.item;
695 const QString localPath = item.localPath();
696 if (!localPath.isEmpty()) {
697 createThumbnail(localPath);
698 return;
699 }
700
701 if (item.isDir()) {
702 // Skip remote dirs (bug 208625)
703 cleanupTempFile();
704 determineNextFile();
705 return;
706 }
707 // No plugin support access to this remote content, copy the file
708 // to the local machine, then create the thumbnail
709 state = PreviewJobPrivate::STATE_GETORIG;
710 QTemporaryFile localFile;
711
712 // Some thumbnailers, like libkdcraw, depend on the file extension being
713 // correct
714 const QString extension = item.suffix();
715 if (!extension.isEmpty()) {
716 localFile.setFileTemplate(QStringLiteral("%1.%2").arg(localFile.fileTemplate(), extension));
717 }
718
719 localFile.setAutoRemove(false);
720 localFile.open();
721 tempName = localFile.fileName();
722 const QUrl currentURL = item.mostLocalUrl();
723 KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
724 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
725 q->addSubjob(job);
726}
727
728PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
729{
730 // If checked file is directory on a different filesystem than its parent, we need to check it separately
731 int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
732 // special case for root folders
733 const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex);
734
735 int parentId = getDeviceId(parentDirPath);
736 if (parentId == idUnknown) {
737 return CachePolicy::Unknown;
738 }
739
740 bool isDifferentSystem = !parentId || parentId != currentDeviceId;
741 if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
742 return currentDeviceCachePolicy;
743 }
744 int checkedId;
745 QString checkedPath;
746 if (isDifferentSystem) {
747 checkedId = currentDeviceId;
748 checkedPath = path;
749 } else {
750 checkedId = getDeviceId(parentDirPath);
751 checkedPath = parentDirPath;
752 if (checkedId == idUnknown) {
753 return CachePolicy::Unknown;
754 }
755 }
756 // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
757 int thumbRootId = getDeviceId(thumbRoot);
758 if (thumbRootId == idUnknown) {
759 return CachePolicy::Unknown;
760 }
761 bool shouldAllow = checkedId && checkedId == thumbRootId;
762 if (!shouldAllow) {
764 if (device.isValid()) {
765 // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location.
766 // Or, if the checked device is unencrypted, allow thumbnailing.
767 if (device.as<Solid::StorageAccess>()->isEncrypted()) {
768 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot);
769 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted();
770 } else {
771 shouldAllow = true;
772 }
773 }
774 }
775 if (!isDifferentSystem) {
776 currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
777 }
778 return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
779}
780
781int PreviewJobPrivate::getDeviceId(const QString &path)
782{
783 Q_Q(PreviewJob);
784 auto iter = deviceIdMap.find(path);
785 if (iter != deviceIdMap.end()) {
786 return iter.value();
787 }
788 QUrl url = QUrl::fromLocalFile(path);
789 if (!url.isValid()) {
790 qCWarning(KIO_GUI) << "Could not get device id for file preview, Invalid url" << path;
791 return 0;
792 }
793 state = PreviewJobPrivate::STATE_DEVICE_INFO;
795 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
796 q->addSubjob(job);
797
798 return idUnknown;
799}
800
801void PreviewJobPrivate::createThumbnail(const QString &pixPath)
802{
803 Q_Q(PreviewJob);
804 state = PreviewJobPrivate::STATE_CREATETHUMB;
805 QUrl thumbURL;
806 thumbURL.setScheme(QStringLiteral("thumbnail"));
807 thumbURL.setPath(pixPath);
808
809 bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
810
811 bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
812 CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
813
814 if (cachePolicy == CachePolicy::Unknown) {
815 // If Unknown is returned, creating thumbnail should be called again by slotResult
816 return;
817 }
818
819 // If caching is off, thumbPath is empty. Then we can't use standard thumbnailers.
820 if (currentItem.standardThumbnailer && !thumbPath.isEmpty()) {
821 // Using /usr/share/thumbnailers
822 QString exec;
823 for (const auto &thumbnailer : standardThumbnailers().asKeyValueRange()) {
824 for (const auto &mimetype : std::as_const(thumbnailer.second.mimetypes)) {
825 if (currentItem.plugin.supportsMimeType(mimetype)) {
826 exec = thumbnailer.second.exec;
827 }
828 }
829 }
830 if (exec.isEmpty()) {
831 qCWarning(KIO_GUI) << "The exec entry for standard thumbnailer " << currentItem.plugin.name() << " was empty!";
832 return;
833 }
834
835 // Thumbnailer binaries only save and load files, so they can't be loaded directly into memory
836 const QString tempPath = thumbPath + QStringLiteral("/tmp/");
837 if (!QDir().exists(tempPath)) {
838 QDir().mkpath(tempPath);
839 }
840 const QString tempOutputPath = tempPath + thumbName;
841
842 KIO::StandardThumbnailJob *job = new KIO::StandardThumbnailJob(exec, width * devicePixelRatio, currentItem.item.localPath(), tempOutputPath);
843 q->addSubjob(job);
844 q->connect(job, &KIO::StandardThumbnailJob::data, q, [=, this](KIO::Job *job, const QImage &thumb) {
845 slotStandardThumbData(job, thumb);
846 // Delete the tmp file
847 QFile::remove(tempOutputPath);
848 });
849
850 } else {
851 // Using thumbnailer plugin
852 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
853 q->addSubjob(job);
854 q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
855 slotThumbData(job, data);
856 });
857 int thumb_width = width;
858 int thumb_height = height;
859 if (save) {
860 thumb_width = thumb_height = cacheSize;
861 }
862
863 job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
864 job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
865 job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
866 job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
867 job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
868 job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
869 job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
870 if (sequenceIndex) {
871 job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
872 }
873
874#if WITH_SHM
875 size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
876 if (shmid == -1 || shmsize < requiredSize) {
877 if (shmaddr) {
878 // clean previous shared memory segment
879 shmdt((char *)shmaddr);
880 shmaddr = nullptr;
881 shmctl(shmid, IPC_RMID, nullptr);
882 shmid = -1;
883 }
884 if (requiredSize > 0) {
885 shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
886 if (shmid != -1) {
887 shmsize = requiredSize;
888 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
889 if (shmaddr == (uchar *)-1) {
890 shmctl(shmid, IPC_RMID, nullptr);
891 shmaddr = nullptr;
892 shmid = -1;
893 }
894 }
895 }
896 }
897 if (shmid != -1) {
898 job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
899 }
900#endif
901 }
902}
903
904void PreviewJobPrivate::slotStandardThumbData(KIO::Job *job, const QImage &thumbData)
905{
906 thumbnailWorkerMetaData = job->metaData();
907
908 if (thumbData.isNull()) {
909 // let succeeded in false state
910 // failed will get called in determineNextFile()
911 return;
912 }
913
914 QImage thumb = thumbData;
915 saveThumbnailData(thumb);
916
917 emitPreview(thumb);
918 succeeded = true;
919}
920
921void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
922{
923 QImage thumb;
924 // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp
925 QDataStream str(data);
926
927#if WITH_SHM
928 if (shmaddr != nullptr) {
929 int width;
930 int height;
931 QImage::Format format;
932 qreal imgDevicePixelRatio;
933 // TODO KF6: add a version number as first parameter
934 str >> width >> height >> format >> imgDevicePixelRatio;
935 thumb = QImage(shmaddr, width, height, format).copy();
936 thumb.setDevicePixelRatio(imgDevicePixelRatio);
937 }
938#endif
939
940 if (thumb.isNull()) {
941 // fallback a raw QImage
942 str >> thumb;
943 }
944
945 slotStandardThumbData(job, thumb);
946}
947
948void PreviewJobPrivate::saveThumbnailData(QImage &thumb)
949{
950 const bool save = bSave && !sequenceIndex && currentDeviceCachePolicy == CachePolicy::Allow
951 && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
952 && (!currentItem.item.targetUrl().isLocalFile() || !currentItem.item.targetUrl().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
953
954 if (save) {
955 thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
956 thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
957 thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
958 thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
959 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
960 QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
961 if (!thumbnailerVersion.isEmpty()) {
962 signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
963 }
964 thumb.setText(QStringLiteral("Software"), signature);
965 QSaveFile saveFile(thumbPath + thumbName);
966 if (saveFile.open(QIODevice::WriteOnly)) {
967 if (thumb.save(&saveFile, "PNG")) {
968 saveFile.commit();
969 }
970 }
971 }
972}
973
974void PreviewJobPrivate::emitPreview(const QImage &thumb)
975{
976 Q_Q(PreviewJob);
977 QPixmap pix;
978 const qreal ratio = thumb.devicePixelRatio();
979 if (thumb.width() > width * ratio || thumb.height() > height * ratio) {
980 pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation));
981 } else {
982 pix = QPixmap::fromImage(thumb);
983 }
984 pix.setDevicePixelRatio(ratio);
985 Q_EMIT q->gotPreview(currentItem.item, pix);
986}
987
989{
990 return PreviewJobPrivate::loadAvailablePlugins();
991}
992
994{
996 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
997 for (const KPluginMetaData &plugin : plugins) {
998 result << plugin.pluginId();
999 }
1000 return result;
1001}
1002
1004{
1005 const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
1006
1008 for (const QString &plugin : blacklist) {
1009 defaultPlugins.removeAll(plugin);
1010 }
1011
1012 return defaultPlugins;
1013}
1014
1016{
1018 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1019 for (const KPluginMetaData &plugin : plugins) {
1020 result += plugin.mimeTypes();
1021 }
1022 return result;
1023}
1024
1025PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
1026{
1027 return new PreviewJob(items, size, enabledPlugins);
1028}
1029
1030#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:630
A KFileItem is a generic class to handle a file, local or remote.
Definition kfileitem.h:36
QUrl mostLocalUrl(bool *local=nullptr) const
Tries to return a local URL for this file item if possible.
KIO::filesize_t size() const
Returns the size of the file, if known.
bool isNull() const
Return true if default-constructed.
QString suffix() const
Returns the file extension Similar to QFileInfo::suffix except it takes into account UDS_DISPLAY_NAME...
The FileCopyJob copies data from one place to another.
The base class for all jobs.
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.
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
A KIO job that retrieves information about a file or directory.
const UDSEntry & statResult() const
Result of the stat operation.
Definition statjob.cpp:80
The transfer job pumps data into and/or out of a KIO worker.
void data(KIO::Job *job, const QByteArray &data)
Data from the worker has arrived.
Universal Directory Service.
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)
bool kill(KJob::KillVerbosity verbosity=KJob::Quietly)
QString pluginId() const
QStringList mimeTypes() const
bool value(const QString &key, bool defaultValue) const
QString fileName() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
QString name() const
bool isValid() const
bool supportsMimeType(const QString &mimeType) 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()
bool isEncrypted() const
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()
bool isEmpty() const const
QDateTime fromSecsSinceEpoch(qint64 secs)
qint64 toSecsSinceEpoch() const const
bool mkpath(const QString &dirPath) const const
bool remove()
QString baseName() const const
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)
void clear()
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 end()
iterator find(const Key &key)
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
QString join(QChar separator) 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-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.