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
676 // Some thumbnailers, like libkdcraw, depend on the file extension being
677 // correct
678 const QString extension = item.suffix();
679 if (!extension.isEmpty()) {
680 localFile.setFileTemplate(QStringLiteral("%1.%2").arg(localFile.fileTemplate(), extension));
681 }
682
683 localFile.setAutoRemove(false);
684 localFile.open();
685 tempName = localFile.fileName();
686 const QUrl currentURL = item.mostLocalUrl();
687 KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
688 job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
689 q->addSubjob(job);
690}
691
692PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
693{
694 // If checked file is directory on a different filesystem than its parent, we need to check it separately
695 int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
696 // special case for root folders
697 const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex);
698
699 int parentId = getDeviceId(parentDirPath);
700 if (parentId == idUnknown) {
701 return CachePolicy::Unknown;
702 }
703
704 bool isDifferentSystem = !parentId || parentId != currentDeviceId;
705 if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
706 return currentDeviceCachePolicy;
707 }
708 int checkedId;
709 QString checkedPath;
710 if (isDifferentSystem) {
711 checkedId = currentDeviceId;
712 checkedPath = path;
713 } else {
714 checkedId = getDeviceId(parentDirPath);
715 checkedPath = parentDirPath;
716 if (checkedId == idUnknown) {
717 return CachePolicy::Unknown;
718 }
719 }
720 // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
721 int thumbRootId = getDeviceId(thumbRoot);
722 if (thumbRootId == idUnknown) {
723 return CachePolicy::Unknown;
724 }
725 bool shouldAllow = checkedId && checkedId == thumbRootId;
726 if (!shouldAllow) {
728 if (device.isValid()) {
729 // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location.
730 // Or, if the checked device is unencrypted, allow thumbnailing.
731 if (device.as<Solid::StorageAccess>()->isEncrypted()) {
732 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot);
733 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted();
734 } else {
735 shouldAllow = true;
736 }
737 }
738 }
739 if (!isDifferentSystem) {
740 currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
741 }
742 return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
743}
744
745int PreviewJobPrivate::getDeviceId(const QString &path)
746{
747 Q_Q(PreviewJob);
748 auto iter = deviceIdMap.find(path);
749 if (iter != deviceIdMap.end()) {
750 return iter.value();
751 }
752 QUrl url = QUrl::fromLocalFile(path);
753 if (!url.isValid()) {
754 qCWarning(KIO_GUI) << "Could not get device id for file preview, Invalid url" << path;
755 return 0;
756 }
757 state = PreviewJobPrivate::STATE_DEVICE_INFO;
759 job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
760 q->addSubjob(job);
761
762 return idUnknown;
763}
764
765void PreviewJobPrivate::createThumbnail(const QString &pixPath)
766{
767 Q_Q(PreviewJob);
768 state = PreviewJobPrivate::STATE_CREATETHUMB;
769 QUrl thumbURL;
770 thumbURL.setScheme(QStringLiteral("thumbnail"));
771 thumbURL.setPath(pixPath);
772
773 bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
774
775 bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
776 CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
777
778 if (cachePolicy == CachePolicy::Unknown) {
779 // If Unknown is returned, creating thumbnail should be called again by slotResult
780 return;
781 }
782
783 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
784 q->addSubjob(job);
785 q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
786 slotThumbData(job, data);
787 });
788
789 int thumb_width = width;
790 int thumb_height = height;
791 if (save) {
792 thumb_width = thumb_height = cacheSize;
793 }
794
795 job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
796 job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
797 job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
798 job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
799 job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
800 job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
801 job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
802 if (sequenceIndex) {
803 job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
804 }
805
806#if WITH_SHM
807 size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
808 if (shmid == -1 || shmsize < requiredSize) {
809 if (shmaddr) {
810 // clean previous shared memory segment
811 shmdt((char *)shmaddr);
812 shmaddr = nullptr;
813 shmctl(shmid, IPC_RMID, nullptr);
814 shmid = -1;
815 }
816 if (requiredSize > 0) {
817 shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
818 if (shmid != -1) {
819 shmsize = requiredSize;
820 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
821 if (shmaddr == (uchar *)-1) {
822 shmctl(shmid, IPC_RMID, nullptr);
823 shmaddr = nullptr;
824 shmid = -1;
825 }
826 }
827 }
828 }
829 if (shmid != -1) {
830 job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
831 }
832#endif
833}
834
835void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
836{
837 thumbnailWorkerMetaData = job->metaData();
838 /* clang-format off */
839 const bool save = bSave
840 && !sequenceIndex
841 && currentDeviceCachePolicy == CachePolicy::Allow
842 && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
843 && (!currentItem.item.targetUrl().isLocalFile()
844 || !currentItem.item.targetUrl().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
845 /* clang-format on */
846
847 QImage thumb;
848 // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp
849 QDataStream str(data);
850 int width;
851 int height;
852 QImage::Format format;
853 qreal imgDevicePixelRatio;
854 // TODO KF6: add a version number as first parameter
855 str >> width >> height >> format >> imgDevicePixelRatio;
856#if WITH_SHM
857 if (shmaddr != nullptr) {
858 thumb = QImage(shmaddr, width, height, format).copy();
859 } else {
860#endif
861 str >> thumb;
862#if WITH_SHM
863 }
864#endif
865 thumb.setDevicePixelRatio(imgDevicePixelRatio);
866
867 if (thumb.isNull()) {
868 QDataStream s(data);
869 s >> thumb;
870 }
871
872 if (thumb.isNull()) {
873 // let succeeded in false state
874 // failed will get called in determineNextFile()
875 return;
876 }
877
878 if (save) {
879 thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
880 thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
881 thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
882 thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
883 QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
884 QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
885 if (!thumbnailerVersion.isEmpty()) {
886 signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
887 }
888 thumb.setText(QStringLiteral("Software"), signature);
889 QSaveFile saveFile(thumbPath + thumbName);
890 if (saveFile.open(QIODevice::WriteOnly)) {
891 if (thumb.save(&saveFile, "PNG")) {
892 saveFile.commit();
893 }
894 }
895 }
896 emitPreview(thumb);
897 succeeded = true;
898}
899
900void PreviewJobPrivate::emitPreview(const QImage &thumb)
901{
902 Q_Q(PreviewJob);
903 QPixmap pix;
904 const qreal ratio = thumb.devicePixelRatio();
905 if (thumb.width() > width * ratio || thumb.height() > height * ratio) {
906 pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation));
907 } else {
908 pix = QPixmap::fromImage(thumb);
909 }
910 pix.setDevicePixelRatio(ratio);
911 Q_EMIT q->gotPreview(currentItem.item, pix);
912}
913
915{
916 return PreviewJobPrivate::loadAvailablePlugins();
917}
918
920{
922 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
923 for (const KPluginMetaData &plugin : plugins) {
924 result << plugin.pluginId();
925 }
926 return result;
927}
928
930{
931 const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
932
934 for (const QString &plugin : blacklist) {
936 }
937
938 return defaultPlugins;
939}
940
942{
944 const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
945 for (const KPluginMetaData &plugin : plugins) {
946 result += plugin.mimeTypes();
947 }
948 return result;
949}
950
951PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
952{
953 return new PreviewJob(items, size, enabledPlugins);
954}
955
956#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
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)
KGuiItem 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
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
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 Fri Jul 26 2024 11:54:08 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.