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

KDE's Doxygen guidelines are available online.