KIO

previewjob.cpp
1 // -*- c++ -*-
2 /*
3  This file is part of the KDE libraries
4  SPDX-FileCopyrightText: 2000 David Faure <[email protected]>
5  SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <[email protected]>
6  SPDX-FileCopyrightText: 2001 Malte Starostik <[email protected]>
7 
8  SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "previewjob.h"
12 #include "kio_widgets_debug.h"
13 
14 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
15 #define WITH_SHM 1
16 #else
17 #define WITH_SHM 0
18 #endif
19 
20 #if WITH_SHM
21 #include <sys/ipc.h>
22 #include <sys/shm.h>
23 #endif
24 
25 #include <limits>
26 #include <set>
27 
28 #include <QDir>
29 #include <QFile>
30 #include <QImage>
31 #include <QPixmap>
32 #include <QRegularExpression>
33 #include <QSaveFile>
34 #include <QTemporaryFile>
35 #include <QTimer>
36 
37 #include <QCryptographicHash>
38 
39 #include <KConfigGroup>
40 #include <KMountPoint>
41 #include <KPluginInfo>
42 #include <KService>
43 #include <KServiceTypeTrader>
44 #include <KSharedConfig>
45 #include <QMimeDatabase>
46 #include <QStandardPaths>
47 #include <Solid/Device>
48 #include <Solid/StorageAccess>
49 #include <kprotocolinfo.h>
50 
51 #include <algorithm>
52 #include <cmath>
53 
54 #include "job_p.h"
55 
56 namespace {
57  static int s_defaultDevicePixelRatio = 1;
58 }
59 
60 namespace KIO
61 {
62 struct PreviewItem;
63 }
64 using namespace KIO;
65 
66 struct KIO::PreviewItem {
67  KFileItem item;
68  KPluginMetaData plugin;
69 };
70 
71 class KIO::PreviewJobPrivate : public KIO::JobPrivate
72 {
73 public:
74  PreviewJobPrivate(const KFileItemList &items, const QSize &size)
75  : initialItems(items)
76  , width(size.width())
77  , height(size.height())
78  , cacheSize(0)
79  , bScale(true)
80  , bSave(true)
81  , ignoreMaximumSize(false)
82  , sequenceIndex(0)
83  , succeeded(false)
84  , maximumLocalSize(0)
85  , maximumRemoteSize(0)
86  , iconSize(0)
87  , iconAlpha(70)
88  , shmid(-1)
89  , shmaddr(nullptr)
90  {
91  // http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
93  }
94 
95  enum {
96  STATE_STATORIG, // if the thumbnail exists
97  STATE_GETORIG, // if we create it
98  STATE_CREATETHUMB, // thumbnail:/ slave
99  STATE_DEVICE_INFO, // additional state check to get needed device ids
100  } state;
101 
102  KFileItemList initialItems;
103  QStringList enabledPlugins;
104  // Some plugins support remote URLs, <protocol, mimetypes>
105  QHash<QString, QStringList> m_remoteProtocolPlugins;
106  // Our todo list :)
107  // We remove the first item at every step, so use std::list
108  std::list<PreviewItem> items;
109  // The current item
110  PreviewItem currentItem;
111  // The modification time of that URL
112  QDateTime tOrig;
113  // Path to thumbnail cache for the current size
114  QString thumbPath;
115  // Original URL of current item in RFC2396 format
116  // (file:///path/to/a%20file instead of file:/path/to/a file)
117  QByteArray origName;
118  // Thumbnail file name for current item
119  QString thumbName;
120  // Size of thumbnail
121  int width;
122  int height;
123  // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled)
124  short cacheSize;
125  // Whether the thumbnail should be scaled
126  bool bScale;
127  // Whether we should save the thumbnail
128  bool bSave;
129  bool ignoreMaximumSize;
130  int sequenceIndex;
131  bool succeeded;
132  // If the file to create a thumb for was a temp file, this is its name
133  QString tempName;
134  KIO::filesize_t maximumLocalSize;
135  KIO::filesize_t maximumRemoteSize;
136  // the size for the icon overlay
137  int iconSize;
138  // the transparency of the blended MIME type icon
139  int iconAlpha;
140  // Shared memory segment Id. The segment is allocated to a size
141  // of extent x extent x 4 (32 bit image) on first need.
142  int shmid;
143  // And the data area
144  uchar *shmaddr;
145  // Size of the shm segment
146  size_t shmsize;
147  // Root of thumbnail cache
148  QString thumbRoot;
149  // Metadata returned from the KIO thumbnail slave
150  QMap<QString, QString> thumbnailSlaveMetaData;
151  int devicePixelRatio = s_defaultDevicePixelRatio;
152  static const int idUnknown = -1;
153  // Id of a device storing currently processed file
154  int currentDeviceId = 0;
155  // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on.
156  QMap<QString, int> deviceIdMap;
157  enum CachePolicy { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown;
158 
159  void getOrCreateThumbnail();
160  bool statResultThumbnail();
161  void createThumbnail(const QString &);
162  void cleanupTempFile();
163  void determineNextFile();
164  void emitPreview(const QImage &thumb);
165 
166  void startPreview();
167  void slotThumbData(KIO::Job *, const QByteArray &);
168  // Checks if thumbnail is on encrypted partition different than thumbRoot
169  CachePolicy canBeCached(const QString &path);
170  int getDeviceId(const QString &path);
171 
172  Q_DECLARE_PUBLIC(PreviewJob)
173 
174  static QVector<KPluginMetaData> loadAvailablePlugins()
175  {
176  static QVector<KPluginMetaData> jsonMetaDataPlugins;
177  if (!jsonMetaDataPlugins.isEmpty()) {
178  return jsonMetaDataPlugins;
179  }
180  jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf5/thumbcreator"));
181  std::set<QString> pluginIds;
182  for (const KPluginMetaData &data : std::as_const(jsonMetaDataPlugins)) {
183  pluginIds.insert(data.pluginId());
184  }
185  QT_WARNING_PUSH
186  QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
187  QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
188  const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator"));
189  for (const auto &plugin : plugins) {
190  if (KPluginInfo info(plugin); info.isValid()) {
191  if (auto [it, inserted] = pluginIds.insert(info.pluginName()); inserted) {
192  jsonMetaDataPlugins << info.toMetaData();
193  }
194  } else {
195  // Hack for directory thumbnailer: It has a hardcoded plugin id in the kio-slave and not any C++ plugin
196  // Consequently we just use the base name as the plugin file for our KPluginMetaData object
197  const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + plugin->entryPath());
199  jsonMetaDataPlugins << KPluginMetaData(tmpData.rawData(), QFileInfo(path).baseName(), path);
200  }
201  }
202  QT_WARNING_POP
203  return jsonMetaDataPlugins;
204  }
205 };
206 
207 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
208 void PreviewJob::setDefaultDevicePixelRatio(int defaultDevicePixelRatio)
209 {
210  s_defaultDevicePixelRatio = defaultDevicePixelRatio;
211 }
212 #endif
213 
214 void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
215 {
216  s_defaultDevicePixelRatio = std::ceil(defaultDevicePixelRatio);
217 }
218 
219 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
220 PreviewJob::PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
221  : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width)))
222 {
223  Q_D(PreviewJob);
224  d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins();
225  d->iconSize = iconSize;
226  d->iconAlpha = iconAlpha;
227  d->bScale = scale;
228  d->bSave = save && scale;
229 
230  // Return to event loop first, determineNextFile() might delete this;
231  QTimer::singleShot(0, this, SLOT(startPreview()));
232 }
233 #endif
234 
235 PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
236  : KIO::Job(*new PreviewJobPrivate(items, size))
237 {
238  Q_D(PreviewJob);
239 
240  if (enabledPlugins) {
241  d->enabledPlugins = *enabledPlugins;
242  } else {
243  const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
244  d->enabledPlugins =
245  globalConfig.readEntry("Plugins",
246  QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
247  }
248 
249  // Return to event loop first, determineNextFile() might delete this;
250  QTimer::singleShot(0, this, SLOT(startPreview()));
251 }
252 
253 PreviewJob::~PreviewJob()
254 {
255 #if WITH_SHM
256  Q_D(PreviewJob);
257  if (d->shmaddr) {
258  shmdt((char *)d->shmaddr);
259  shmctl(d->shmid, IPC_RMID, nullptr);
260  }
261 #endif
262 }
263 
265 {
266  Q_D(PreviewJob);
267  d->iconSize = size;
268 }
269 
271 {
272  Q_D(const PreviewJob);
273  return d->iconSize;
274 }
275 
277 {
278  Q_D(PreviewJob);
279  d->iconAlpha = qBound(0, alpha, 255);
280 }
281 
283 {
284  Q_D(const PreviewJob);
285  return d->iconAlpha;
286 }
287 
289 {
290  Q_D(PreviewJob);
291  switch (type) {
292  case Unscaled:
293  d->bScale = false;
294  d->bSave = false;
295  break;
296  case Scaled:
297  d->bScale = true;
298  d->bSave = false;
299  break;
300  case ScaledAndCached:
301  d->bScale = true;
302  d->bSave = true;
303  break;
304  default:
305  break;
306  }
307 }
308 
310 {
311  Q_D(const PreviewJob);
312  if (d->bScale) {
313  return d->bSave ? ScaledAndCached : Scaled;
314  }
315  return Unscaled;
316 }
317 
318 void PreviewJobPrivate::startPreview()
319 {
320  Q_Q(PreviewJob);
321  // Load the list of plugins to determine which MIME types are supported
322  const QVector<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
325 
326  for (const KPluginMetaData &plugin : plugins) {
327  QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols"), QStringList());
328  const QString p = plugin.value(QStringLiteral("X-KDE-Protocol"));
329  if (!p.isEmpty()) {
330  protocols.append(p);
331  }
332  for (const QString &protocol : std::as_const(protocols)) {
333  // Add supported MIME type for this protocol
334  QStringList &_ms = m_remoteProtocolPlugins[protocol];
335  const auto mimeTypes = plugin.mimeTypes();
336  for (const QString &_m : mimeTypes) {
337  protocolMap[protocol].insert(_m, plugin);
338  if (!_ms.contains(_m)) {
339  _ms.append(_m);
340  }
341  }
342  }
343  if (enabledPlugins.contains(plugin.pluginId())) {
344  const auto mimeTypes = plugin.mimeTypes();
345  for (const QString &mimeType : mimeTypes) {
346  mimeMap.insert(mimeType, plugin);
347  }
348  }
349  }
350 
351  // Look for images and store the items in our todo list :)
352  bool bNeedCache = false;
353  for (const auto &fileItem : std::as_const(initialItems)) {
354  PreviewItem item;
355  item.item = fileItem;
356 
357  const QString mimeType = item.item.mimetype();
358  KPluginMetaData plugin;
359 
360  // look for protocol-specific thumbnail plugins first
361  auto it = protocolMap.constFind(item.item.url().scheme());
362  if (it != protocolMap.constEnd()) {
363  plugin = it.value().value(mimeType);
364  }
365 
366  if (!plugin.isValid()) {
367  auto pluginIt = mimeMap.constFind(mimeType);
368  if (pluginIt == mimeMap.constEnd()) {
369  QString groupMimeType = mimeType;
370  groupMimeType.replace(QRegularExpression(QStringLiteral("/.*")), QStringLiteral("/*"));
371  pluginIt = mimeMap.constFind(groupMimeType);
372 
373  if (pluginIt == mimeMap.constEnd()) {
374  QMimeDatabase db;
375  // check MIME type inheritance, resolve aliases
376  const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
377  if (mimeInfo.isValid()) {
378  const QStringList parentMimeTypes = mimeInfo.allAncestors();
379  for (const QString &parentMimeType : parentMimeTypes) {
380  pluginIt = mimeMap.constFind(parentMimeType);
381  if (pluginIt != mimeMap.constEnd()) {
382  break;
383  }
384  }
385  }
386  }
387  }
388 
389  if (pluginIt != mimeMap.constEnd()) {
390  plugin = *pluginIt;
391  }
392  }
393 
394  if (plugin.isValid()) {
395  item.plugin = plugin;
396  items.push_back(item);
397  if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
398  const QUrl url = fileItem.url();
399  if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
400  bNeedCache = true;
401  }
402  }
403  } else {
404  Q_EMIT q->failed(fileItem);
405  }
406  }
407 
408  KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
409  maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
410  maximumRemoteSize = cg.readEntry("MaximumRemoteSize", 0);
411 
412  if (bNeedCache) {
413 
414  if (width <= 128 && height <= 128) {
415  cacheSize = 128;
416  } else if (width <= 256 && height <= 256) {
417  cacheSize = 256;
418  } else {
419  cacheSize = 512;
420  }
421 
422  struct CachePool {
423  QString path;
424  int minSize;
425  };
426 
427  const static auto pools = {
428  CachePool{QStringLiteral("/normal/"), 128},
429  CachePool{QStringLiteral("/large/"), 256},
430  CachePool{QStringLiteral("/x-large/"), 512},
431  CachePool{QStringLiteral("/xx-large/"), 1024},
432  };
433 
434  QString thumbDir;
435  int wants = devicePixelRatio * cacheSize;
436  for (const auto &p : pools) {
437  if (p.minSize < wants) {
438  continue;
439  } else {
440  thumbDir = p.path;
441  break;
442  }
443  }
444  thumbPath = thumbRoot + thumbDir;
445 
446  if (!QDir(thumbPath).exists()) {
447  if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions)
448  QFile f(thumbPath);
450  }
451  }
452  } else {
453  bSave = false;
454  }
455 
456  initialItems.clear();
457  determineNextFile();
458 }
459 
460 void PreviewJob::removeItem(const QUrl &url)
461 {
462  Q_D(PreviewJob);
463 
464  auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
465  return url == pItem.item.url();
466  });
467  if (it != d->items.cend()) {
468  d->items.erase(it);
469  }
470 
471  if (d->currentItem.item.url() == url) {
472  KJob *job = subjobs().first();
473  job->kill();
474  removeSubjob(job);
475  d->determineNextFile();
476  }
477 }
478 
480 {
481  d_func()->sequenceIndex = index;
482 }
483 
485 {
486  return d_func()->sequenceIndex;
487 }
488 
490 {
491  return d_func()->thumbnailSlaveMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
492 }
493 
495 {
496  return d_func()->thumbnailSlaveMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
497 }
498 
499 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
501 {
502  d_func()->devicePixelRatio = dpr;
503 }
504 #endif
505 
507 {
508  d_func()->devicePixelRatio = std::ceil(dpr);
509 }
510 
511 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize)
512 {
513  d_func()->ignoreMaximumSize = ignoreSize;
514 }
515 
516 void PreviewJobPrivate::cleanupTempFile()
517 {
518  if (!tempName.isEmpty()) {
519  Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
520  QFile::remove(tempName);
521  tempName.clear();
522  }
523 }
524 
525 void PreviewJobPrivate::determineNextFile()
526 {
527  Q_Q(PreviewJob);
528  if (!currentItem.item.isNull()) {
529  if (!succeeded) {
530  Q_EMIT q->failed(currentItem.item);
531  }
532  }
533  // No more items ?
534  if (items.empty()) {
535  q->emitResult();
536  return;
537  } else {
538  // First, stat the orig file
539  state = PreviewJobPrivate::STATE_STATORIG;
540  currentItem = items.front();
541  items.pop_front();
542  succeeded = false;
543  KIO::Job *job = KIO::statDetails(currentItem.item.url(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
544  job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
545  job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
546  q->addSubjob(job);
547  }
548 }
549 
550 void PreviewJob::slotResult(KJob *job)
551 {
552  Q_D(PreviewJob);
553 
554  removeSubjob(job);
555  Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
556  switch (d->state) {
557  case PreviewJobPrivate::STATE_STATORIG: {
558  if (job->error()) { // that's no good news...
559  // Drop this one and move on to the next one
560  d->determineNextFile();
561  return;
562  }
563  const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
564  d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
566 
567  bool skipCurrentItem = false;
569  const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
570 
571  if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) {
572  skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
573  } else {
574  // For remote items the "IgnoreMaximumSize" plugin property is not respected
575  skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize;
576 
577  // Remote directories are not supported, don't try to do a file_copy on them
578  if (!skipCurrentItem) {
579  // TODO update item.mimeType from the UDS entry, in case it wasn't set initially
580  // But we don't use the MIME type anymore, we just use isDir().
581  if (d->currentItem.item.isDir()) {
582  skipCurrentItem = true;
583  }
584  }
585  }
586  if (skipCurrentItem) {
587  d->determineNextFile();
588  return;
589  }
590 
591  bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
592  if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
593  // This preview will not be cached, no need to look for a saved thumbnail
594  // Just create it, and be done
595  d->getOrCreateThumbnail();
596  return;
597  }
598 
599  if (d->statResultThumbnail()) {
600  return;
601  }
602 
603  d->getOrCreateThumbnail();
604  return;
605  }
606  case PreviewJobPrivate::STATE_DEVICE_INFO: {
607  KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
608  int id;
609  QString path = statJob->url().toLocalFile();
610  if (job->error()) {
611  // We set id to 0 to know we tried getting it
612  qCWarning(KIO_WIDGETS) << "Cannot read information about filesystem under path" << path;
613  id = 0;
614  } else {
616  }
617  d->deviceIdMap[path] = id;
618  d->createThumbnail(d->currentItem.item.localPath());
619  return;
620  }
621  case PreviewJobPrivate::STATE_GETORIG: {
622  if (job->error()) {
623  d->cleanupTempFile();
624  d->determineNextFile();
625  return;
626  }
627 
628  d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
629  return;
630  }
631  case PreviewJobPrivate::STATE_CREATETHUMB: {
632  d->cleanupTempFile();
633  d->determineNextFile();
634  return;
635  }
636  }
637 }
638 
639 bool PreviewJobPrivate::statResultThumbnail()
640 {
641  if (thumbPath.isEmpty()) {
642  return false;
643  }
644 
645  bool isLocal;
646  const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
647  if (isLocal) {
648  const QFileInfo localFile(url.toLocalFile());
649  const QString canonicalPath = localFile.canonicalFilePath();
651  if (origName.isEmpty()) {
652  qCWarning(KIO_WIDGETS) << "Failed to convert" << url << "to canonical path";
653  return false;
654  }
655  } else {
656  // Don't include the password if any
657  origName = url.toEncoded(QUrl::RemovePassword);
658  }
659 
661  md5.addData(origName);
662  thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
663 
664  QImage thumb;
665  QFile thumbFile(thumbPath + thumbName);
666  if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
667  return false;
668  }
669 
670  if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
671  || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
672  return false;
673  }
674 
675  const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
676  if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
677  // Thumb::Size is not required, but if it is set it should match
678  return false;
679  }
680 
681  // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
682  // When a thumbnail is DPR-invariant, use the DPR passed in the request.
683  thumb.setDevicePixelRatio(devicePixelRatio);
684 
685  QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
686 
687  if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
688  // Check if the version matches
689  // The software string should read "KDE Thumbnail Generator pluginName (vX)"
690  QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
691  if (softwareString.isEmpty()) {
692  // The thumbnail has been created with an older version, recreating
693  return false;
694  }
695  int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
696  if (versionIndex < 0) {
697  return false;
698  }
699 
700  QString cachedVersion = softwareString.remove(0, versionIndex + 2);
701  cachedVersion.chop(1);
702  uint thumbnailerMajor = thumbnailerVersion.toInt();
703  uint cachedMajor = cachedVersion.toInt();
704  if (thumbnailerMajor > cachedMajor) {
705  return false;
706  }
707  }
708 
709  // Found it, use it
710  emitPreview(thumb);
711  succeeded = true;
712  determineNextFile();
713  return true;
714 }
715 
716 void PreviewJobPrivate::getOrCreateThumbnail()
717 {
718  Q_Q(PreviewJob);
719  // We still need to load the orig file ! (This is getting tedious) :)
720  const KFileItem &item = currentItem.item;
721  const QString localPath = item.localPath();
722  if (!localPath.isEmpty()) {
723  createThumbnail(localPath);
724  } else {
725  const QUrl fileUrl = item.url();
726  // heuristics for remote URL support
727  bool supportsProtocol = false;
728  if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) {
729  // There's a plugin supporting this protocol and MIME type
730  supportsProtocol = true;
731  } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) {
732  // Assume KIO understands any URL, ThumbCreator slaves who have
733  // X-KDE-Protocols=KIO will get fed the remote URL directly.
734  supportsProtocol = true;
735  }
736 
737  if (supportsProtocol) {
738  createThumbnail(fileUrl.toString());
739  return;
740  }
741 
742  // No plugin support access to this remote content, don't
743  // create the thumbnail (bugs 208625, 368104)
744  cleanupTempFile();
745  determineNextFile();
746  }
747 }
748 
749 PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
750 {
751  // If checked file is directory on a different filesystem than its parent, we need to check it separately
752  int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
753  QString parentDirPath = path.left(separatorIndex);
754 
755  int parentId = getDeviceId(parentDirPath);
756  if (parentId == idUnknown) {
757  return CachePolicy::Unknown;
758  }
759 
760  bool isDifferentSystem = !parentId || parentId != currentDeviceId;
761  if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
762  return currentDeviceCachePolicy;
763  }
764  int checkedId;
765  QString checkedPath;
766  if (isDifferentSystem) {
767  checkedId = currentDeviceId;
768  checkedPath = path;
769  } else {
770  checkedId = getDeviceId(parentDirPath);
771  checkedPath = parentDirPath;
772  if (checkedId == idUnknown) {
773  return CachePolicy::Unknown;
774  }
775  }
776  // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
777  int thumbRootId = getDeviceId(thumbRoot);
778  if (thumbRootId == idUnknown) {
779  return CachePolicy::Unknown;
780  }
781  bool shouldAllow = checkedId && checkedId == thumbRootId;
782  if (!shouldAllow) {
784  if (device.isValid()) {
785  shouldAllow = !device.as<Solid::StorageAccess>()->isEncrypted();
786  }
787  }
788  if (!isDifferentSystem) {
789  currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
790  }
791  return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
792 }
793 
794 int 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_WIDGETS) << "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 
814 void PreviewJobPrivate::createThumbnail(const QString &pixPath)
815 {
816  Q_Q(PreviewJob);
817  state = PreviewJobPrivate::STATE_CREATETHUMB;
818  QUrl thumbURL;
819  thumbURL.setScheme(QStringLiteral("thumbnail"));
820  thumbURL.setPath(pixPath);
821 
822  bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
823 
824  bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
825  CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
826 
827  if (cachePolicy == CachePolicy::Unknown) {
828  // If Unknown is returned, creating thumbnail should be called again by slotResult
829  return;
830  }
831 
832  KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
833  q->addSubjob(job);
834  q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
835  slotThumbData(job, data);
836  });
837 
838  int thumb_width = width;
839  int thumb_height = height;
840  int thumb_iconSize = iconSize;
841  if (save) {
842  thumb_width = thumb_height = cacheSize;
843  thumb_iconSize = 64;
844  }
845 
846  job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
847  job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
848  job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
849  job->addMetaData(QStringLiteral("iconSize"), QString::number(thumb_iconSize));
850  job->addMetaData(QStringLiteral("iconAlpha"), QString::number(iconAlpha));
851  job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
852  job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
853  job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
854  job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
855  if (sequenceIndex) {
856  job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
857  }
858 
859 #if WITH_SHM
860  size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
861  if (shmid == -1 || shmsize < requiredSize) {
862  if (shmaddr) {
863  // clean previous shared memory segment
864  shmdt((char *)shmaddr);
865  shmaddr = nullptr;
866  shmctl(shmid, IPC_RMID, nullptr);
867  shmid = -1;
868  }
869  if (requiredSize > 0) {
870  shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
871  if (shmid != -1) {
872  shmsize = requiredSize;
873  shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
874  if (shmaddr == (uchar *)-1) {
875  shmctl(shmid, IPC_RMID, nullptr);
876  shmaddr = nullptr;
877  shmid = -1;
878  }
879  }
880  }
881  }
882  if (shmid != -1) {
883  job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
884  }
885 #endif
886 }
887 
888 void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
889 {
890  thumbnailSlaveMetaData = job->metaData();
891  /* clang-format off */
892  const bool save = bSave
893  && !sequenceIndex
894  && currentDeviceCachePolicy == CachePolicy::Allow
895  && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
896  && (!currentItem.item.url().isLocalFile()
897  || !currentItem.item.url().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
898  /* clang-format on */
899 
900  QImage thumb;
901 #if WITH_SHM
902  if (shmaddr) {
903  // Keep this in sync with kdebase/kioslave/thumbnail.cpp
904  QDataStream str(data);
905  int width;
906  int height;
907  quint8 iFormat;
908  int imgDevicePixelRatio = 1;
909  // TODO KF6: add a version number as first parameter
910  str >> width >> height >> iFormat;
911  if (iFormat & 0x80) {
912  // HACK to deduce if imgDevicePixelRatio is present
913  iFormat &= 0x7f;
914  str >> imgDevicePixelRatio;
915  }
916  QImage::Format format = static_cast<QImage::Format>(iFormat);
917  thumb = QImage(shmaddr, width, height, format).copy();
918  thumb.setDevicePixelRatio(imgDevicePixelRatio);
919  } else {
920  thumb.loadFromData(data);
921  }
922 #else
923  thumb.loadFromData(data);
924 #endif
925 
926  if (thumb.isNull()) {
927  QDataStream s(data);
928  s >> thumb;
929  }
930 
931  if (save) {
932  thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
933  thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
934  thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
935  thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
936  QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
937  QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
938  if (!thumbnailerVersion.isEmpty()) {
939  signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
940  }
941  thumb.setText(QStringLiteral("Software"), signature);
942  QSaveFile saveFile(thumbPath + thumbName);
943  if (saveFile.open(QIODevice::WriteOnly)) {
944  if (thumb.save(&saveFile, "PNG")) {
945  saveFile.commit();
946  }
947  }
948  }
949  emitPreview(thumb);
950  succeeded = true;
951 }
952 
953 void PreviewJobPrivate::emitPreview(const QImage &thumb)
954 {
955  Q_Q(PreviewJob);
956  QPixmap pix;
957  if (thumb.width() > width * thumb.devicePixelRatio() || thumb.height() > height * thumb.devicePixelRatio()) {
959  } else {
960  pix = QPixmap::fromImage(thumb);
961  }
963  Q_EMIT q->gotPreview(currentItem.item, pix);
964 }
965 
967 {
969  const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
970  for (const KPluginMetaData &plugin : plugins) {
971  result << plugin.pluginId();
972  }
973  return result;
974 }
975 
977 {
978  const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
979 
981  for (const QString &plugin : blacklist) {
982  defaultPlugins.removeAll(plugin);
983  }
984 
985  return defaultPlugins;
986 }
987 
989 {
991  const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
992  for (const KPluginMetaData &plugin : plugins) {
993  result += plugin.mimeTypes();
994  }
995  return result;
996 }
997 
998 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
999 PreviewJob *
1000 KIO::filePreview(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1001 {
1002  return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1003 }
1004 #endif
1005 
1006 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
1007 PreviewJob *
1008 KIO::filePreview(const QList<QUrl> &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1009 {
1010  KFileItemList fileItems;
1011  fileItems.reserve(items.size());
1012  for (const QUrl &url : items) {
1013  Q_ASSERT(url.isValid()); // please call us with valid urls only
1014  fileItems.append(KFileItem(url));
1015  }
1016  return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1017 }
1018 #endif
1019 
1020 PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
1021 {
1022  return new PreviewJob(items, size, enabledPlugins);
1023 }
1024 
1025 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 5)
1027 {
1028  KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
1029  return cg.readEntry("MaximumSize", 5 * 1024 * 1024LL /* 5MB */);
1030 }
1031 #endif
1032 
1033 #include "moc_previewjob.cpp"
QString url(QUrl::FormattingOptions options) const const
RemoveFilename
bool kill(KillVerbosity verbosity=Quietly)
void clear()
static QStringList defaultPlugins()
Returns a list of plugins that should be enabled by default, which is all plugins Minus the plugins s...
Definition: previewjob.cpp:976
static QStringList availablePlugins()
Returns a list of all available preview plugins.
Definition: previewjob.cpp:966
QString value(const QString &key, const QString &defaultValue=QString()) const
bool loadFromData(const uchar *data, int len, const char *format)
QString & append(QChar ch)
qulonglong filesize_t
64-bit file size
Definition: global.h:39
QHash::iterator insert(const Key &key, const T &value)
bool load(QIODevice *device, const char *format)
void addMetaData(const QString &key, const QString &value)
Add key/value pair to the meta data that is sent to the slave.
Definition: job.cpp:221
FullyEncoded
DevIface * as()
QString writableLocation(QStandardPaths::StandardLocation type)
void setOverlayIconSize(int size)
Sets the size of the MIME-type icon which overlays the preview.
Definition: previewjob.cpp:264
QByteArray toHex() const const
Device number for this file, used to detect hardlinks.
Definition: udsentry.h:327
const QUrl & url() const
Returns the SimpleJob&#39;s URL.
Definition: simplejob.cpp:70
Universal Directory Service.
Definition: udsentry.h:77
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
Definition: mkpathjob.cpp:145
QString mimetype() const
Returns the MIME type of the file item.
Definition: kfileitem.cpp:845
bool remove()
A namespace for KIO globals.
bool save(const QString &fileName, const char *format, int quality) const const
void reserve(int alloc)
Hide progress information dialog, i.e.
Definition: job_base.h:274
void setScaleType(ScaleType type)
Sets the scale type for the generated preview.
Definition: previewjob.cpp:288
bool isValid() const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QPixmap fromImage(const QImage &image, Qt::ImageConversionFlags flags)
QHash::const_iterator constFind(const Key &key) const const
A KIO job that retrieves information about a file or directory.
Definition: statjob.h:25
QMap::const_iterator constFind(const Key &key) const const
virtual bool open(QIODevice::OpenMode mode) override
virtual bool setPermissions(QFileDevice::Permissions permissions) override
QDateTime fromSecsSinceEpoch(qint64 secs, Qt::TimeSpec spec, int offsetSeconds)
QString join(const QString &separator) const const
bool isNull() const const
QString & remove(int position, int n)
QImage copy(const QRect &rectangle) const const
QString text(const QString &key) const const
void setDevicePixelRatio(qreal scaleFactor)
void chop(int n)
QString toString(QUrl::FormattingOptions options) const const
qreal devicePixelRatio() const const
bool hasSubjobs() const
The original size of the preview will be returned.
Definition: previewjob.h:44
int size() const const
int overlayIconAlpha() const
Definition: previewjob.cpp:282
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
T value(int i) const const
void clear()
void setPath(const QString &path, QUrl::ParsingMode mode)
void setText(const QString &key, const QString &text)
void setDevicePixelRatio(int dpr)
Request preview to use the device pixel ratio dpr.
Definition: previewjob.cpp:500
Base class for thumbnail generator plugins.
Definition: thumbcreator.h:85
int sequenceIndex() const
Returns the currently set sequence index.
Definition: previewjob.cpp:484
KIOCORE_EXPORT QString number(KIO::filesize_t size)
Converts a size to a string representation Not unlike QString::number(...)
Definition: global.cpp:55
void removeItem(const QUrl &url)
Removes an item from preview processing.
Definition: previewjob.cpp:460
QString number(int n, int base)
void append(const T &value)
QString fromUtf8(const char *str, int size)
QHash::const_iterator constEnd() const const
static KIO::filesize_t maximumFileSize()
Returns the default "maximum file size", in bytes, used by PreviewJob.
bool commit()
ScaleType scaleType() const
Definition: previewjob.cpp:309
long long numberValue(uint field, long long defaultValue=0) const
Definition: udsentry.cpp:379
int width() const const
int toInt(bool *ok, int base) const const
QString localPath() const
Returns the local path if isLocalFile() == true or the KIO item has a UDS_LOCAL_PATH atom...
Definition: kfileitem.cpp:725
bool isEmpty() const const
int removeAll(const T &value)
QString trimmed() const const
QMap::const_iterator constEnd() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
void setScheme(const QString &scheme)
dev, inode
Definition: global.h:347
List of KFileItems, which adds a few helper methods to QList<KFileItem>.
Definition: kfileitem.h:601
KIOWIDGETS_EXPORT PreviewJob * filePreview(const KFileItemList &items, int width, int height=0, int iconSize=0, int iconAlpha=70, bool scale=true, bool save=true, const QStringList *enabledPlugins=nullptr)
Creates a PreviewJob to generate or retrieve a preview image for the given URL.
void addData(const char *data, int length)
MetaData metaData() const
Get meta data received from the slave.
Definition: job.cpp:205
T & first()
PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
Creates a new PreviewJob.
Definition: previewjob.cpp:220
QMap::iterator end()
qulonglong toULongLong(bool *ok, int base) const const
static QString protocolClass(const QString &protocol)
Returns the protocol class for the specified protocol.
virtual bool open(QIODevice::OpenMode mode) override
QString scheme() const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QString toLocalFile() const const
The last time the file was modified. Required time format: seconds since UNIX epoch.
Definition: udsentry.h:259
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
Get (means: read).
const T value(const Key &key) const const
KIO Job to get a thumbnail picture.
Definition: previewjob.h:29
bool isValid() const const
bool isValid() const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QString & replace(int position, int n, QChar after)
void data(KIO::Job *job, const QByteArray &data)
Data from the slave has arrived.
The preview will be scaled to the size specified when constructing the PreviewJob.
Definition: previewjob.h:55
bool isValid() const const
void setIgnoreMaximumSize(bool ignoreSize=true)
If ignoreSize is true, then the preview is always generated regardless of the settings.
Definition: previewjob.cpp:511
QUrl adjusted(QUrl::FormattingOptions options) const const
bool isEmpty() const const
qint64 toSecsSinceEpoch() const const
KeepAspectRatio
void setOverlayIconAlpha(int alpha)
Sets the alpha-value for the MIME-type icon which overlays the preview.
Definition: previewjob.cpp:276
The base class for all jobs.
Definition: job_base.h:44
const QList< KJob * > & subjobs() const
bool removeSubjob(KJob *job) override
Mark a sub job as being done.
Definition: job.cpp:88
static Device storageAccessFromPath(const QString &path)
QString left(int n) const const
QByteArray result() const const
static QVector< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={})
QString fromLatin1(const char *str, int size)
int height() const const
Default StatDetail flag when creating a StatJob.
Definition: global.h:357
QMap::iterator insert(const Key &key, const T &value)
ScaleType
Specifies the type of scaling that is applied to the generated preview.
Definition: previewjob.h:39
QUrl url() const
Returns the url of the file.
Definition: kfileitem.cpp:1549
void result(KJob *job)
bool handlesSequences() const
Determines whether the ThumbCreator in use is a ThumbSequenceCreator.
Definition: previewjob.cpp:494
The transfer job pumps data into and/or out of a Slave.
Definition: transferjob.h:25
static QStringList supportedMimeTypes()
Returns a list of all supported MIME types.
Definition: previewjob.cpp:988
const UDSEntry & statResult() const
Result of the stat operation.
Definition: statjob.cpp:113
void setDevicePixelRatio(qreal scaleFactor)
float sequenceIndexWraparoundPoint() const
Returns the index at which the thumbs of a ThumbSequenceCreator start wrapping around ("looping")...
Definition: previewjob.cpp:489
static KPluginMetaData fromDesktopFile(const QString &file, const QStringList &serviceTypes=QStringList())
T readEntry(const QString &key, const T &aDefault) const
SmoothTransformation
QString baseName() const const
qlonglong toLongLong(bool *ok, int base) const const
QMap::iterator find(const Key &key)
Size of the file.
Definition: udsentry.h:230
The preview will be scaled to the size specified when constructing the PreviewJob.
Definition: previewjob.h:49
KIOCORE_EXPORT StatJob * statDetails(const QUrl &url, KIO::StatJob::StatSide side, KIO::StatDetails details=KIO::StatDefaultDetails, JobFlags flags=DefaultFlags)
Find all details for one file or directory.
Definition: statjob.cpp:261
QByteArray toEncoded(QUrl::FormattingOptions options) const const
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
bool isValid() const
Q_EMITQ_EMIT
A KFileItem is a generic class to handle a file, local or remote.
Definition: kfileitem.h:35
int overlayIconSize() const
Definition: previewjob.cpp:270
QUrl fromLocalFile(const QString &localFile)
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
int error() const
bool isLocalFile() const const
void setSequenceIndex(int index)
Sets the sequence index given to the thumb creators.
Definition: previewjob.cpp:479
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sun Dec 5 2021 22:52:54 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.