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 "statjob.h"
15
16#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
17#define WITH_SHM 1
18#else
19#define WITH_SHM 0
20#endif
21
22#if WITH_SHM
23#include <sys/ipc.h>
24#include <sys/shm.h>
25#endif
26
27#include <algorithm>
28#include <limits>
29
30#include <QDir>
31#include <QFile>
32#include <QImage>
33#include <QPixmap>
34#include <QRegularExpression>
35#include <QSaveFile>
36#include <QTemporaryFile>
37#include <QTimer>
38
39#include <QCryptographicHash>
40
41#include <KConfigGroup>
42#include <KMountPoint>
43#include <KPluginMetaData>
44#include <KService>
45#include <KSharedConfig>
46#include <QMimeDatabase>
47#include <QStandardPaths>
48#include <Solid/Device>
49#include <Solid/StorageAccess>
50#include <kprotocolinfo.h>
51
52#include "job_p.h"
53
54namespace
55{
56static qreal s_defaultDevicePixelRatio = 1.0;
57}
58
59namespace KIO
60{
61struct PreviewItem;
62}
63using namespace KIO;
64
65struct KIO::PreviewItem {
66 KFileItem item;
67 KPluginMetaData plugin;
68};
69
70class KIO::PreviewJobPrivate : public KIO::JobPrivate
71{
72public:
73 PreviewJobPrivate(const KFileItemList &items, const QSize &size)
74 : initialItems(items)
75 , width(size.width())
76 , height(size.height())
77 , cacheSize(0)
78 , bScale(true)
79 , bSave(true)
80 , ignoreMaximumSize(false)
81 , sequenceIndex(0)
82 , succeeded(false)
83 , maximumLocalSize(0)
84 , maximumRemoteSize(0)
85 , enableRemoteFolderThumbnail(false)
86 , shmid(-1)
87 , shmaddr(nullptr)
88 {
89 // https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
91 }
92
93 enum {
94 STATE_STATORIG, // if the thumbnail exists
95 STATE_GETORIG, // if we create it
96 STATE_CREATETHUMB, // thumbnail:/ worker
97 STATE_DEVICE_INFO, // additional state check to get needed device ids
98 } state;
99
100 KFileItemList initialItems;
101 QStringList enabledPlugins;
102 // Some plugins support remote URLs, <protocol, mimetypes>
103 QHash<QString, QStringList> m_remoteProtocolPlugins;
104 // Our todo list :)
105 // We remove the first item at every step, so use std::list
106 std::list<PreviewItem> items;
107 // The current item
108 PreviewItem currentItem;
109 // The modification time of that URL
110 QDateTime tOrig;
111 // Path to thumbnail cache for the current size
112 QString thumbPath;
113 // Original URL of current item in RFC2396 format
114 // (file:///path/to/a%20file instead of file:/path/to/a file)
115 QByteArray origName;
116 // Thumbnail file name for current item
117 QString thumbName;
118 // Size of thumbnail
119 int width;
120 int height;
121 // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled)
122 short cacheSize;
123 // Whether the thumbnail should be scaled
124 bool bScale;
125 // Whether we should save the thumbnail
126 bool bSave;
127 bool ignoreMaximumSize;
128 int sequenceIndex;
129 bool succeeded;
130 // If the file to create a thumb for was a temp file, this is its name
131 QString tempName;
132 KIO::filesize_t maximumLocalSize;
133 KIO::filesize_t maximumRemoteSize;
134 // Manage preview for locally mounted remote directories
135 bool enableRemoteFolderThumbnail;
136 // Shared memory segment Id. The segment is allocated to a size
137 // of extent x extent x 4 (32 bit image) on first need.
138 int shmid;
139 // And the data area
140 uchar *shmaddr;
141 // Size of the shm segment
142 size_t shmsize;
143 // Root of thumbnail cache
144 QString thumbRoot;
145 // Metadata returned from the KIO thumbnail worker
146 QMap<QString, QString> thumbnailWorkerMetaData;
147 qreal devicePixelRatio = s_defaultDevicePixelRatio;
148 static const int idUnknown = -1;
149 // Id of a device storing currently processed file
150 int currentDeviceId = 0;
151 // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on.
152 QMap<QString, int> deviceIdMap;
153 enum CachePolicy { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown;
154
155 void getOrCreateThumbnail();
156 bool statResultThumbnail();
157 void createThumbnail(const QString &);
158 void cleanupTempFile();
159 void determineNextFile();
160 void emitPreview(const QImage &thumb);
161
162 void startPreview();
163 void slotThumbData(KIO::Job *, const QByteArray &);
164 // Checks if thumbnail is on encrypted partition different than thumbRoot
165 CachePolicy canBeCached(const QString &path);
166 int getDeviceId(const QString &path);
167
168 Q_DECLARE_PUBLIC(PreviewJob)
169
170 static QList<KPluginMetaData> loadAvailablePlugins()
171 {
172 static QList<KPluginMetaData> jsonMetaDataPlugins;
173 if (jsonMetaDataPlugins.isEmpty()) {
174 jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/thumbcreator"));
175 }
176 return jsonMetaDataPlugins;
177 }
178};
179
180void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
181{
182 s_defaultDevicePixelRatio = defaultDevicePixelRatio;
183}
184
185PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
186 : KIO::Job(*new PreviewJobPrivate(items, size))
187{
189
190 if (enabledPlugins) {
191 d->enabledPlugins = *enabledPlugins;
192 } else {
193 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
194 d->enabledPlugins =
195 globalConfig.readEntry("Plugins",
196 QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
197 }
198
199 // Return to event loop first, determineNextFile() might delete this;
200 QTimer::singleShot(0, this, [d]() {
201 d->startPreview();
202 });
203}
204
205PreviewJob::~PreviewJob()
206{
207#if WITH_SHM
209 if (d->shmaddr) {
210 shmdt((char *)d->shmaddr);
211 shmctl(d->shmid, IPC_RMID, nullptr);
212 }
213#endif
214}
215
217{
219 switch (type) {
220 case Unscaled:
221 d->bScale = false;
222 d->bSave = false;
223 break;
224 case Scaled:
225 d->bScale = true;
226 d->bSave = false;
227 break;
228 case ScaledAndCached:
229 d->bScale = true;
230 d->bSave = true;
231 break;
232 default:
233 break;
234 }
235}
236
238{
239 Q_D(const PreviewJob);
240 if (d->bScale) {
241 return d->bSave ? ScaledAndCached : Scaled;
242 }
243 return Unscaled;
244}
245
246void PreviewJobPrivate::startPreview()
247{
248 Q_Q(PreviewJob);
249 // Load the list of plugins to determine which MIME types are supported
250 const QList<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
253
254 for (const KPluginMetaData &plugin : plugins) {
255 QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols"), QStringList());
256 const QString p = plugin.value(QStringLiteral("X-KDE-Protocol"));
257 if (!p.isEmpty()) {
258 protocols.append(p);
259 }
260 for (const QString &protocol : std::as_const(protocols)) {
261 // Add supported MIME type for this protocol
262 QStringList &_ms = m_remoteProtocolPlugins[protocol];
263 const auto mimeTypes = plugin.mimeTypes();
264 for (const QString &_m : mimeTypes) {
265 protocolMap[protocol].insert(_m, plugin);
266 if (!_ms.contains(_m)) {
267 _ms.append(_m);
268 }
269 }
270 }
271 if (enabledPlugins.contains(plugin.pluginId())) {
272 const auto mimeTypes = plugin.mimeTypes();
273 for (const QString &mimeType : mimeTypes) {
274 mimeMap.insert(mimeType, plugin);
275 }
276 }
277 }
278
279 // Look for images and store the items in our todo list :)
280 bool bNeedCache = false;
281 for (const auto &fileItem : std::as_const(initialItems)) {
282 PreviewItem item;
283 item.item = fileItem;
284
285 const QString mimeType = item.item.mimetype();
286 KPluginMetaData plugin;
287
288 // look for protocol-specific thumbnail plugins first
289 auto it = protocolMap.constFind(item.item.url().scheme());
290 if (it != protocolMap.constEnd()) {
291 plugin = it.value().value(mimeType);
292 }
293
294 if (!plugin.isValid()) {
295 auto pluginIt = mimeMap.constFind(mimeType);
296 if (pluginIt == mimeMap.constEnd()) {
297 // check MIME type inheritance, resolve aliases
298 QMimeDatabase db;
299 const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
300 if (mimeInfo.isValid()) {
301 const QStringList parentMimeTypes = mimeInfo.allAncestors();
302 for (const QString &parentMimeType : parentMimeTypes) {
303 pluginIt = mimeMap.constFind(parentMimeType);
304 if (pluginIt != mimeMap.constEnd()) {
305 break;
306 }
307 }
308 }
309
310 if (pluginIt == mimeMap.constEnd()) {
311 // Check the wildcards last, see BUG 453480
312 QString groupMimeType = mimeType;
313 const int slashIdx = groupMimeType.indexOf(QLatin1Char('/'));
314 if (slashIdx != -1) {
315 // Replace everything after '/' with '*'
316 groupMimeType.truncate(slashIdx + 1);
317 groupMimeType += QLatin1Char('*');
318 }
319 pluginIt = mimeMap.constFind(groupMimeType);
320 }
321 }
322
323 if (pluginIt != mimeMap.constEnd()) {
324 plugin = *pluginIt;
325 }
326 }
327
328 if (plugin.isValid()) {
329 item.plugin = plugin;
330 items.push_back(item);
331 if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
332 const QUrl url = fileItem.targetUrl();
333 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
334 bNeedCache = true;
335 }
336 }
337 } else {
338 Q_EMIT q->failed(fileItem);
339 }
340 }
341
342 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
343 maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
344 maximumRemoteSize = cg.readEntry<KIO::filesize_t>("MaximumRemoteSize", 0);
345 enableRemoteFolderThumbnail = cg.readEntry("EnableRemoteFolderThumbnail", false);
346
347 if (bNeedCache) {
348 const int longer = std::max(width, height);
349 if (longer <= 128) {
350 cacheSize = 128;
351 } else if (longer <= 256) {
352 cacheSize = 256;
353 } else if (longer <= 512) {
354 cacheSize = 512;
355 } else {
356 cacheSize = 1024;
357 }
358
359 struct CachePool {
361 int minSize;
362 };
363
364 const static auto pools = {
365 CachePool{QStringLiteral("/normal/"), 128},
366 CachePool{QStringLiteral("/large/"), 256},
367 CachePool{QStringLiteral("/x-large/"), 512},
368 CachePool{QStringLiteral("/xx-large/"), 1024},
369 };
370
371 QString thumbDir;
372 int wants = devicePixelRatio * cacheSize;
373 for (const auto &p : pools) {
374 if (p.minSize < wants) {
375 continue;
376 } else {
377 thumbDir = p.path;
378 break;
379 }
380 }
381 thumbPath = thumbRoot + thumbDir;
382
383 if (!QDir(thumbPath).exists()) {
384 if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions)
385 QFile f(thumbPath);
386 f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700
387 }
388 }
389 } else {
390 bSave = false;
391 }
392
393 initialItems.clear();
394 determineNextFile();
395}
396
398{
400
401 auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
402 return url == pItem.item.url();
403 });
404 if (it != d->items.cend()) {
405 d->items.erase(it);
406 }
407
408 if (d->currentItem.item.url() == url) {
409 KJob *job = subjobs().first();
410 job->kill();
411 removeSubjob(job);
412 d->determineNextFile();
413 }
414}
415
417{
418 d_func()->sequenceIndex = index;
419}
420
422{
423 return d_func()->sequenceIndex;
424}
425
427{
428 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
429}
430
432{
433 return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
434}
435
437{
438 d_func()->devicePixelRatio = dpr;
439}
440
442{
443 d_func()->ignoreMaximumSize = ignoreSize;
444}
445
446void PreviewJobPrivate::cleanupTempFile()
447{
448 if (!tempName.isEmpty()) {
449 Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
450 QFile::remove(tempName);
451 tempName.clear();
452 }
453}
454
455void PreviewJobPrivate::determineNextFile()
456{
457 Q_Q(PreviewJob);
458 if (!currentItem.item.isNull()) {
459 if (!succeeded) {
460 Q_EMIT q->failed(currentItem.item);
461 }
462 }
463 // No more items ?
464 if (items.empty()) {
465 q->emitResult();
466 return;
467 } else {
468 // First, stat the orig file
469 state = PreviewJobPrivate::STATE_STATORIG;
470 currentItem = items.front();
471 items.pop_front();
472 succeeded = false;
473 KIO::Job *job = KIO::stat(currentItem.item.targetUrl(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
474 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
475 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
476 q->addSubjob(job);
477 }
478}
479
480void PreviewJob::slotResult(KJob *job)
481{
483
484 removeSubjob(job);
485 Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
486 switch (d->state) {
487 case PreviewJobPrivate::STATE_STATORIG: {
488 if (job->error()) { // that's no good news...
489 // Drop this one and move on to the next one
490 d->determineNextFile();
491 return;
492 }
493 const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
494 d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
496
497 bool skipCurrentItem = false;
499 const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
500
501 if ((itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) && !d->currentItem.item.isSlow()) {
502 skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
503 } else {
504 // For remote items the "IgnoreMaximumSize" plugin property is not respected
505 // Also we need to check if remote (but locally mounted) folder preview is enabled
506 skipCurrentItem = (!d->ignoreMaximumSize && size > d->maximumRemoteSize) || (d->currentItem.item.isDir() && !d->enableRemoteFolderThumbnail);
507 }
508 if (skipCurrentItem) {
509 d->determineNextFile();
510 return;
511 }
512
513 bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
514 if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
515 // This preview will not be cached, no need to look for a saved thumbnail
516 // Just create it, and be done
517 d->getOrCreateThumbnail();
518 return;
519 }
520
521 if (d->statResultThumbnail()) {
522 return;
523 }
524
525 d->getOrCreateThumbnail();
526 return;
527 }
528 case PreviewJobPrivate::STATE_DEVICE_INFO: {
529 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
530 int id;
531 QString path = statJob->url().toLocalFile();
532 if (job->error()) {
533 // We set id to 0 to know we tried getting it
534 qCWarning(KIO_GUI) << "Cannot read information about filesystem under path" << path;
535 id = 0;
536 } else {
538 }
539 d->deviceIdMap[path] = id;
540 d->createThumbnail(d->currentItem.item.localPath());
541 return;
542 }
543 case PreviewJobPrivate::STATE_GETORIG: {
544 if (job->error()) {
545 d->cleanupTempFile();
546 d->determineNextFile();
547 return;
548 }
549
550 d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
551 return;
552 }
553 case PreviewJobPrivate::STATE_CREATETHUMB: {
554 d->cleanupTempFile();
555 d->determineNextFile();
556 return;
557 }
558 }
559}
560
561bool PreviewJobPrivate::statResultThumbnail()
562{
563 if (thumbPath.isEmpty()) {
564 return false;
565 }
566
567 bool isLocal;
568 const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
569 if (isLocal) {
570 const QFileInfo localFile(url.toLocalFile());
571 const QString canonicalPath = localFile.canonicalFilePath();
573 if (origName.isEmpty()) {
574 qCWarning(KIO_GUI) << "Failed to convert" << url << "to canonical path";
575 return false;
576 }
577 } else {
578 // Don't include the password if any
579 origName = currentItem.item.targetUrl().toEncoded(QUrl::RemovePassword);
580 }
581
583 md5.addData(origName);
584 thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
585
586 QImage thumb;
587 QFile thumbFile(thumbPath + thumbName);
588 if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
589 return false;
590 }
591
592 if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
593 || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
594 return false;
595 }
596
597 const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
598 if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
599 // Thumb::Size is not required, but if it is set it should match
600 return false;
601 }
602
603 // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
604 // When a thumbnail is DPR-invariant, use the DPR passed in the request.
605 thumb.setDevicePixelRatio(devicePixelRatio);
606
607 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
608
609 if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
610 // Check if the version matches
611 // The software string should read "KDE Thumbnail Generator pluginName (vX)"
612 QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
613 if (softwareString.isEmpty()) {
614 // The thumbnail has been created with an older version, recreating
615 return false;
616 }
617 int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
618 if (versionIndex < 0) {
619 return false;
620 }
621
622 QString cachedVersion = softwareString.remove(0, versionIndex + 2);
623 cachedVersion.chop(1);
624 uint thumbnailerMajor = thumbnailerVersion.toInt();
625 uint cachedMajor = cachedVersion.toInt();
626 if (thumbnailerMajor > cachedMajor) {
627 return false;
628 }
629 }
630
631 // Found it, use it
632 emitPreview(thumb);
633 succeeded = true;
634 determineNextFile();
635 return true;
636}
637
638void PreviewJobPrivate::getOrCreateThumbnail()
639{
640 Q_Q(PreviewJob);
641 // We still need to load the orig file ! (This is getting tedious) :)
642 const KFileItem &item = currentItem.item;
643 const QString localPath = item.localPath();
644 if (!localPath.isEmpty()) {
645 createThumbnail(localPath);
646 return;
647 }
648
649 // heuristics for remote URL support
650 const QUrl fileUrl = item.targetUrl();
651 bool supportsProtocol = false;
652 if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) {
653 // There's a plugin supporting this protocol and MIME type
654 supportsProtocol = true;
655 } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) {
656 // Assume KIO understands any URL, ThumbCreator workers who have
657 // X-KDE-Protocols=KIO will get fed the remote URL directly.
658 supportsProtocol = true;
659 }
660
661 if (supportsProtocol) {
662 createThumbnail(fileUrl.toString());
663 return;
664 }
665 if (item.isDir()) {
666 // Skip remote dirs (bug 208625)
667 cleanupTempFile();
668 determineNextFile();
669 return;
670 }
671 // No plugin support access to this remote content, copy the file
672 // to the local machine, then create the thumbnail
673 state = PreviewJobPrivate::STATE_GETORIG;
674 QTemporaryFile localFile;
675 localFile.setAutoRemove(false);
676 localFile.open();
677 tempName = localFile.fileName();
678 const QUrl currentURL = item.mostLocalUrl();
679 KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
680 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
681 q->addSubjob(job);
682}
683
684PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
685{
686 // If checked file is directory on a different filesystem than its parent, we need to check it separately
687 int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
688 // special case for root folders
689 const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex);
690
691 int parentId = getDeviceId(parentDirPath);
692 if (parentId == idUnknown) {
693 return CachePolicy::Unknown;
694 }
695
696 bool isDifferentSystem = !parentId || parentId != currentDeviceId;
697 if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
698 return currentDeviceCachePolicy;
699 }
700 int checkedId;
701 QString checkedPath;
702 if (isDifferentSystem) {
703 checkedId = currentDeviceId;
704 checkedPath = path;
705 } else {
706 checkedId = getDeviceId(parentDirPath);
707 checkedPath = parentDirPath;
708 if (checkedId == idUnknown) {
709 return CachePolicy::Unknown;
710 }
711 }
712 // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
713 int thumbRootId = getDeviceId(thumbRoot);
714 if (thumbRootId == idUnknown) {
715 return CachePolicy::Unknown;
716 }
717 bool shouldAllow = checkedId && checkedId == thumbRootId;
718 if (!shouldAllow) {
720 if (device.isValid()) {
721 // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location.
722 // Or, if the checked device is unencrypted, allow thumbnailing.
723 if (device.as<Solid::StorageAccess>()->isEncrypted()) {
724 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot);
725 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted();
726 } else {
727 shouldAllow = true;
728 }
729 }
730 }
731 if (!isDifferentSystem) {
732 currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
733 }
734 return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
735}
736
737int PreviewJobPrivate::getDeviceId(const QString &path)
738{
739 Q_Q(PreviewJob);
740 auto iter = deviceIdMap.find(path);
741 if (iter != deviceIdMap.end()) {
742 return iter.value();
743 }
744 QUrl url = QUrl::fromLocalFile(path);
745 if (!url.isValid()) {
746 qCWarning(KIO_GUI) << "Could not get device id for file preview, Invalid url" << path;
747 return 0;
748 }
749 state = PreviewJobPrivate::STATE_DEVICE_INFO;
751 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
752 q->addSubjob(job);
753
754 return idUnknown;
755}
756
757void PreviewJobPrivate::createThumbnail(const QString &pixPath)
758{
759 Q_Q(PreviewJob);
760 state = PreviewJobPrivate::STATE_CREATETHUMB;
761 QUrl thumbURL;
762 thumbURL.setScheme(QStringLiteral("thumbnail"));
763 thumbURL.setPath(pixPath);
764
765 bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
766
767 bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
768 CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
769
770 if (cachePolicy == CachePolicy::Unknown) {
771 // If Unknown is returned, creating thumbnail should be called again by slotResult
772 return;
773 }
774
775 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
776 q->addSubjob(job);
777 q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
778 slotThumbData(job, data);
779 });
780
781 int thumb_width = width;
782 int thumb_height = height;
783 if (save) {
784 thumb_width = thumb_height = cacheSize;
785 }
786
787 job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
788 job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
789 job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
790 job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
791 job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
792 job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
793 job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
794 if (sequenceIndex) {
795 job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
796 }
797
798#if WITH_SHM
799 size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
800 if (shmid == -1 || shmsize < requiredSize) {
801 if (shmaddr) {
802 // clean previous shared memory segment
803 shmdt((char *)shmaddr);
804 shmaddr = nullptr;
805 shmctl(shmid, IPC_RMID, nullptr);
806 shmid = -1;
807 }
808 if (requiredSize > 0) {
809 shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
810 if (shmid != -1) {
811 shmsize = requiredSize;
812 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
813 if (shmaddr == (uchar *)-1) {
814 shmctl(shmid, IPC_RMID, nullptr);
815 shmaddr = nullptr;
816 shmid = -1;
817 }
818 }
819 }
820 }
821 if (shmid != -1) {
822 job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
823 }
824#endif
825}
826
827void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
828{
829 thumbnailWorkerMetaData = job->metaData();
830 /* clang-format off */
831 const bool save = bSave
832 && !sequenceIndex
833 && currentDeviceCachePolicy == CachePolicy::Allow
834 && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
835 && (!currentItem.item.targetUrl().isLocalFile()
836 || !currentItem.item.targetUrl().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
837 /* clang-format on */
838
839 QImage thumb;
840 // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp
841 QDataStream str(data);
842 int width;
843 int height;
844 QImage::Format format;
845 qreal imgDevicePixelRatio;
846 // TODO KF6: add a version number as first parameter
847 str >> width >> height >> format >> imgDevicePixelRatio;
848#if WITH_SHM
849 if (shmaddr != nullptr) {
850 thumb = QImage(shmaddr, width, height, format).copy();
851 } else {
852#endif
853 str >> thumb;
854#if WITH_SHM
855 }
856#endif
857 thumb.setDevicePixelRatio(imgDevicePixelRatio);
858
859 if (thumb.isNull()) {
860 QDataStream s(data);
861 s >> thumb;
862 }
863
864 if (thumb.isNull()) {
865 // let succeeded in false state
866 // failed will get called in determineNextFile()
867 return;
868 }
869
870 if (save) {
871 thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
872 thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
873 thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
874 thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
875 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
876 QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
877 if (!thumbnailerVersion.isEmpty()) {
878 signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
879 }
880 thumb.setText(QStringLiteral("Software"), signature);
881 QSaveFile saveFile(thumbPath + thumbName);
882 if (saveFile.open(QIODevice::WriteOnly)) {
883 if (thumb.save(&saveFile, "PNG")) {
884 saveFile.commit();
885 }
886 }
887 }
888 emitPreview(thumb);
889 succeeded = true;
890}
891
892void PreviewJobPrivate::emitPreview(const QImage &thumb)
893{
894 Q_Q(PreviewJob);
895 QPixmap pix;
896 const qreal ratio = thumb.devicePixelRatio();
897 if (thumb.width() > width * ratio || thumb.height() > height * ratio) {
898 pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation));
899 } else {
900 pix = QPixmap::fromImage(thumb);
901 }
902 pix.setDevicePixelRatio(ratio);
903 Q_EMIT q->gotPreview(currentItem.item, pix);
904}
905
907{
908 return PreviewJobPrivate::loadAvailablePlugins();
909}
910
912{
914 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
915 for (const KPluginMetaData &plugin : plugins) {
916 result << plugin.pluginId();
917 }
918 return result;
919}
920
922{
923 const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
924
926 for (const QString &plugin : blacklist) {
928 }
929
930 return defaultPlugins;
931}
932
934{
936 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
937 for (const KPluginMetaData &plugin : plugins) {
938 result += plugin.mimeTypes();
939 }
940 return result;
941}
942
943PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
944{
945 return new PreviewJob(items, size, enabledPlugins);
946}
947
948#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.
The FileCopyJob copies data from one place to another.
Definition filecopyjob.h:26
The base class for all jobs.
Definition job_base.h:45
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
A KIO job that retrieves information about a file or directory.
Definition statjob.h:26
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.
Definition transferjob.h:26
void data(KIO::Job *job, const QByteArray &data)
Data from the worker has arrived.
Universal Directory Service.
Definition udsentry.h:78
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
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()
A namespace for KIO globals.
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 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:271
@ StatInode
dev, inode
Definition global.h:261
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
QString path(const QString &relativePath)
const QList< QKeySequence > & save()
bool isEmpty() const const
QDateTime fromSecsSinceEpoch(qint64 secs)
qint64 toSecsSinceEpoch() const const
bool remove()
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) 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
void append(QList< T > &&value)
void clear()
T & first()
bool isEmpty() const const
qsizetype removeAll(const AT &t)
T value(qsizetype i) const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) 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)
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
void setAutoRemove(bool b)
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
QString toString(FormattingOptions options) const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Apr 27 2024 22:13:16 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.