KIO

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

KDE's Doxygen guidelines are available online.