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 | |
54 | namespace |
55 | { |
56 | static qreal s_defaultDevicePixelRatio = 1.0; |
57 | } |
58 | |
59 | namespace KIO |
60 | { |
61 | struct PreviewItem; |
62 | } |
63 | using namespace KIO; |
64 | |
65 | struct KIO::PreviewItem { |
66 | KFileItem item; |
67 | KPluginMetaData plugin; |
68 | }; |
69 | |
70 | class KIO::PreviewJobPrivate : public KIO::JobPrivate |
71 | { |
72 | public: |
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 |
90 | thumbRoot = QStandardPaths::writableLocation(type: QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/" ); |
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 | |
180 | void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio) |
181 | { |
182 | s_defaultDevicePixelRatio = defaultDevicePixelRatio; |
183 | } |
184 | |
185 | PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) |
186 | : KIO::Job(*new PreviewJobPrivate(items, size)) |
187 | { |
188 | Q_D(PreviewJob); |
189 | |
190 | if (enabledPlugins) { |
191 | d->enabledPlugins = *enabledPlugins; |
192 | } else { |
193 | const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings" )); |
194 | d->enabledPlugins = |
195 | globalConfig.readEntry(key: "Plugins" , |
196 | aDefault: QStringList{QStringLiteral("directorythumbnail" ), QStringLiteral("imagethumbnail" ), QStringLiteral("jpegthumbnail" )}); |
197 | } |
198 | |
199 | // Return to event loop first, determineNextFile() might delete this; |
200 | QTimer::singleShot(interval: 0, receiver: this, slot: [d]() { |
201 | d->startPreview(); |
202 | }); |
203 | } |
204 | |
205 | PreviewJob::~PreviewJob() |
206 | { |
207 | #if WITH_SHM |
208 | Q_D(PreviewJob); |
209 | if (d->shmaddr) { |
210 | shmdt(shmaddr: (char *)d->shmaddr); |
211 | shmctl(shmid: d->shmid, IPC_RMID, buf: nullptr); |
212 | } |
213 | #endif |
214 | } |
215 | |
216 | void PreviewJob::setScaleType(ScaleType type) |
217 | { |
218 | Q_D(PreviewJob); |
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 | |
237 | PreviewJob::ScaleType PreviewJob::scaleType() const |
238 | { |
239 | Q_D(const PreviewJob); |
240 | if (d->bScale) { |
241 | return d->bSave ? ScaledAndCached : Scaled; |
242 | } |
243 | return Unscaled; |
244 | } |
245 | |
246 | void 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(); |
251 | QMap<QString, KPluginMetaData> mimeMap; |
252 | QHash<QString, QHash<QString, KPluginMetaData>> protocolMap; |
253 | |
254 | for (const KPluginMetaData &plugin : plugins) { |
255 | QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols" ), defaultValue: QStringList()); |
256 | const QString p = plugin.value(QStringLiteral("X-KDE-Protocol" )); |
257 | if (!p.isEmpty()) { |
258 | protocols.append(t: p); |
259 | } |
260 | for (const QString &protocol : std::as_const(t&: 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(key: _m, value: plugin); |
266 | if (!_ms.contains(str: _m)) { |
267 | _ms.append(t: _m); |
268 | } |
269 | } |
270 | } |
271 | if (enabledPlugins.contains(str: plugin.pluginId())) { |
272 | const auto mimeTypes = plugin.mimeTypes(); |
273 | for (const QString &mimeType : mimeTypes) { |
274 | mimeMap.insert(key: mimeType, value: 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(t&: 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(key: item.item.url().scheme()); |
290 | if (it != protocolMap.constEnd()) { |
291 | plugin = it.value().value(key: mimeType); |
292 | } |
293 | |
294 | if (!plugin.isValid()) { |
295 | auto pluginIt = mimeMap.constFind(key: mimeType); |
296 | if (pluginIt == mimeMap.constEnd()) { |
297 | // check MIME type inheritance, resolve aliases |
298 | QMimeDatabase db; |
299 | const QMimeType mimeInfo = db.mimeTypeForName(nameOrAlias: mimeType); |
300 | if (mimeInfo.isValid()) { |
301 | const QStringList parentMimeTypes = mimeInfo.allAncestors(); |
302 | for (const QString &parentMimeType : parentMimeTypes) { |
303 | pluginIt = mimeMap.constFind(key: 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(c: QLatin1Char('/')); |
314 | if (slashIdx != -1) { |
315 | // Replace everything after '/' with '*' |
316 | groupMimeType.truncate(pos: slashIdx + 1); |
317 | groupMimeType += QLatin1Char('*'); |
318 | } |
319 | pluginIt = mimeMap.constFind(key: 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(x: item); |
331 | if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail" ), defaultValue: true)) { |
332 | const QUrl url = fileItem.targetUrl(); |
333 | if (!url.isLocalFile() || !url.adjusted(options: QUrl::RemoveFilename).toLocalFile().startsWith(s: thumbRoot)) { |
334 | bNeedCache = true; |
335 | } |
336 | } |
337 | } else { |
338 | Q_EMIT q->failed(item: fileItem); |
339 | } |
340 | } |
341 | |
342 | KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings" )); |
343 | maximumLocalSize = cg.readEntry(key: "MaximumSize" , defaultValue: std::numeric_limits<KIO::filesize_t>::max()); |
344 | maximumRemoteSize = cg.readEntry<KIO::filesize_t>(key: "MaximumRemoteSize" , defaultValue: 0); |
345 | enableRemoteFolderThumbnail = cg.readEntry(key: "EnableRemoteFolderThumbnail" , defaultValue: false); |
346 | |
347 | if (bNeedCache) { |
348 | const int longer = std::max(a: width, b: 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 { |
360 | QString path; |
361 | int minSize; |
362 | }; |
363 | |
364 | const static auto pools = { |
365 | CachePool{QStringLiteral("/normal/" ), .minSize: 128}, |
366 | CachePool{QStringLiteral("/large/" ), .minSize: 256}, |
367 | CachePool{QStringLiteral("/x-large/" ), .minSize: 512}, |
368 | CachePool{QStringLiteral("/xx-large/" ), .minSize: 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(dirPath: 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 | |
397 | void PreviewJob::removeItem(const QUrl &url) |
398 | { |
399 | Q_D(PreviewJob); |
400 | |
401 | auto it = std::find_if(first: d->items.cbegin(), last: d->items.cend(), pred: [&url](const PreviewItem &pItem) { |
402 | return url == pItem.item.url(); |
403 | }); |
404 | if (it != d->items.cend()) { |
405 | d->items.erase(position: 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 | |
416 | void KIO::PreviewJob::setSequenceIndex(int index) |
417 | { |
418 | d_func()->sequenceIndex = index; |
419 | } |
420 | |
421 | int KIO::PreviewJob::sequenceIndex() const |
422 | { |
423 | return d_func()->sequenceIndex; |
424 | } |
425 | |
426 | float KIO::PreviewJob::sequenceIndexWraparoundPoint() const |
427 | { |
428 | return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint" ), QStringLiteral("-1.0" )).toFloat(); |
429 | } |
430 | |
431 | bool KIO::PreviewJob::handlesSequences() const |
432 | { |
433 | return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences" )) == QStringLiteral("1" ); |
434 | } |
435 | |
436 | void KIO::PreviewJob::setDevicePixelRatio(qreal dpr) |
437 | { |
438 | d_func()->devicePixelRatio = dpr; |
439 | } |
440 | |
441 | void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) |
442 | { |
443 | d_func()->ignoreMaximumSize = ignoreSize; |
444 | } |
445 | |
446 | void PreviewJobPrivate::cleanupTempFile() |
447 | { |
448 | if (!tempName.isEmpty()) { |
449 | Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink()); |
450 | QFile::remove(fileName: tempName); |
451 | tempName.clear(); |
452 | } |
453 | } |
454 | |
455 | void PreviewJobPrivate::determineNextFile() |
456 | { |
457 | Q_Q(PreviewJob); |
458 | if (!currentItem.item.isNull()) { |
459 | if (!succeeded) { |
460 | Q_EMIT q->failed(item: 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(url: currentItem.item.targetUrl(), side: StatJob::SourceSide, details: KIO::StatDefaultDetails | KIO::StatInode, flags: 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 | |
480 | void PreviewJob::slotResult(KJob *job) |
481 | { |
482 | Q_D(PreviewJob); |
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(field: KIO::UDSEntry::UDS_DEVICE_ID, defaultValue: 0); |
495 | d->tOrig = QDateTime::fromSecsSinceEpoch(secs: statResult.numberValue(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, defaultValue: 0)); |
496 | |
497 | bool skipCurrentItem = false; |
498 | const KIO::filesize_t size = (KIO::filesize_t)statResult.numberValue(field: KIO::UDSEntry::UDS_SIZE, defaultValue: 0); |
499 | const QUrl itemUrl = d->currentItem.item.mostLocalUrl(); |
500 | |
501 | if ((itemUrl.isLocalFile() || KProtocolInfo::protocolClass(protocol: itemUrl.scheme()) == QLatin1String(":local" )) && !d->currentItem.item.isSlow()) { |
502 | skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize" ), defaultValue: 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" ), defaultValue: false); |
514 | if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail" ), defaultValue: 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 { |
537 | id = statJob->statResult().numberValue(field: KIO::UDSEntry::UDS_DEVICE_ID, defaultValue: 0); |
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 | |
561 | bool PreviewJobPrivate::statResultThumbnail() |
562 | { |
563 | if (thumbPath.isEmpty()) { |
564 | return false; |
565 | } |
566 | |
567 | bool isLocal; |
568 | const QUrl url = currentItem.item.mostLocalUrl(local: &isLocal); |
569 | if (isLocal) { |
570 | const QFileInfo localFile(url.toLocalFile()); |
571 | const QString canonicalPath = localFile.canonicalFilePath(); |
572 | origName = QUrl::fromLocalFile(localfile: canonicalPath).toEncoded(options: QUrl::RemovePassword | QUrl::FullyEncoded); |
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(options: QUrl::RemovePassword); |
580 | } |
581 | |
582 | QCryptographicHash md5(QCryptographicHash::Md5); |
583 | md5.addData(data: origName); |
584 | thumbName = QString::fromLatin1(ba: md5.result().toHex()) + QLatin1String(".png" ); |
585 | |
586 | QImage thumb; |
587 | QFile thumbFile(thumbPath + thumbName); |
588 | if (!thumbFile.open(flags: QIODevice::ReadOnly) || !thumb.load(device: &thumbFile, format: "png" )) { |
589 | return false; |
590 | } |
591 | |
592 | if (thumb.text(QStringLiteral("Thumb::URI" )) != QString::fromUtf8(ba: 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(s: 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(s: QLatin1String("(v" )); |
618 | if (versionIndex < 0) { |
619 | return false; |
620 | } |
621 | |
622 | QString cachedVersion = softwareString.remove(i: 0, len: versionIndex + 2); |
623 | cachedVersion.chop(n: 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 | |
638 | void 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(key: fileUrl.scheme()).contains(str: 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(str: item.mimetype())) { |
656 | // Assume KIO understands any URL, ThumbCreator workers who have |
657 | // X-KDE-Protocols=KIO will get fed the remote URL directly. |
658 | supportsProtocol = true; |
659 | } |
660 | |
661 | if (supportsProtocol) { |
662 | createThumbnail(fileUrl.toString()); |
663 | return; |
664 | } |
665 | if (item.isDir()) { |
666 | // Skip remote dirs (bug 208625) |
667 | cleanupTempFile(); |
668 | determineNextFile(); |
669 | return; |
670 | } |
671 | // No plugin support access to this remote content, copy the file |
672 | // to the local machine, then create the thumbnail |
673 | state = PreviewJobPrivate::STATE_GETORIG; |
674 | QTemporaryFile localFile; |
675 | localFile.setAutoRemove(false); |
676 | localFile.open(); |
677 | tempName = localFile.fileName(); |
678 | const QUrl currentURL = item.mostLocalUrl(); |
679 | KIO::Job *job = KIO::file_copy(src: currentURL, dest: QUrl::fromLocalFile(localfile: tempName), permissions: -1, flags: KIO::Overwrite | KIO::HideProgressInfo /* No GUI */); |
680 | job->addMetaData(QStringLiteral("thumbnail" ), QStringLiteral("1" )); |
681 | q->addSubjob(job); |
682 | } |
683 | |
684 | PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path) |
685 | { |
686 | // If checked file is directory on a different filesystem than its parent, we need to check it separately |
687 | int separatorIndex = path.lastIndexOf(c: QLatin1Char('/')); |
688 | // special case for root folders |
689 | const QString parentDirPath = separatorIndex == 0 ? path : path.left(n: separatorIndex); |
690 | |
691 | int parentId = getDeviceId(path: parentDirPath); |
692 | if (parentId == idUnknown) { |
693 | return CachePolicy::Unknown; |
694 | } |
695 | |
696 | bool isDifferentSystem = !parentId || parentId != currentDeviceId; |
697 | if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) { |
698 | return currentDeviceCachePolicy; |
699 | } |
700 | int checkedId; |
701 | QString checkedPath; |
702 | if (isDifferentSystem) { |
703 | checkedId = currentDeviceId; |
704 | checkedPath = path; |
705 | } else { |
706 | checkedId = getDeviceId(path: parentDirPath); |
707 | checkedPath = parentDirPath; |
708 | if (checkedId == idUnknown) { |
709 | return CachePolicy::Unknown; |
710 | } |
711 | } |
712 | // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot |
713 | int thumbRootId = getDeviceId(path: thumbRoot); |
714 | if (thumbRootId == idUnknown) { |
715 | return CachePolicy::Unknown; |
716 | } |
717 | bool shouldAllow = checkedId && checkedId == thumbRootId; |
718 | if (!shouldAllow) { |
719 | Solid::Device device = Solid::Device::storageAccessFromPath(path: checkedPath); |
720 | if (device.isValid()) { |
721 | // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location. |
722 | // Or, if the checked device is unencrypted, allow thumbnailing. |
723 | if (device.as<Solid::StorageAccess>()->isEncrypted()) { |
724 | const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(path: thumbRoot); |
725 | shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted(); |
726 | } else { |
727 | shouldAllow = true; |
728 | } |
729 | } |
730 | } |
731 | if (!isDifferentSystem) { |
732 | currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent; |
733 | } |
734 | return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent; |
735 | } |
736 | |
737 | int PreviewJobPrivate::getDeviceId(const QString &path) |
738 | { |
739 | Q_Q(PreviewJob); |
740 | auto iter = deviceIdMap.find(key: path); |
741 | if (iter != deviceIdMap.end()) { |
742 | return iter.value(); |
743 | } |
744 | QUrl url = QUrl::fromLocalFile(localfile: path); |
745 | if (!url.isValid()) { |
746 | qCWarning(KIO_GUI) << "Could not get device id for file preview, Invalid url" << path; |
747 | return 0; |
748 | } |
749 | state = PreviewJobPrivate::STATE_DEVICE_INFO; |
750 | KIO::Job *job = KIO::stat(url, side: StatJob::SourceSide, details: KIO::StatDefaultDetails | KIO::StatInode, flags: KIO::HideProgressInfo); |
751 | job->addMetaData(QStringLiteral("no-auth-prompt" ), QStringLiteral("true" )); |
752 | q->addSubjob(job); |
753 | |
754 | return idUnknown; |
755 | } |
756 | |
757 | void PreviewJobPrivate::createThumbnail(const QString &pixPath) |
758 | { |
759 | Q_Q(PreviewJob); |
760 | state = PreviewJobPrivate::STATE_CREATETHUMB; |
761 | QUrl thumbURL; |
762 | thumbURL.setScheme(QStringLiteral("thumbnail" )); |
763 | thumbURL.setPath(path: pixPath); |
764 | |
765 | bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail" ), defaultValue: true) && !sequenceIndex; |
766 | |
767 | bool isRemoteProtocol = currentItem.item.localPath().isEmpty(); |
768 | CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(path: pixPath); |
769 | |
770 | if (cachePolicy == CachePolicy::Unknown) { |
771 | // If Unknown is returned, creating thumbnail should be called again by slotResult |
772 | return; |
773 | } |
774 | |
775 | KIO::TransferJob *job = KIO::get(url: thumbURL, reload: NoReload, flags: HideProgressInfo); |
776 | q->addSubjob(job); |
777 | q->connect(sender: job, signal: &KIO::TransferJob::data, context: q, slot: [this](KIO::Job *job, const QByteArray &data) { |
778 | slotThumbData(job, data); |
779 | }); |
780 | |
781 | int thumb_width = width; |
782 | int thumb_height = height; |
783 | if (save) { |
784 | thumb_width = thumb_height = cacheSize; |
785 | } |
786 | |
787 | job->addMetaData(QStringLiteral("mimeType" ), value: currentItem.item.mimetype()); |
788 | job->addMetaData(QStringLiteral("width" ), value: QString::number(thumb_width)); |
789 | job->addMetaData(QStringLiteral("height" ), value: QString::number(thumb_height)); |
790 | job->addMetaData(QStringLiteral("plugin" ), value: currentItem.plugin.fileName()); |
791 | job->addMetaData(QStringLiteral("enabledPlugins" ), value: enabledPlugins.join(sep: QLatin1Char(','))); |
792 | job->addMetaData(QStringLiteral("devicePixelRatio" ), value: QString::number(devicePixelRatio)); |
793 | job->addMetaData(QStringLiteral("cache" ), value: QString::number(cachePolicy == CachePolicy::Allow)); |
794 | if (sequenceIndex) { |
795 | job->addMetaData(QStringLiteral("sequence-index" ), value: QString::number(sequenceIndex)); |
796 | } |
797 | |
798 | #if WITH_SHM |
799 | size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4; |
800 | if (shmid == -1 || shmsize < requiredSize) { |
801 | if (shmaddr) { |
802 | // clean previous shared memory segment |
803 | shmdt(shmaddr: (char *)shmaddr); |
804 | shmaddr = nullptr; |
805 | shmctl(shmid: shmid, IPC_RMID, buf: nullptr); |
806 | shmid = -1; |
807 | } |
808 | if (requiredSize > 0) { |
809 | shmid = shmget(IPC_PRIVATE, size: requiredSize, IPC_CREAT | 0600); |
810 | if (shmid != -1) { |
811 | shmsize = requiredSize; |
812 | shmaddr = (uchar *)(shmat(shmid: shmid, shmaddr: nullptr, SHM_RDONLY)); |
813 | if (shmaddr == (uchar *)-1) { |
814 | shmctl(shmid: shmid, IPC_RMID, buf: nullptr); |
815 | shmaddr = nullptr; |
816 | shmid = -1; |
817 | } |
818 | } |
819 | } |
820 | } |
821 | if (shmid != -1) { |
822 | job->addMetaData(QStringLiteral("shmid" ), value: QString::number(shmid)); |
823 | } |
824 | #endif |
825 | } |
826 | |
827 | void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data) |
828 | { |
829 | thumbnailWorkerMetaData = job->metaData(); |
830 | /* clang-format off */ |
831 | const bool save = bSave |
832 | && !sequenceIndex |
833 | && currentDeviceCachePolicy == CachePolicy::Allow |
834 | && currentItem.plugin.value(QStringLiteral("CacheThumbnail" ), defaultValue: true) |
835 | && (!currentItem.item.targetUrl().isLocalFile() |
836 | || !currentItem.item.targetUrl().adjusted(options: QUrl::RemoveFilename).toLocalFile().startsWith(s: thumbRoot)); |
837 | /* clang-format on */ |
838 | |
839 | QImage thumb; |
840 | // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp |
841 | QDataStream str(data); |
842 | int width; |
843 | int height; |
844 | QImage::Format format; |
845 | qreal imgDevicePixelRatio; |
846 | // TODO KF6: add a version number as first parameter |
847 | str >> width >> height >> format >> imgDevicePixelRatio; |
848 | #if WITH_SHM |
849 | if (shmaddr != nullptr) { |
850 | thumb = QImage(shmaddr, width, height, format).copy(); |
851 | } else { |
852 | #endif |
853 | str >> thumb; |
854 | #if WITH_SHM |
855 | } |
856 | #endif |
857 | thumb.setDevicePixelRatio(imgDevicePixelRatio); |
858 | |
859 | if (thumb.isNull()) { |
860 | QDataStream s(data); |
861 | s >> thumb; |
862 | } |
863 | |
864 | if (thumb.isNull()) { |
865 | // let succeeded in false state |
866 | // failed will get called in determineNextFile() |
867 | return; |
868 | } |
869 | |
870 | if (save) { |
871 | thumb.setText(QStringLiteral("Thumb::URI" ), value: QString::fromUtf8(ba: origName)); |
872 | thumb.setText(QStringLiteral("Thumb::MTime" ), value: QString::number(tOrig.toSecsSinceEpoch())); |
873 | thumb.setText(QStringLiteral("Thumb::Size" ), value: number(size: currentItem.item.size())); |
874 | thumb.setText(QStringLiteral("Thumb::Mimetype" ), value: currentItem.item.mimetype()); |
875 | QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion" )); |
876 | QString signature = QLatin1String("KDE Thumbnail Generator " ) + currentItem.plugin.name(); |
877 | if (!thumbnailerVersion.isEmpty()) { |
878 | signature.append(s: QLatin1String(" (v" ) + thumbnailerVersion + QLatin1Char(')')); |
879 | } |
880 | thumb.setText(QStringLiteral("Software" ), value: signature); |
881 | QSaveFile saveFile(thumbPath + thumbName); |
882 | if (saveFile.open(flags: QIODevice::WriteOnly)) { |
883 | if (thumb.save(device: &saveFile, format: "PNG" )) { |
884 | saveFile.commit(); |
885 | } |
886 | } |
887 | } |
888 | emitPreview(thumb); |
889 | succeeded = true; |
890 | } |
891 | |
892 | void PreviewJobPrivate::emitPreview(const QImage &thumb) |
893 | { |
894 | Q_Q(PreviewJob); |
895 | QPixmap pix; |
896 | const qreal ratio = thumb.devicePixelRatio(); |
897 | if (thumb.width() > width * ratio || thumb.height() > height * ratio) { |
898 | pix = QPixmap::fromImage(image: thumb.scaled(s: QSize(width * ratio, height * ratio), aspectMode: Qt::KeepAspectRatio, mode: Qt::SmoothTransformation)); |
899 | } else { |
900 | pix = QPixmap::fromImage(image: thumb); |
901 | } |
902 | pix.setDevicePixelRatio(ratio); |
903 | Q_EMIT q->gotPreview(item: currentItem.item, preview: pix); |
904 | } |
905 | |
906 | QList<KPluginMetaData> PreviewJob::availableThumbnailerPlugins() |
907 | { |
908 | return PreviewJobPrivate::loadAvailablePlugins(); |
909 | } |
910 | |
911 | QStringList PreviewJob::availablePlugins() |
912 | { |
913 | QStringList result; |
914 | const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins(); |
915 | for (const KPluginMetaData &plugin : plugins) { |
916 | result << plugin.pluginId(); |
917 | } |
918 | return result; |
919 | } |
920 | |
921 | QStringList PreviewJob::defaultPlugins() |
922 | { |
923 | const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail" ); |
924 | |
925 | QStringList defaultPlugins = availablePlugins(); |
926 | for (const QString &plugin : blacklist) { |
927 | defaultPlugins.removeAll(t: plugin); |
928 | } |
929 | |
930 | return defaultPlugins; |
931 | } |
932 | |
933 | QStringList PreviewJob::supportedMimeTypes() |
934 | { |
935 | QStringList result; |
936 | const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins(); |
937 | for (const KPluginMetaData &plugin : plugins) { |
938 | result += plugin.mimeTypes(); |
939 | } |
940 | return result; |
941 | } |
942 | |
943 | PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) |
944 | { |
945 | return new PreviewJob(items, size, enabledPlugins); |
946 | } |
947 | |
948 | #include "moc_previewjob.cpp" |
949 | |