1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
4 SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
5 SPDX-FileCopyrightText: 2003-2005 David Faure <faure@kde.org>
6 SPDX-FileCopyrightText: 2001-2006 Michael Brade <brade@kde.org>
7 SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@kde.org>
8
9 SPDX-License-Identifier: LGPL-2.0-or-later
10*/
11
12#include "kcoredirlister.h"
13#include "kcoredirlister_p.h"
14
15#include "../utils_p.h"
16#include "kiocoredebug.h"
17#include "kmountpoint.h"
18#include <kio/listjob.h>
19
20#include <KJobUiDelegate>
21#include <KLocalizedString>
22
23#include <QDir>
24#include <QFile>
25#include <QFileInfo>
26#include <QMimeDatabase>
27#include <QRegularExpression>
28#include <QTextStream>
29#include <QThreadStorage>
30
31#include <QLoggingCategory>
32Q_DECLARE_LOGGING_CATEGORY(KIO_CORE_DIRLISTER)
33Q_LOGGING_CATEGORY(KIO_CORE_DIRLISTER, "kf.kio.core.dirlister", QtWarningMsg)
34
35// Enable this to get printDebug() called often, to see the contents of the cache
36// #define DEBUG_CACHE
37
38// Make really sure it doesn't get activated in the final build
39#ifdef NDEBUG
40#undef DEBUG_CACHE
41#endif
42
43QThreadStorage<KCoreDirListerCache> s_kDirListerCache;
44
45KCoreDirListerCache::KCoreDirListerCache()
46 : itemsCached(10)
47 , // keep the last 10 directories around
48 m_cacheHiddenFiles(10) // keep the last 10 ".hidden" files around
49{
50 qCDebug(KIO_CORE_DIRLISTER);
51
52 connect(sender: &pendingUpdateTimer, signal: &QTimer::timeout, context: this, slot: &KCoreDirListerCache::processPendingUpdates);
53 pendingUpdateTimer.setSingleShot(true);
54
55 connect(sender: KDirWatch::self(), signal: &KDirWatch::dirty, context: this, slot: &KCoreDirListerCache::slotFileDirty);
56 connect(sender: KDirWatch::self(), signal: &KDirWatch::created, context: this, slot: &KCoreDirListerCache::slotFileCreated);
57 connect(sender: KDirWatch::self(), signal: &KDirWatch::deleted, context: this, slot: &KCoreDirListerCache::slotFileDeleted);
58
59#ifndef KIO_ANDROID_STUB
60 kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this);
61 connect(sender: kdirnotify, signal: &org::kde::KDirNotify::FileRenamedWithLocalPath, context: this, slot: &KCoreDirListerCache::slotFileRenamed);
62 connect(sender: kdirnotify, signal: &org::kde::KDirNotify::FilesAdded, context: this, slot: &KCoreDirListerCache::slotFilesAdded);
63 connect(sender: kdirnotify, signal: &org::kde::KDirNotify::FilesChanged, context: this, slot: &KCoreDirListerCache::slotFilesChanged);
64 connect(sender: kdirnotify, signal: &org::kde::KDirNotify::FilesRemoved, context: this, slot: qOverload<const QStringList &>(&KCoreDirListerCache::slotFilesRemoved));
65#endif
66}
67
68KCoreDirListerCache::~KCoreDirListerCache()
69{
70 qCDebug(KIO_CORE_DIRLISTER);
71
72 qDeleteAll(c: itemsInUse);
73 itemsInUse.clear();
74
75 itemsCached.clear();
76 directoryData.clear();
77 m_cacheHiddenFiles.clear();
78
79 if (KDirWatch::exists()) {
80 KDirWatch::self()->disconnect(receiver: this);
81 }
82}
83
84// setting _reload to true will emit the old files and
85// call updateDirectory
86bool KCoreDirListerCache::listDir(KCoreDirLister *lister, const QUrl &dirUrl, bool _keep, bool _reload)
87{
88 QUrl _url(dirUrl);
89 _url.setPath(path: QDir::cleanPath(path: _url.path())); // kill consecutive slashes
90
91 // like this we don't have to worry about trailing slashes any further
92 _url = _url.adjusted(options: QUrl::StripTrailingSlash);
93
94 QString resolved;
95 if (_url.isLocalFile()) {
96 // Resolve symlinks (#213799)
97 const QString local = _url.toLocalFile();
98 resolved = QFileInfo(local).canonicalFilePath();
99 if (local != resolved) {
100 canonicalUrls[QUrl::fromLocalFile(localfile: resolved)].append(t: _url);
101 }
102 // TODO: remove entry from canonicalUrls again in forgetDirs
103 // Note: this is why we use a QStringList value in there rather than a std::set:
104 // we can just remove one entry and not have to worry about other dirlisters
105 // (the non-unicity of the stringlist gives us the refcounting, basically).
106 }
107
108 qCDebug(KIO_CORE_DIRLISTER) << lister << "url=" << _url << "keep=" << _keep << "reload=" << _reload;
109#ifdef DEBUG_CACHE
110 printDebug();
111#endif
112
113 if (!_keep) {
114 // stop any running jobs for lister
115 stop(lister, silent: true /*silent*/);
116
117 // clear our internal list for lister
118 forgetDirs(lister);
119
120 lister->d->rootFileItem = KFileItem();
121 } else if (lister->d->lstDirs.contains(t: _url)) {
122 // stop the job listing _url for this lister
123 stopListingUrl(lister, _url, silent: true /*silent*/);
124
125 // remove the _url as well, it will be added in a couple of lines again!
126 // forgetDirs with three args does not do this
127 // TODO: think about moving this into forgetDirs
128 lister->d->lstDirs.removeAll(t: _url);
129
130 // clear _url for lister
131 forgetDirs(lister, _url, notify: true);
132
133 if (lister->d->url == _url) {
134 lister->d->rootFileItem = KFileItem();
135 }
136 }
137
138 lister->d->complete = false;
139
140 lister->d->lstDirs.append(t: _url);
141
142 if (lister->d->url.isEmpty() || !_keep) { // set toplevel URL only if not set yet
143 lister->d->url = _url;
144 }
145
146 DirItem *itemU = itemsInUse.value(key: _url);
147
148 KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; // find or insert
149
150 if (dirData.listersCurrentlyListing.isEmpty()) {
151 // if there is an update running for _url already we get into
152 // the following case - it will just be restarted by updateDirectory().
153
154 dirData.listersCurrentlyListing.append(t: lister);
155
156 DirItem *itemFromCache = nullptr;
157 if (itemU || (!_reload && (itemFromCache = itemsCached.take(key: _url)))) {
158 if (itemU) {
159 qCDebug(KIO_CORE_DIRLISTER) << "Entry already in use:" << _url;
160 // if _reload is set, then we'll emit cached items and then updateDirectory.
161 } else {
162 qCDebug(KIO_CORE_DIRLISTER) << "Entry in cache:" << _url;
163 itemsInUse.insert(key: _url, value: itemFromCache);
164 itemU = itemFromCache;
165 }
166 if (lister->d->autoUpdate) {
167 itemU->incAutoUpdate();
168 }
169 if (itemFromCache && itemFromCache->watchedWhileInCache) {
170 // item is promoted from cache, update item autoupdate refcount accordingly
171 itemFromCache->watchedWhileInCache = false;
172 itemFromCache->decAutoUpdate();
173 }
174
175 Q_EMIT lister->started(dirUrl: _url);
176
177 // List items from the cache in a delayed manner, just like things would happen
178 // if we were not using the cache.
179 new KCoreDirListerPrivate::CachedItemsJob(lister, _url, _reload);
180
181 } else {
182 // dir not in cache or _reload is true
183 if (_reload) {
184 qCDebug(KIO_CORE_DIRLISTER) << "Reloading directory:" << _url;
185 itemsCached.remove(key: _url);
186 } else {
187 qCDebug(KIO_CORE_DIRLISTER) << "Listing directory:" << _url;
188 }
189
190 itemU = new DirItem(_url, resolved);
191 itemsInUse.insert(key: _url, value: itemU);
192 if (lister->d->autoUpdate) {
193 itemU->incAutoUpdate();
194 }
195
196 KIO::ListJob *job = KIO::listDir(url: _url, flags: KIO::HideProgressInfo);
197 if (lister->requestMimeTypeWhileListing()) {
198 job->addMetaData(QStringLiteral("details"), value: QString::number(KIO::StatDefaultDetails | KIO::StatMimeType));
199 }
200 runningListJobs.insert(key: job, value: KIO::UDSEntryList());
201
202 lister->jobStarted(job);
203 lister->d->connectJob(job);
204
205 connect(sender: job, signal: &KIO::ListJob::entries, context: this, slot: &KCoreDirListerCache::slotEntries);
206 connect(sender: job, signal: &KJob::result, context: this, slot: &KCoreDirListerCache::slotResult);
207 connect(sender: job, signal: &KIO::ListJob::redirection, context: this, slot: &KCoreDirListerCache::slotRedirection);
208
209 Q_EMIT lister->started(dirUrl: _url);
210
211 qCDebug(KIO_CORE_DIRLISTER) << "Entry now being listed by" << dirData.listersCurrentlyListing;
212 }
213 } else {
214 qCDebug(KIO_CORE_DIRLISTER) << "Entry currently being listed:" << _url << "by" << dirData.listersCurrentlyListing;
215#ifdef DEBUG_CACHE
216 printDebug();
217#endif
218
219 Q_EMIT lister->started(dirUrl: _url);
220
221 // Maybe listersCurrentlyListing/listersCurrentlyHolding should be QSets?
222 Q_ASSERT(!dirData.listersCurrentlyListing.contains(lister));
223 dirData.listersCurrentlyListing.append(t: lister);
224
225 // a new lister is listing this _url, incr watch refcount
226 if (lister->d->autoUpdate) {
227 itemU->incAutoUpdate();
228 }
229
230 KIO::ListJob *job = jobForUrl(url: _url);
231 // job will be 0 if we were listing from cache rather than listing from a kio job.
232 if (job) {
233 lister->jobStarted(job);
234 lister->d->connectJob(job);
235 }
236 Q_ASSERT(itemU);
237
238 // List existing items in a delayed manner, just like things would happen
239 // if we were not using the cache.
240 qCDebug(KIO_CORE_DIRLISTER) << "Listing" << itemU->lstItems.count() << "cached items soon";
241 auto *cachedItemsJob = new KCoreDirListerPrivate::CachedItemsJob(lister, _url, _reload);
242 if (job) {
243 // The ListJob will take care of emitting completed.
244 // ### If it finishes before the CachedItemsJob, then we'll emit cached items after completed(), not sure how bad this is.
245 cachedItemsJob->setEmitCompleted(false);
246 }
247
248#ifdef DEBUG_CACHE
249 printDebug();
250#endif
251 }
252
253 return true;
254}
255
256KCoreDirListerPrivate::CachedItemsJob *KCoreDirListerPrivate::cachedItemsJobForUrl(const QUrl &url) const
257{
258 for (CachedItemsJob *job : m_cachedItemsJobs) {
259 if (job->url() == url) {
260 return job;
261 }
262 }
263 return nullptr;
264}
265
266KCoreDirListerPrivate::CachedItemsJob::CachedItemsJob(KCoreDirLister *lister, const QUrl &url, bool reload)
267 : KJob(lister)
268 , m_lister(lister)
269 , m_url(url)
270 , m_reload(reload)
271 , m_emitCompleted(true)
272{
273 qCDebug(KIO_CORE_DIRLISTER) << "Creating CachedItemsJob" << this << "for lister" << lister << url;
274 if (lister->d->cachedItemsJobForUrl(url)) {
275 qCWarning(KIO_CORE) << "Lister" << lister << "has a cached items job already for" << url;
276 }
277 lister->d->m_cachedItemsJobs.append(t: this);
278 setAutoDelete(true);
279 start();
280}
281
282// Called by start() via QueuedConnection
283void KCoreDirListerPrivate::CachedItemsJob::done()
284{
285 if (!m_lister) { // job was already killed, but waiting deletion due to deleteLater
286 return;
287 }
288 s_kDirListerCache.localData().emitItemsFromCache(job: this, lister: m_lister, url: m_url, reload: m_reload, emitCompleted: m_emitCompleted);
289 emitResult();
290}
291
292bool KCoreDirListerPrivate::CachedItemsJob::doKill()
293{
294 qCDebug(KIO_CORE_DIRLISTER) << this;
295 s_kDirListerCache.localData().forgetCachedItemsJob(job: this, lister: m_lister, url: m_url);
296 if (!property(name: "_kdlc_silent").toBool()) {
297 Q_EMIT m_lister->listingDirCanceled(dirUrl: m_url);
298
299 Q_EMIT m_lister->canceled();
300 }
301 m_lister = nullptr;
302 return true;
303}
304
305void KCoreDirListerCache::emitItemsFromCache(KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob,
306 KCoreDirLister *lister,
307 const QUrl &_url,
308 bool _reload,
309 bool _emitCompleted)
310{
311 lister->d->complete = false;
312
313 DirItem *itemU = s_kDirListerCache.localData().itemsInUse.value(key: _url);
314 if (!itemU) {
315 qCWarning(KIO_CORE) << "Can't find item for directory" << _url << "anymore";
316 } else {
317 const QList<KFileItem> items = itemU->lstItems;
318 const KFileItem rootItem = itemU->rootItem;
319 _reload = _reload || !itemU->complete;
320
321 if (lister->d->rootFileItem.isNull() && !rootItem.isNull() && lister->d->url == _url) {
322 lister->d->rootFileItem = rootItem;
323 }
324 if (!items.isEmpty()) {
325 qCDebug(KIO_CORE_DIRLISTER) << "emitting" << items.count() << "for lister" << lister;
326 lister->d->addNewItems(directoryUrl: _url, items);
327 lister->d->emitItems();
328 }
329 }
330
331 forgetCachedItemsJob(job: cachedItemsJob, lister, url: _url);
332
333 // Emit completed, unless we were told not to,
334 // or if listDir() was called while another directory listing for this dir was happening,
335 // so we "joined" it. We detect that using jobForUrl to ensure it's a real ListJob,
336 // not just a lister-specific CachedItemsJob (which wouldn't emit completed for us).
337 if (_emitCompleted) {
338 lister->d->complete = true;
339
340 Q_EMIT lister->listingDirCompleted(dirUrl: _url);
341 Q_EMIT lister->completed();
342
343 if (_reload) {
344 updateDirectory(dir: _url);
345 }
346 }
347}
348
349void KCoreDirListerCache::forgetCachedItemsJob(KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url)
350{
351 // Modifications to data structures only below this point;
352 // so that addNewItems is called with a consistent state
353
354 lister->d->m_cachedItemsJobs.removeAll(t: cachedItemsJob);
355
356 KCoreDirListerCacheDirectoryData &dirData = directoryData[_url];
357 Q_ASSERT(dirData.listersCurrentlyListing.contains(lister));
358
359 KIO::ListJob *listJob = jobForUrl(url: _url);
360 if (!listJob) {
361 Q_ASSERT(!dirData.listersCurrentlyHolding.contains(lister));
362 qCDebug(KIO_CORE_DIRLISTER) << "Moving from listing to holding, because no more job" << lister << _url;
363 dirData.listersCurrentlyHolding.append(t: lister);
364 dirData.listersCurrentlyListing.removeAll(t: lister);
365 } else {
366 qCDebug(KIO_CORE_DIRLISTER) << "Still having a listjob" << listJob << ", so not moving to currently-holding.";
367 }
368}
369
370void KCoreDirListerCache::stop(KCoreDirLister *lister, bool silent)
371{
372 qCDebug(KIO_CORE_DIRLISTER) << "lister:" << lister << "silent=" << silent;
373
374 const QList<QUrl> urls = lister->d->lstDirs;
375 for (const QUrl &url : urls) {
376 stopListingUrl(lister, url: url, silent);
377 }
378}
379
380void KCoreDirListerCache::stopListingUrl(KCoreDirLister *lister, const QUrl &_u, bool silent)
381{
382 QUrl url(_u);
383 url = url.adjusted(options: QUrl::StripTrailingSlash);
384
385 KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(url);
386 if (cachedItemsJob) {
387 if (silent) {
388 cachedItemsJob->setProperty(name: "_kdlc_silent", value: true);
389 }
390 cachedItemsJob->kill(); // removes job from list, too
391 }
392
393 // TODO: consider to stop all the "child jobs" of url as well
394 qCDebug(KIO_CORE_DIRLISTER) << lister << " url=" << url;
395
396 const auto dirit = directoryData.find(key: url);
397 if (dirit == directoryData.end()) {
398 return;
399 }
400 KCoreDirListerCacheDirectoryData &dirData = dirit.value();
401 if (dirData.listersCurrentlyListing.contains(t: lister)) {
402 qCDebug(KIO_CORE_DIRLISTER) << " found lister" << lister << "in list - for" << url;
403 if (dirData.listersCurrentlyListing.count() == 1) {
404 // This was the only dirlister interested in the list job -> kill the job
405 stopListJob(url, silent);
406 } else {
407 // Leave the job running for the other dirlisters, just unsubscribe us.
408 dirData.listersCurrentlyListing.removeAll(t: lister);
409 if (!silent) {
410 Q_EMIT lister->canceled();
411 Q_EMIT lister->listingDirCanceled(dirUrl: url);
412 }
413 }
414 }
415}
416
417// Helper for stop() and stopListingUrl()
418void KCoreDirListerCache::stopListJob(const QUrl &url, bool silent)
419{
420 // Old idea: if it's an update job, let's just leave the job running.
421 // After all, update jobs do run for "listersCurrentlyHolding",
422 // so there's no reason to kill them just because @p lister is now a holder.
423
424 // However it could be a long-running non-local job (e.g. filenamesearch), which
425 // the user wants to abort, and which will never be used for updating...
426 // And in any case slotEntries/slotResult is not meant to be called by update jobs.
427 // So, change of plan, let's kill it after all, in a way that triggers slotResult/slotUpdateResult.
428
429 KIO::ListJob *job = jobForUrl(url);
430 if (job) {
431 qCDebug(KIO_CORE_DIRLISTER) << "Killing list job" << job << "for" << url;
432 if (silent) {
433 job->setProperty(name: "_kdlc_silent", value: true);
434 }
435 job->kill(verbosity: KJob::EmitResult);
436 }
437}
438
439void KCoreDirListerCache::setAutoUpdate(KCoreDirLister *lister, bool enable)
440{
441 // IMPORTANT: this method does not check for the current autoUpdate state!
442
443 for (const QUrl &url : std::as_const(t&: lister->d->lstDirs)) {
444 DirItem *dirItem = itemsInUse.value(key: url);
445 Q_ASSERT(dirItem);
446 if (enable) {
447 dirItem->incAutoUpdate();
448 } else {
449 dirItem->decAutoUpdate();
450 }
451 }
452}
453
454void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister)
455{
456 qCDebug(KIO_CORE_DIRLISTER) << lister;
457
458 Q_EMIT lister->clear();
459 // clear lister->d->lstDirs before calling forgetDirs(), so that
460 // it doesn't contain things that itemsInUse doesn't. When emitting
461 // the canceled signals, lstDirs must not contain anything that
462 // itemsInUse does not contain. (otherwise it might crash in findByName()).
463 const QList<QUrl> lstDirsCopy = lister->d->lstDirs;
464 lister->d->lstDirs.clear();
465
466 qCDebug(KIO_CORE_DIRLISTER) << "Iterating over dirs" << lstDirsCopy;
467 for (const QUrl &dir : lstDirsCopy) {
468 forgetDirs(lister, url: dir, notify: false);
469 }
470}
471
472static bool manually_mounted(const QString &path, const KMountPoint::List &possibleMountPoints)
473{
474 KMountPoint::Ptr mp = possibleMountPoints.findByPath(path);
475 if (!mp) { // not listed in fstab -> yes, manually mounted
476 if (possibleMountPoints.isEmpty()) { // no fstab at all -> don't assume anything
477 return false;
478 }
479 return true;
480 }
481 // noauto -> manually mounted. Otherwise, mounted at boot time, won't be unmounted any time soon hopefully.
482 return mp->mountOptions().contains(str: QLatin1String("noauto"));
483}
484
485void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, bool notify)
486{
487 qCDebug(KIO_CORE_DIRLISTER) << lister << " _url: " << _url;
488
489 const QUrl url = _url.adjusted(options: QUrl::StripTrailingSlash);
490
491 DirectoryDataHash::iterator dit = directoryData.find(key: url);
492 if (dit == directoryData.end()) {
493 return;
494 }
495 KCoreDirListerCacheDirectoryData &dirData = *dit;
496 dirData.listersCurrentlyHolding.removeAll(t: lister);
497
498 // This lister doesn't care for updates running in <url> anymore
499 KIO::ListJob *job = jobForUrl(url);
500 if (job) {
501 lister->d->jobDone(job);
502 }
503
504 DirItem *item = itemsInUse.value(key: url);
505 Q_ASSERT(item);
506 bool insertIntoCache = false;
507
508 if (dirData.listersCurrentlyHolding.isEmpty() && dirData.listersCurrentlyListing.isEmpty()) {
509 // item not in use anymore -> move into cache if complete
510 directoryData.erase(it: dit);
511 itemsInUse.remove(key: url);
512
513 // this job is a running update which nobody cares about anymore
514 if (job) {
515 killJob(job);
516 qCDebug(KIO_CORE_DIRLISTER) << "Killing update job for " << url;
517
518 // Well, the user of KCoreDirLister doesn't really care that we're stopping
519 // a background-running job from a previous URL (in listDir) -> commented out.
520 // stop() already emitted canceled.
521 // emit lister->canceled( url );
522 if (lister->d->numJobs() == 0) {
523 lister->d->complete = true;
524 // emit lister->canceled();
525 }
526 }
527
528 if (notify) {
529 lister->d->lstDirs.removeAll(t: url);
530 Q_EMIT lister->clearDir(dirUrl: url);
531 }
532
533 insertIntoCache = item->complete;
534 if (insertIntoCache) {
535 // TODO(afiestas): remove use of KMountPoint+manually_mounted and port to Solid:
536 // 1) find Volume for the local path "item->url.toLocalFile()" (which could be anywhere
537 // under the mount point) -- probably needs a new operator in libsolid query parser
538 // 2) [**] becomes: if (Drive is hotpluggable or Volume is removable) "set to dirty" else "keep watch"
539 const KMountPoint::List possibleMountPoints = KMountPoint::possibleMountPoints(infoNeeded: KMountPoint::NeedMountOptions);
540
541 // Should we forget the dir for good, or keep a watch on it?
542 // Generally keep a watch, except when it would prevent
543 // unmounting a removable device (#37780)
544 const bool isLocal = item->url.isLocalFile();
545 bool isManuallyMounted = false;
546 bool containsManuallyMounted = false;
547 if (isLocal) {
548 isManuallyMounted = manually_mounted(path: item->url.toLocalFile(), possibleMountPoints);
549 if (!isManuallyMounted) {
550 // Look for a manually-mounted directory inside
551 // If there's one, we can't keep a watch either, FAM would prevent unmounting the CDROM
552 // I hope this isn't too slow
553 auto kit = item->lstItems.constBegin();
554 const auto kend = item->lstItems.constEnd();
555 for (; kit != kend && !containsManuallyMounted; ++kit) {
556 if ((*kit).isDir() && manually_mounted(path: (*kit).url().toLocalFile(), possibleMountPoints)) {
557 containsManuallyMounted = true;
558 }
559 }
560 }
561 }
562
563 if (isManuallyMounted || containsManuallyMounted) { // [**]
564 qCDebug(KIO_CORE_DIRLISTER) << "Not adding a watch on " << item->url << " because it "
565 << (isManuallyMounted ? "is manually mounted" : "contains a manually mounted subdir");
566 item->complete = false; // set to "dirty"
567 } else {
568 item->incAutoUpdate(); // keep watch, incr refcount to account for cache
569 item->watchedWhileInCache = true;
570 }
571 } else {
572 delete item;
573 item = nullptr;
574 }
575 }
576
577 if (item && lister->d->autoUpdate) {
578 item->decAutoUpdate();
579 }
580
581 // Inserting into QCache must be done last, since it might delete the item
582 if (item && insertIntoCache) {
583 qCDebug(KIO_CORE_DIRLISTER) << lister << "item moved into cache:" << url;
584 itemsCached.insert(key: url, object: item);
585 }
586}
587
588void KCoreDirListerCache::updateDirectory(const QUrl &_dir)
589{
590 qCDebug(KIO_CORE_DIRLISTER) << _dir;
591
592 QUrl dir = _dir.adjusted(options: QUrl::StripTrailingSlash);
593 if (!checkUpdate(url: dir)) {
594 auto parentDir = dir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
595 if (checkUpdate(url: parentDir)) {
596 // if the parent is in use, update it instead
597 dir = parentDir;
598 } else {
599 return;
600 }
601 }
602
603 // A job can be running to
604 // - only list a new directory: the listers are in listersCurrentlyListing
605 // - only update a directory: the listers are in listersCurrentlyHolding
606 // - update a currently running listing: the listers are in both
607
608 KCoreDirListerCacheDirectoryData &dirData = directoryData[dir];
609 const QList<KCoreDirLister *> listers = dirData.listersCurrentlyListing;
610 const QList<KCoreDirLister *> holders = dirData.listersCurrentlyHolding;
611
612 qCDebug(KIO_CORE_DIRLISTER) << dir << "listers=" << listers << "holders=" << holders;
613
614 bool killed = false;
615 KIO::ListJob *job = jobForUrl(url: dir);
616 if (job) {
617 // the job is running already, tell it to do another update at the end
618 // (don't kill it, we would keep doing that during a long download to a slow sshfs mount)
619 job->setProperty(name: "need_another_update", value: true);
620 return;
621 } else {
622 // Emit any cached items.
623 // updateDirectory() is about the diff compared to the cached items...
624 for (const KCoreDirLister *kdl : listers) {
625 KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob = kdl->d->cachedItemsJobForUrl(url: dir);
626 if (cachedItemsJob) {
627 cachedItemsJob->setEmitCompleted(false);
628 cachedItemsJob->done(); // removes from cachedItemsJobs list
629 delete cachedItemsJob;
630 killed = true;
631 }
632 }
633 }
634 qCDebug(KIO_CORE_DIRLISTER) << "Killed=" << killed;
635
636 // we don't need to emit canceled signals since we only replaced the job,
637 // the listing is continuing.
638
639 if (!(listers.isEmpty() || killed)) {
640 qCWarning(KIO_CORE) << "The unexpected happened.";
641 qCWarning(KIO_CORE) << "listers for" << dir << "=" << listers;
642 qCWarning(KIO_CORE) << "job=" << job;
643 for (const KCoreDirLister *kdl : listers) {
644 qCDebug(KIO_CORE_DIRLISTER) << "lister" << kdl << "m_cachedItemsJobs=" << kdl->d->m_cachedItemsJobs;
645 }
646#ifndef NDEBUG
647 printDebug();
648#endif
649 }
650 Q_ASSERT(listers.isEmpty() || killed);
651
652 job = KIO::listDir(url: dir, flags: KIO::HideProgressInfo);
653 runningListJobs.insert(key: job, value: KIO::UDSEntryList());
654
655 const bool requestFromListers = std::any_of(first: listers.cbegin(), last: listers.cend(), pred: [](KCoreDirLister *lister) {
656 return lister->requestMimeTypeWhileListing();
657 });
658 const bool requestFromholders = std::any_of(first: holders.cbegin(), last: holders.cend(), pred: [](KCoreDirLister *lister) {
659 return lister->requestMimeTypeWhileListing();
660 });
661
662 if (requestFromListers || requestFromholders) {
663 job->addMetaData(QStringLiteral("details"), value: QString::number(KIO::StatDefaultDetails | KIO::StatMimeType));
664 }
665
666 connect(sender: job, signal: &KIO::ListJob::entries, context: this, slot: &KCoreDirListerCache::slotUpdateEntries);
667 connect(sender: job, signal: &KJob::result, context: this, slot: &KCoreDirListerCache::slotUpdateResult);
668
669 qCDebug(KIO_CORE_DIRLISTER) << "update started in" << dir;
670
671 for (KCoreDirLister *kdl : listers) {
672 kdl->jobStarted(job);
673 }
674
675 if (!holders.isEmpty()) {
676 if (!killed) {
677 for (KCoreDirLister *kdl : holders) {
678 kdl->jobStarted(job);
679 Q_EMIT kdl->started(dirUrl: dir);
680 }
681 } else {
682 for (KCoreDirLister *kdl : holders) {
683 kdl->jobStarted(job);
684 }
685 }
686 }
687}
688
689bool KCoreDirListerCache::checkUpdate(const QUrl &_dir)
690{
691 if (!itemsInUse.contains(key: _dir)) {
692 DirItem *item = itemsCached[_dir];
693 if (item && item->complete) {
694 item->complete = false;
695 item->watchedWhileInCache = false;
696 item->decAutoUpdate();
697 qCDebug(KIO_CORE_DIRLISTER) << "directory " << _dir << " not in use, marked dirty.";
698 }
699 // else
700 qCDebug(KIO_CORE_DIRLISTER) << "aborted, directory " << _dir << " not in cache.";
701 return false;
702 } else {
703 return true;
704 }
705}
706
707KFileItem KCoreDirListerCache::itemForUrl(const QUrl &url) const
708{
709 return findByUrl(lister: nullptr, url);
710}
711
712KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir) const
713{
714 const QUrl url = dir.adjusted(options: QUrl::StripTrailingSlash);
715 DirItem *item = itemsInUse.value(key: url);
716 if (!item) {
717 item = itemsCached[url];
718 }
719 return item;
720}
721
722QList<KFileItem> *KCoreDirListerCache::itemsForDir(const QUrl &dir) const
723{
724 DirItem *item = dirItemForUrl(dir);
725 return item ? &item->lstItems : nullptr;
726}
727
728KFileItem KCoreDirListerCache::findByName(const KCoreDirLister *lister, const QString &_name) const
729{
730 Q_ASSERT(lister);
731
732 auto isMatch = [&_name](const KFileItem &item) {
733 return _name == item.name();
734 };
735
736 for (const auto &dirUrl : std::as_const(t&: lister->d->lstDirs)) {
737 DirItem *dirItem = itemsInUse.value(key: dirUrl);
738 Q_ASSERT(dirItem);
739
740 auto it = std::find_if(first: dirItem->lstItems.cbegin(), last: dirItem->lstItems.cend(), pred: isMatch);
741 if (it != dirItem->lstItems.cend()) {
742 return *it;
743 }
744 }
745
746 return {};
747}
748
749KFileItem KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const
750{
751 QUrl url(_u);
752 url = url.adjusted(options: QUrl::StripTrailingSlash);
753
754 const QUrl parentDir = url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
755 DirItem *dirItem = dirItemForUrl(dir: parentDir);
756 if (dirItem) {
757 // If lister is set, check that it contains this dir
758 if (!lister || lister->d->lstDirs.contains(t: parentDir)) {
759 // Binary search
760 auto it = std::lower_bound(first: dirItem->lstItems.begin(), last: dirItem->lstItems.end(), val: url);
761 if (it != dirItem->lstItems.end() && it->url() == url) {
762 return *it;
763 }
764 }
765 }
766
767 // Maybe _u is a directory itself? (see KDirModelTest::testChmodDirectory)
768 // We check this last, though, we prefer returning a kfileitem with an actual
769 // name if possible (and we make it '.' for root items later).
770 dirItem = dirItemForUrl(dir: url);
771 if (dirItem && !dirItem->rootItem.isNull() && dirItem->rootItem.url() == url) {
772 // If lister is set, check that it contains this dir
773 if (!lister || lister->d->lstDirs.contains(t: url)) {
774 return dirItem->rootItem;
775 }
776 }
777
778 return KFileItem();
779}
780
781void KCoreDirListerCache::slotFilesAdded(const QString &dir /*url*/) // from KDirNotify signals
782{
783 QUrl urlDir(dir);
784 itemsAddedInDirectory(url: urlDir);
785}
786
787void KCoreDirListerCache::itemsAddedInDirectory(const QUrl &urlDir)
788{
789 qCDebug(KIO_CORE_DIRLISTER) << urlDir;
790 const QList<QUrl> urls = directoriesForCanonicalPath(dir: urlDir);
791 for (const QUrl &u : urls) {
792 updateDirectory(dir: u);
793 }
794}
795
796void KCoreDirListerCache::slotFilesRemoved(const QStringList &fileList) // from KDirNotify signals
797{
798 slotFilesRemoved(urls: QUrl::fromStringList(uris: fileList));
799}
800
801void KCoreDirListerCache::slotFilesRemoved(const QList<QUrl> &fileList)
802{
803 qCDebug(KIO_CORE_DIRLISTER) << fileList.count();
804 // Group notifications by parent dirs (usually there would be only one parent dir)
805 QMap<QUrl, KFileItemList> removedItemsByDir;
806 QList<QUrl> deletedSubdirs;
807
808 for (const QUrl &url : fileList) {
809 const QList<QUrl> dirUrls = directoriesForCanonicalPath(dir: url);
810 for (const QUrl &dir : dirUrls) {
811 DirItem *dirItem = dirItemForUrl(dir); // is it a listed directory?
812 if (dirItem) {
813 deletedSubdirs.append(t: dir);
814 if (!dirItem->rootItem.isNull()) {
815 removedItemsByDir[url].append(t: dirItem->rootItem);
816 }
817 }
818 }
819
820 const QUrl parentDir = url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
821 const QList<QUrl> parentDirUrls = directoriesForCanonicalPath(dir: parentDir);
822 for (const QUrl &dir : parentDirUrls) {
823 DirItem *dirItem = dirItemForUrl(dir);
824 if (!dirItem) {
825 continue;
826 }
827
828 const auto dirItemIt = std::find_if(first: dirItem->lstItems.cbegin(), last: dirItem->lstItems.cend(), pred: [&url](const KFileItem &fitem) {
829 return fitem.name() == url.fileName();
830 });
831 if (dirItemIt != dirItem->lstItems.cend()) {
832 const KFileItem fileitem = *dirItemIt;
833 removedItemsByDir[dir].append(t: fileitem);
834 // If we found a fileitem, we can test if it's a dir. If not, we'll go to deleteDir just in case.
835 if (fileitem.isNull() || fileitem.isDir()) {
836 deletedSubdirs.append(t: url);
837 }
838 dirItem->lstItems.erase(pos: dirItemIt); // remove fileitem from list
839 }
840 }
841 }
842
843 for (auto rit = removedItemsByDir.constBegin(), cend = removedItemsByDir.constEnd(); rit != cend; ++rit) {
844 // Tell the views about it before calling deleteDir.
845 // They might need the subdirs' file items (see the dirtree).
846 auto dit = directoryData.constFind(key: rit.key());
847 if (dit != directoryData.constEnd()) {
848 itemsDeleted(listers: (*dit).listersCurrentlyHolding, deletedItems: rit.value());
849 }
850 }
851
852 for (const QUrl &url : std::as_const(t&: deletedSubdirs)) {
853 // in case of a dir, check if we have any known children, there's much to do in that case
854 // (stopping jobs, removing dirs from cache etc.)
855 deleteDir(dirUrl: url);
856 }
857}
858
859void KCoreDirListerCache::slotFilesChanged(const QStringList &fileList) // from KDirNotify signals
860{
861 qCDebug(KIO_CORE_DIRLISTER) << fileList;
862 QList<QUrl> dirsToUpdate;
863 for (const QString &fileUrl : fileList) {
864 const QUrl url(fileUrl);
865 const KFileItem &fileitem = findByUrl(lister: nullptr, u: url);
866 if (fileitem.isNull()) {
867 qCDebug(KIO_CORE_DIRLISTER) << "item not found for" << url;
868 continue;
869 }
870 if (url.isLocalFile()) {
871 pendingUpdates.insert(x: url.toLocalFile()); // delegate the work to processPendingUpdates
872 } else {
873 pendingRemoteUpdates.insert(x: fileitem);
874 // For remote files, we won't be able to figure out the new information,
875 // we have to do a update (directory listing)
876 const QUrl dir = url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
877 if (!dirsToUpdate.contains(t: dir)) {
878 dirsToUpdate.prepend(t: dir);
879 }
880 }
881 }
882
883 for (const QUrl &dirUrl : std::as_const(t&: dirsToUpdate)) {
884 updateDirectory(dir: dirUrl);
885 }
886 // ## TODO problems with current jobs listing/updating that dir
887 // ( see kde-2.2.2's kdirlister )
888
889 processPendingUpdates();
890}
891
892void KCoreDirListerCache::slotFileRenamed(const QString &_src, const QString &_dst, const QString &dstPath) // from KDirNotify signals
893{
894 QUrl src(_src);
895 QUrl dst(_dst);
896 qCDebug(KIO_CORE_DIRLISTER) << src << "->" << dst;
897#ifdef DEBUG_CACHE
898 printDebug();
899#endif
900
901 QUrl oldurl = src.adjusted(options: QUrl::StripTrailingSlash);
902 KFileItem fileitem = findByUrl(lister: nullptr, u: oldurl);
903 if (fileitem.isNull()) {
904 qCDebug(KIO_CORE_DIRLISTER) << "Item not found:" << oldurl;
905 return;
906 }
907
908 const KFileItem oldItem = fileitem;
909
910 // Dest already exists? Was overwritten then (testcase: #151851)
911 // We better emit it as deleted -before- doing the renaming, otherwise
912 // the "update" mechanism will emit the old one as deleted and
913 // kdirmodel will delete the new (renamed) one!
914 const KFileItem &existingDestItem = findByUrl(lister: nullptr, u: dst);
915 if (!existingDestItem.isNull()) {
916 qCDebug(KIO_CORE_DIRLISTER) << dst << "already existed, let's delete it";
917 slotFilesRemoved(fileList: QList<QUrl>{dst});
918 }
919
920 // If the item had a UDS_URL as well as UDS_NAME set, the user probably wants
921 // to be updating the name only (since they can't see the URL).
922 // Check to see if a URL exists, and if so, if only the file part has changed,
923 // only update the name and not the underlying URL.
924 bool nameOnly = !fileitem.entry().stringValue(field: KIO::UDSEntry::UDS_URL).isEmpty();
925 nameOnly = nameOnly && src.adjusted(options: QUrl::RemoveFilename) == dst.adjusted(options: QUrl::RemoveFilename);
926
927 if (!nameOnly && fileitem.isDir()) {
928 renameDir(oldUrl: oldurl, url: dst);
929 // #172945 - if the fileitem was the root item of a DirItem that was just removed from the cache,
930 // then it's a dangling pointer now...
931 fileitem = findByUrl(lister: nullptr, u: oldurl);
932 if (fileitem.isNull()) { // deleted from cache altogether, #188807
933 return;
934 }
935 }
936
937 // Now update the KFileItem representing that file or dir (not exclusive with the above!)
938 if (!oldItem.isLocalFile() && !oldItem.localPath().isEmpty()
939 && dstPath.isEmpty()) { // it uses UDS_LOCAL_PATH and we don't know the new path? needs an update then
940 slotFilesChanged(fileList: QStringList{src.toString()});
941 } else {
942 const QUrl &itemOldUrl = fileitem.url();
943 if (nameOnly) {
944 fileitem.setName(dst.fileName());
945 } else {
946 fileitem.setUrl(dst);
947 }
948
949 if (!dstPath.isEmpty()) {
950 fileitem.setLocalPath(dstPath);
951 }
952
953 fileitem.refreshMimeType();
954 fileitem.determineMimeType();
955 reinsert(item: fileitem, oldUrl: itemOldUrl);
956
957 const std::set<KCoreDirLister *> listers = emitRefreshItem(oldItem, fileitem);
958 for (KCoreDirLister *kdl : listers) {
959 kdl->d->emitItems();
960 }
961 }
962
963#ifdef DEBUG_CACHE
964 printDebug();
965#endif
966}
967
968std::set<KCoreDirLister *> KCoreDirListerCache::emitRefreshItem(const KFileItem &oldItem, const KFileItem &fileitem)
969{
970 qCDebug(KIO_CORE_DIRLISTER) << "old:" << oldItem.name() << oldItem.url() << "new:" << fileitem.name() << fileitem.url();
971 // Look whether this item was shown in any view, i.e. held by any dirlister
972 const QUrl parentDir = oldItem.url().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
973 DirectoryDataHash::iterator dit = directoryData.find(key: parentDir);
974 QList<KCoreDirLister *> listers;
975 // Also look in listersCurrentlyListing, in case the user manages to rename during a listing
976 if (dit != directoryData.end()) {
977 listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing;
978 }
979 if (oldItem.isDir()) {
980 // For a directory, look for dirlisters where it's the root item.
981 dit = directoryData.find(key: oldItem.url());
982 if (dit != directoryData.end()) {
983 listers += (*dit).listersCurrentlyHolding + (*dit).listersCurrentlyListing;
984 }
985 }
986 std::set<KCoreDirLister *> listersToRefresh;
987 for (KCoreDirLister *kdl : std::as_const(t&: listers)) {
988 // deduplicate listers
989 listersToRefresh.insert(x: kdl);
990 }
991 for (KCoreDirLister *kdl : std::as_const(t&: listersToRefresh)) {
992 // For a directory, look for dirlisters where it's the root item.
993 QUrl directoryUrl(oldItem.url());
994 if (oldItem.isDir() && kdl->d->rootFileItem == oldItem) {
995 const KFileItem oldRootItem = kdl->d->rootFileItem;
996 kdl->d->rootFileItem = fileitem;
997 kdl->d->addRefreshItem(directoryUrl, oldItem: oldRootItem, item: fileitem);
998 } else {
999 directoryUrl = directoryUrl.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1000 kdl->d->addRefreshItem(directoryUrl, oldItem, item: fileitem);
1001 }
1002 }
1003 return listersToRefresh;
1004}
1005
1006QList<QUrl> KCoreDirListerCache::directoriesForCanonicalPath(const QUrl &dir) const
1007{
1008 QList<QUrl> urlList = canonicalUrls.value(key: dir);
1009 // make unique
1010 if (urlList.size() > 1) {
1011 std::sort(first: urlList.begin(), last: urlList.end());
1012 auto end_unique = std::unique(first: urlList.begin(), last: urlList.end());
1013 urlList.erase(abegin: end_unique, aend: urlList.end());
1014 }
1015
1016 QList<QUrl> dirs({dir});
1017 dirs.append(l: urlList);
1018
1019 if (dirs.count() > 1) {
1020 qCDebug(KIO_CORE_DIRLISTER) << dir << "known as" << dirs;
1021 }
1022 return dirs;
1023}
1024
1025// private slots
1026
1027// Called by KDirWatch - usually when a dir we're watching has been modified,
1028// but it can also be called for a file.
1029void KCoreDirListerCache::slotFileDirty(const QString &path)
1030{
1031 qCDebug(KIO_CORE_DIRLISTER) << path;
1032 QUrl url = QUrl::fromLocalFile(localfile: path).adjusted(options: QUrl::StripTrailingSlash);
1033 // File or dir?
1034 bool isDir;
1035 const KFileItem item = itemForUrl(url);
1036
1037 if (!item.isNull()) {
1038 isDir = item.isDir();
1039 } else {
1040 QFileInfo info(path);
1041 if (!info.exists()) {
1042 return; // error
1043 }
1044 isDir = info.isDir();
1045 }
1046
1047 if (isDir) {
1048 const QList<QUrl> urls = directoriesForCanonicalPath(dir: url);
1049 for (const QUrl &dir : urls) {
1050 handleDirDirty(url: dir);
1051 }
1052 }
1053 // Also do this for dirs, e.g. to handle permission changes
1054 const QList<QUrl> urls = directoriesForCanonicalPath(dir: url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1055 for (const QUrl &dir : urls) {
1056 QUrl aliasUrl(dir);
1057 aliasUrl.setPath(path: Utils::concatPaths(path1: aliasUrl.path(), path2: url.fileName()));
1058 handleFileDirty(url: aliasUrl);
1059 }
1060}
1061
1062// Called by slotFileDirty
1063void KCoreDirListerCache::handleDirDirty(const QUrl &url)
1064{
1065 // A dir: launch an update job if anyone cares about it
1066
1067 // This also means we can forget about pending updates to individual files in that dir
1068 const QString dir = url.toLocalFile();
1069 const QString dirPath = Utils::slashAppended(s: dir);
1070
1071 for (auto pendingIt = pendingUpdates.cbegin(); pendingIt != pendingUpdates.cend(); /* */) {
1072 const QString updPath = *pendingIt;
1073 qCDebug(KIO_CORE_DIRLISTER) << "had pending update" << updPath;
1074 if (updPath.startsWith(s: dirPath) && updPath.indexOf(c: QLatin1Char('/'), from: dirPath.length()) == -1) { // direct child item
1075 qCDebug(KIO_CORE_DIRLISTER) << "forgetting about individual update to" << updPath;
1076 pendingIt = pendingUpdates.erase(position: pendingIt);
1077 } else {
1078 ++pendingIt;
1079 }
1080 }
1081
1082 if (checkUpdate(dir: url)) {
1083 const auto [it, isInserted] = pendingDirectoryUpdates.insert(x: dir);
1084 if (isInserted && !pendingUpdateTimer.isActive()) {
1085 pendingUpdateTimer.start(msec: 200);
1086 }
1087 }
1088}
1089
1090// Called by slotFileDirty, for every alias of <url>
1091void KCoreDirListerCache::handleFileDirty(const QUrl &url)
1092{
1093 // A file: do we know about it already?
1094 const KFileItem &existingItem = findByUrl(lister: nullptr, u: url);
1095 const QUrl dir = url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1096 if (existingItem.isNull()) {
1097 // No - update the parent dir then
1098 handleDirDirty(url: dir);
1099 }
1100
1101 // Delay updating the file, FAM is flooding us with events
1102 if (checkUpdate(dir: dir)) {
1103 const QString filePath = url.toLocalFile();
1104 const auto [it, isInserted] = pendingUpdates.insert(x: filePath);
1105 if (isInserted && !pendingUpdateTimer.isActive()) {
1106 pendingUpdateTimer.start(msec: 200);
1107 }
1108 }
1109}
1110
1111void KCoreDirListerCache::slotFileCreated(const QString &path) // from KDirWatch
1112{
1113 qCDebug(KIO_CORE_DIRLISTER) << path;
1114 // XXX: how to avoid a complete rescan here?
1115 // We'd need to stat that one file separately and refresh the item(s) for it.
1116 QUrl fileUrl(QUrl::fromLocalFile(localfile: path));
1117 itemsAddedInDirectory(urlDir: fileUrl.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1118}
1119
1120void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch
1121{
1122 qCDebug(KIO_CORE_DIRLISTER) << path;
1123 const QString fileName = QFileInfo(path).fileName();
1124 QUrl dirUrl(QUrl::fromLocalFile(localfile: path));
1125 QStringList fileUrls;
1126 const QList<QUrl> urls = directoriesForCanonicalPath(dir: dirUrl.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1127 for (const QUrl &url : urls) {
1128 QUrl urlInfo(url);
1129 urlInfo.setPath(path: Utils::concatPaths(path1: urlInfo.path(), path2: fileName));
1130 fileUrls << urlInfo.toString();
1131 }
1132 slotFilesRemoved(fileList: fileUrls);
1133}
1134
1135void KCoreDirListerCache::slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries)
1136{
1137 QUrl url(joburl(job: static_cast<KIO::ListJob *>(job)));
1138 url = url.adjusted(options: QUrl::StripTrailingSlash);
1139
1140 qCDebug(KIO_CORE_DIRLISTER) << "new entries for " << url;
1141
1142 DirItem *dir = itemsInUse.value(key: url);
1143 if (!dir) {
1144 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but itemsInUse only knows about" << itemsInUse.keys();
1145 Q_ASSERT(dir);
1146 return;
1147 }
1148
1149 DirectoryDataHash::iterator dit = directoryData.find(key: url);
1150 if (dit == directoryData.end()) {
1151 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData doesn't know about that url, only about:" << directoryData.keys();
1152 Q_ASSERT(dit != directoryData.end());
1153 return;
1154 }
1155 KCoreDirListerCacheDirectoryData &dirData = *dit;
1156 const QList<KCoreDirLister *> listers = dirData.listersCurrentlyListing;
1157 if (listers.isEmpty()) {
1158 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData says no listers are currently listing " << url;
1159#ifndef NDEBUG
1160 printDebug();
1161#endif
1162 Q_ASSERT(!listers.isEmpty());
1163 return;
1164 }
1165
1166 // check if anyone wants the MIME types immediately
1167 bool delayedMimeTypes = true;
1168 for (const KCoreDirLister *kdl : listers) {
1169 delayedMimeTypes &= kdl->d->delayedMimeTypes;
1170 }
1171
1172 CacheHiddenFile *cachedHidden = nullptr;
1173 bool dotHiddenChecked = false;
1174 KFileItemList newItems;
1175 for (const auto &entry : entries) {
1176 const QString name = entry.stringValue(field: KIO::UDSEntry::UDS_NAME);
1177
1178 Q_ASSERT(!name.isEmpty());
1179 if (name.isEmpty()) {
1180 continue;
1181 }
1182
1183 if (name == QLatin1Char('.')) {
1184 // Try to reuse an existing KFileItem (if we listed the parent dir)
1185 // rather than creating a new one. There are many reasons:
1186 // 1) renames and permission changes to the item would have to emit the signals
1187 // twice, otherwise, so that both views manage to recognize the item.
1188 // 2) with kio_ftp we can only know that something is a symlink when
1189 // listing the parent, so prefer that item, which has more info.
1190 // Note that it gives a funky name() to the root item, rather than "." ;)
1191 dir->rootItem = itemForUrl(url);
1192 if (dir->rootItem.isNull()) {
1193 dir->rootItem = KFileItem(entry, url, delayedMimeTypes, true);
1194 }
1195
1196 for (KCoreDirLister *kdl : listers) {
1197 if (kdl->d->rootFileItem.isNull() && kdl->d->url == url) {
1198 kdl->d->rootFileItem = dir->rootItem;
1199 }
1200 }
1201 } else if (name != QLatin1String("..")) {
1202 KFileItem item(entry, url, delayedMimeTypes, true);
1203
1204 // get the names of the files listed in ".hidden", if it exists and is a local file
1205 if (!dotHiddenChecked) {
1206 const QString localPath = item.localPath();
1207 if (!localPath.isEmpty()) {
1208 const QString rootItemPath = QFileInfo(localPath).absolutePath();
1209 cachedHidden = cachedDotHiddenForDir(dir: rootItemPath);
1210 }
1211 dotHiddenChecked = true;
1212 }
1213
1214 // hide file if listed in ".hidden"
1215 if (cachedHidden && cachedHidden->listedFiles.find(x: name) != cachedHidden->listedFiles.cend()) {
1216 item.setHidden();
1217 }
1218
1219 qCDebug(KIO_CORE_DIRLISTER) << "Adding item: " << item.url();
1220 newItems.append(t: item);
1221 }
1222 }
1223
1224 // sort by url using KFileItem::operator<
1225 std::sort(first: newItems.begin(), last: newItems.end());
1226
1227 // Add the items sorted by url, needed by findByUrl
1228 dir->insertSortedItems(items: newItems);
1229
1230 for (KCoreDirLister *kdl : listers) {
1231 kdl->d->addNewItems(directoryUrl: url, items: newItems);
1232 }
1233
1234 for (KCoreDirLister *kdl : listers) {
1235 kdl->d->emitItems();
1236 }
1237}
1238
1239void KCoreDirListerCache::slotResult(KJob *j)
1240{
1241#ifdef DEBUG_CACHE
1242 // printDebug();
1243#endif
1244
1245 Q_ASSERT(j);
1246 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1247 runningListJobs.remove(key: job);
1248
1249 QUrl jobUrl(joburl(job));
1250 jobUrl = jobUrl.adjusted(options: QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
1251
1252 qCDebug(KIO_CORE_DIRLISTER) << "finished listing" << jobUrl;
1253
1254 const auto dit = directoryData.find(key: jobUrl);
1255 if (dit == directoryData.end()) {
1256 qCWarning(KIO_CORE) << "Nothing found in directoryData for URL" << jobUrl;
1257#ifndef NDEBUG
1258 printDebug();
1259#endif
1260 Q_ASSERT(dit != directoryData.end());
1261 return;
1262 }
1263 KCoreDirListerCacheDirectoryData &dirData = *dit;
1264 if (dirData.listersCurrentlyListing.isEmpty()) {
1265 qCWarning(KIO_CORE) << "OOOOPS, nothing in directoryData.listersCurrentlyListing for" << jobUrl;
1266 // We're about to assert; dump the current state...
1267#ifndef NDEBUG
1268 printDebug();
1269#endif
1270 Q_ASSERT(!dirData.listersCurrentlyListing.isEmpty());
1271 }
1272 const QList<KCoreDirLister *> listers = dirData.listersCurrentlyListing;
1273
1274 // move all listers to the holding list, do it before emitting
1275 // the signals to make sure it exists in KCoreDirListerCache in case someone
1276 // calls listDir during the signal emission
1277 Q_ASSERT(dirData.listersCurrentlyHolding.isEmpty());
1278 dirData.moveListersWithoutCachedItemsJob(url: jobUrl);
1279
1280 if (job->error()) {
1281 bool errorShown = false;
1282 for (KCoreDirLister *kdl : listers) {
1283 kdl->d->jobDone(job);
1284 if (job->error() != KJob::KilledJobError) {
1285 Q_EMIT kdl->jobError(job);
1286 if (kdl->d->m_autoErrorHandling && !errorShown) {
1287 errorShown = true; // do it only once
1288 if (job->uiDelegate()) {
1289 job->uiDelegate()->showErrorMessage();
1290 }
1291 }
1292 }
1293 const bool silent = job->property(name: "_kdlc_silent").toBool();
1294 if (!silent) {
1295 Q_EMIT kdl->listingDirCanceled(dirUrl: jobUrl);
1296 }
1297
1298 if (kdl->d->numJobs() == 0) {
1299 kdl->d->complete = true;
1300 if (!silent) {
1301 Q_EMIT kdl->canceled();
1302 }
1303 }
1304 }
1305 } else {
1306 DirItem *dir = itemsInUse.value(key: jobUrl);
1307 Q_ASSERT(dir);
1308 dir->complete = true;
1309
1310 for (KCoreDirLister *kdl : listers) {
1311 kdl->d->jobDone(job);
1312 Q_EMIT kdl->listingDirCompleted(dirUrl: jobUrl);
1313 if (kdl->d->numJobs() == 0) {
1314 kdl->d->complete = true;
1315 Q_EMIT kdl->completed();
1316 }
1317 }
1318 }
1319
1320 // TODO: hmm, if there was an error and job is a parent of one or more
1321 // of the pending urls we should cancel it/them as well
1322 processPendingUpdates();
1323
1324 if (job->property(name: "need_another_update").toBool()) {
1325 updateDirectory(dir: jobUrl);
1326 }
1327
1328#ifdef DEBUG_CACHE
1329 printDebug();
1330#endif
1331}
1332
1333void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url)
1334{
1335 Q_ASSERT(j);
1336 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1337
1338 QUrl oldUrl(job->url()); // here we really need the old url!
1339 QUrl newUrl(url);
1340
1341 // strip trailing slashes
1342 oldUrl = oldUrl.adjusted(options: QUrl::StripTrailingSlash);
1343 newUrl = newUrl.adjusted(options: QUrl::StripTrailingSlash);
1344
1345 if (oldUrl == newUrl) {
1346 qCDebug(KIO_CORE_DIRLISTER) << "New redirection url same as old, giving up.";
1347 return;
1348 } else if (newUrl.isEmpty()) {
1349 qCDebug(KIO_CORE_DIRLISTER) << "New redirection url is empty, giving up.";
1350 return;
1351 }
1352
1353 qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl;
1354
1355#ifdef DEBUG_CACHE
1356 // Can't do that here. KCoreDirListerCache::joburl() will use the new url already,
1357 // while our data structures haven't been updated yet -> assert fail.
1358 // printDebug();
1359#endif
1360
1361 // I don't think there can be dirItems that are children of oldUrl.
1362 // Am I wrong here? And even if so, we don't need to delete them, right?
1363 // DF: redirection happens before listDir emits any item. Makes little sense otherwise.
1364
1365 // oldUrl cannot be in itemsCached because only completed items are moved there
1366 DirItem *dir = itemsInUse.take(key: oldUrl);
1367 Q_ASSERT(dir);
1368
1369 DirectoryDataHash::iterator dit = directoryData.find(key: oldUrl);
1370 Q_ASSERT(dit != directoryData.end());
1371 KCoreDirListerCacheDirectoryData oldDirData = *dit;
1372 directoryData.erase(it: dit);
1373 Q_ASSERT(!oldDirData.listersCurrentlyListing.isEmpty());
1374 const QList<KCoreDirLister *> listers = oldDirData.listersCurrentlyListing;
1375 Q_ASSERT(!listers.isEmpty());
1376
1377 for (KCoreDirLister *kdl : listers) {
1378 kdl->d->redirect(oldUrl, newUrl, keepItems: false /*clear items*/);
1379 }
1380
1381 // when a lister was stopped before the job emits the redirection signal, the old url will
1382 // also be in listersCurrentlyHolding
1383 const QList<KCoreDirLister *> holders = oldDirData.listersCurrentlyHolding;
1384 for (KCoreDirLister *kdl : holders) {
1385 kdl->jobStarted(job);
1386 // do it like when starting a new list-job that will redirect later
1387 // TODO: maybe don't emit started if there's an update running for newUrl already?
1388 Q_EMIT kdl->started(dirUrl: oldUrl);
1389
1390 kdl->d->redirect(oldUrl, newUrl, keepItems: false /*clear items*/);
1391 }
1392
1393 const QList<KCoreDirLister *> allListers = listers + holders;
1394
1395 DirItem *newDir = itemsInUse.value(key: newUrl);
1396 if (newDir) {
1397 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "already in use";
1398
1399 // only in this case there can newUrl already be in listersCurrentlyListing or listersCurrentlyHolding
1400 delete dir;
1401
1402 // get the job if one's running for newUrl already (can be a list-job or an update-job), but
1403 // do not return this 'job', which would happen because of the use of redirectionURL()
1404 KIO::ListJob *oldJob = jobForUrl(url: newUrl, not_job: job);
1405
1406 // listers of newUrl with oldJob: forget about the oldJob and use the already running one
1407 // which will be converted to an updateJob
1408 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1409
1410 QList<KCoreDirLister *> &curListers = newDirData.listersCurrentlyListing;
1411 if (!curListers.isEmpty()) {
1412 qCDebug(KIO_CORE_DIRLISTER) << "and it is currently listed";
1413
1414 Q_ASSERT(oldJob); // ?!
1415
1416 for (KCoreDirLister *kdl : std::as_const(t&: curListers)) { // listers of newUrl
1417 kdl->d->jobDone(oldJob);
1418
1419 kdl->jobStarted(job);
1420 kdl->d->connectJob(job);
1421 }
1422
1423 // append listers of oldUrl with newJob to listers of newUrl with oldJob
1424 for (KCoreDirLister *kdl : listers) {
1425 curListers.append(t: kdl);
1426 }
1427 } else {
1428 curListers = listers;
1429 }
1430
1431 if (oldJob) { // kill the old job, be it a list-job or an update-job
1432 killJob(job: oldJob);
1433 }
1434
1435 // holders of newUrl: use the already running job which will be converted to an updateJob
1436 QList<KCoreDirLister *> &curHolders = newDirData.listersCurrentlyHolding;
1437 if (!curHolders.isEmpty()) {
1438 qCDebug(KIO_CORE_DIRLISTER) << "and it is currently held.";
1439
1440 for (KCoreDirLister *kdl : std::as_const(t&: curHolders)) { // holders of newUrl
1441 kdl->jobStarted(job);
1442 Q_EMIT kdl->started(dirUrl: newUrl);
1443 }
1444
1445 // append holders of oldUrl to holders of newUrl
1446 for (KCoreDirLister *kdl : holders) {
1447 curHolders.append(t: kdl);
1448 }
1449 } else {
1450 curHolders = holders;
1451 }
1452
1453 // emit old items: listers, holders. NOT: newUrlListers/newUrlHolders, they already have them listed
1454 // TODO: make this a separate method?
1455 for (KCoreDirLister *kdl : allListers) {
1456 if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) {
1457 kdl->d->rootFileItem = newDir->rootItem;
1458 }
1459
1460 kdl->d->addNewItems(directoryUrl: newUrl, items: newDir->lstItems);
1461 kdl->d->emitItems();
1462 }
1463 } else if ((newDir = itemsCached.take(key: newUrl))) {
1464 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "is unused, but already in the cache.";
1465
1466 delete dir;
1467 itemsInUse.insert(key: newUrl, value: newDir);
1468 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1469 newDirData.listersCurrentlyListing = listers;
1470 newDirData.listersCurrentlyHolding = holders;
1471
1472 // emit old items: listers, holders
1473 for (KCoreDirLister *kdl : allListers) {
1474 if (kdl->d->rootFileItem.isNull() && kdl->d->url == newUrl) {
1475 kdl->d->rootFileItem = newDir->rootItem;
1476 }
1477
1478 kdl->d->addNewItems(directoryUrl: newUrl, items: newDir->lstItems);
1479 kdl->d->emitItems();
1480 }
1481 } else {
1482 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "has not been listed yet.";
1483
1484 dir->rootItem = KFileItem();
1485 dir->lstItems.clear();
1486 dir->redirect(newUrl);
1487 itemsInUse.insert(key: newUrl, value: dir);
1488 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1489 newDirData.listersCurrentlyListing = listers;
1490 newDirData.listersCurrentlyHolding = holders;
1491
1492 if (holders.isEmpty()) {
1493#ifdef DEBUG_CACHE
1494 printDebug();
1495#endif
1496 return; // only in this case the job doesn't need to be converted,
1497 }
1498 }
1499
1500 // make the job an update job
1501 job->disconnect(receiver: this);
1502
1503 connect(sender: job, signal: &KIO::ListJob::entries, context: this, slot: &KCoreDirListerCache::slotUpdateEntries);
1504 connect(sender: job, signal: &KJob::result, context: this, slot: &KCoreDirListerCache::slotUpdateResult);
1505
1506 // FIXME: autoUpdate-Counts!!
1507
1508#ifdef DEBUG_CACHE
1509 printDebug();
1510#endif
1511}
1512
1513struct KCoreDirListerCache::ItemInUseChange {
1514 ItemInUseChange(const QUrl &old, const QUrl &newU, DirItem *di)
1515 : oldUrl(old)
1516 , newUrl(newU)
1517 , dirItem(di)
1518 {
1519 }
1520 QUrl oldUrl;
1521 QUrl newUrl;
1522 DirItem *dirItem;
1523};
1524
1525void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl)
1526{
1527 qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl;
1528
1529 std::vector<ItemInUseChange> itemsToChange;
1530 std::set<KCoreDirLister *> listers;
1531
1532 // Look at all dirs being listed/shown
1533 for (auto itu = itemsInUse.begin(), ituend = itemsInUse.end(); itu != ituend; ++itu) {
1534 DirItem *dir = itu.value();
1535 const QUrl &oldDirUrl = itu.key();
1536 qCDebug(KIO_CORE_DIRLISTER) << "itemInUse:" << oldDirUrl;
1537 // Check if this dir is oldUrl, or a subfolder of it
1538 if (oldDirUrl == oldUrl || oldUrl.isParentOf(url: oldDirUrl)) {
1539 // TODO should use KUrl::cleanpath like isParentOf does
1540 QString relPath = oldDirUrl.path().mid(position: oldUrl.path().length() + 1);
1541
1542 QUrl newDirUrl(newUrl); // take new base
1543 if (!relPath.isEmpty()) {
1544 newDirUrl.setPath(path: Utils::concatPaths(path1: newDirUrl.path(), path2: relPath)); // add unchanged relative path
1545 }
1546 qCDebug(KIO_CORE_DIRLISTER) << "new url=" << newDirUrl;
1547
1548 // Update URL in dir item and in itemsInUse
1549 dir->redirect(newUrl: newDirUrl);
1550
1551 itemsToChange.emplace_back(args: oldDirUrl.adjusted(options: QUrl::StripTrailingSlash), args: newDirUrl.adjusted(options: QUrl::StripTrailingSlash), args&: dir);
1552 // Rename all items under that dir
1553 // If all items of the directory change the same part of their url, the order is not
1554 // changed, therefore just change it in the list.
1555 for (KFileItem &item : dir->lstItems) {
1556 const KFileItem oldItem = item;
1557 KFileItem newItem = oldItem;
1558 const QUrl &oldItemUrl = oldItem.url();
1559 QUrl newItemUrl(oldItemUrl);
1560 newItemUrl.setPath(path: Utils::concatPaths(path1: newDirUrl.path(), path2: oldItemUrl.fileName()));
1561 qCDebug(KIO_CORE_DIRLISTER) << "renaming" << oldItemUrl << "to" << newItemUrl;
1562 newItem.setUrl(newItemUrl);
1563
1564 listers.merge(source: emitRefreshItem(oldItem, fileitem: newItem));
1565 // Change the item
1566 item.setUrl(newItemUrl);
1567 }
1568 }
1569 }
1570
1571 for (KCoreDirLister *kdl : listers) {
1572 kdl->d->emitItems();
1573 }
1574
1575 // Do the changes to itemsInUse out of the loop to avoid messing up iterators,
1576 // and so that emitRefreshItem can find the stuff in the hash.
1577 for (const ItemInUseChange &i : itemsToChange) {
1578 itemsInUse.remove(key: i.oldUrl);
1579 itemsInUse.insert(key: i.newUrl, value: i.dirItem);
1580 }
1581 // Now that all the caches are updated and consistent, emit the redirection.
1582 for (const ItemInUseChange &i : itemsToChange) {
1583 emitRedirections(oldUrl: QUrl(i.oldUrl), url: QUrl(i.newUrl));
1584 }
1585 // Is oldUrl a directory in the cache?
1586 // Remove any child of oldUrl from the cache - even if the renamed dir itself isn't in it!
1587 removeDirFromCache(dir: oldUrl);
1588 // TODO rename, instead.
1589}
1590
1591// helper for renameDir, not used for redirections from KIO::listDir().
1592void KCoreDirListerCache::emitRedirections(const QUrl &_oldUrl, const QUrl &_newUrl)
1593{
1594 qCDebug(KIO_CORE_DIRLISTER) << _oldUrl << "->" << _newUrl;
1595 const QUrl oldUrl = _oldUrl.adjusted(options: QUrl::StripTrailingSlash);
1596 const QUrl newUrl = _newUrl.adjusted(options: QUrl::StripTrailingSlash);
1597
1598 KIO::ListJob *job = jobForUrl(url: oldUrl);
1599 if (job) {
1600 killJob(job);
1601 }
1602
1603 // Check if we were listing this dir. Need to abort and restart with new name in that case.
1604 DirectoryDataHash::iterator dit = directoryData.find(key: oldUrl);
1605 if (dit == directoryData.end()) {
1606 return;
1607 }
1608 const QList<KCoreDirLister *> listers = (*dit).listersCurrentlyListing;
1609 const QList<KCoreDirLister *> holders = (*dit).listersCurrentlyHolding;
1610
1611 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1612
1613 // Tell the world that the job listing the old url is dead.
1614 for (KCoreDirLister *kdl : listers) {
1615 if (job) {
1616 kdl->d->jobDone(job);
1617 }
1618 Q_EMIT kdl->listingDirCanceled(dirUrl: oldUrl);
1619 }
1620 newDirData.listersCurrentlyListing += listers;
1621
1622 // Check if we are currently displaying this directory (odds opposite wrt above)
1623 for (KCoreDirLister *kdl : holders) {
1624 if (job) {
1625 kdl->d->jobDone(job);
1626 }
1627 }
1628 newDirData.listersCurrentlyHolding += holders;
1629 directoryData.erase(it: dit);
1630
1631 if (!listers.isEmpty()) {
1632 updateDirectory(dir: newUrl);
1633
1634 // Tell the world about the new url
1635 for (KCoreDirLister *kdl : listers) {
1636 Q_EMIT kdl->started(dirUrl: newUrl);
1637 }
1638 }
1639
1640 // And notify the dirlisters of the redirection
1641 for (KCoreDirLister *kdl : holders) {
1642 kdl->d->redirect(oldUrl, newUrl, keepItems: true /*keep items*/);
1643 }
1644}
1645
1646void KCoreDirListerCache::removeDirFromCache(const QUrl &dir)
1647{
1648 qCDebug(KIO_CORE_DIRLISTER) << dir;
1649 const QList<QUrl> cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator...
1650 for (const QUrl &cachedDir : cachedDirs) {
1651 if (dir == cachedDir || dir.isParentOf(url: cachedDir)) {
1652 itemsCached.remove(key: cachedDir);
1653 }
1654 }
1655}
1656
1657void KCoreDirListerCache::slotUpdateEntries(KIO::Job *job, const KIO::UDSEntryList &list)
1658{
1659 runningListJobs[static_cast<KIO::ListJob *>(job)] += list;
1660}
1661
1662void KCoreDirListerCache::slotUpdateResult(KJob *j)
1663{
1664 Q_ASSERT(j);
1665 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1666
1667 QUrl jobUrl(joburl(job));
1668 jobUrl = jobUrl.adjusted(options: QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
1669
1670 qCDebug(KIO_CORE_DIRLISTER) << "finished update" << jobUrl;
1671
1672 KCoreDirListerCacheDirectoryData &dirData = directoryData[jobUrl];
1673 // Collect the dirlisters which were listing the URL using that ListJob
1674 // plus those that were already holding that URL - they all get updated.
1675 dirData.moveListersWithoutCachedItemsJob(url: jobUrl);
1676 const QList<KCoreDirLister *> listers = dirData.listersCurrentlyHolding + dirData.listersCurrentlyListing;
1677
1678 // once we are updating dirs that are only in the cache this will fail!
1679 Q_ASSERT(!listers.isEmpty());
1680
1681 if (job->error()) {
1682 for (KCoreDirLister *kdl : listers) {
1683 kdl->d->jobDone(job);
1684
1685 // don't bother the user: no jobError signal emitted
1686
1687 const bool silent = job->property(name: "_kdlc_silent").toBool();
1688 if (!silent) {
1689 Q_EMIT kdl->listingDirCanceled(dirUrl: jobUrl);
1690 }
1691 if (kdl->d->numJobs() == 0) {
1692 kdl->d->complete = true;
1693 if (!silent) {
1694 Q_EMIT kdl->canceled();
1695 }
1696 }
1697 }
1698
1699 runningListJobs.remove(key: job);
1700
1701 // TODO: if job is a parent of one or more
1702 // of the pending urls we should cancel them
1703 processPendingUpdates();
1704 return;
1705 }
1706
1707 DirItem *dir = itemsInUse.value(key: jobUrl, defaultValue: nullptr);
1708 if (!dir) {
1709 qCWarning(KIO_CORE) << "Internal error: itemsInUse did not contain" << jobUrl;
1710#ifndef NDEBUG
1711 printDebug();
1712#endif
1713 Q_ASSERT(dir);
1714 } else {
1715 dir->complete = true;
1716 }
1717
1718 // check if anyone wants the MIME types immediately
1719 bool delayedMimeTypes = true;
1720 for (const KCoreDirLister *kdl : listers) {
1721 delayedMimeTypes &= kdl->d->delayedMimeTypes;
1722 }
1723
1724 typedef QHash<QString, KFileItem> FileItemHash; // fileName -> KFileItem
1725 FileItemHash fileItems;
1726
1727 // Fill the hash from the old list of items. We'll remove entries as we see them
1728 // in the new listing, and the resulting hash entries will be the deleted items.
1729 for (const KFileItem &item : std::as_const(t&: dir->lstItems)) {
1730 fileItems.insert(key: item.name(), value: item);
1731 }
1732
1733 CacheHiddenFile *cachedHidden = nullptr;
1734 bool dotHiddenChecked = false;
1735 const KIO::UDSEntryList &buf = runningListJobs.value(key: job);
1736 KFileItemList newItems;
1737 for (const auto &entry : buf) {
1738 // Form the complete url
1739 KFileItem item(entry, jobUrl, delayedMimeTypes, true);
1740
1741 const QString name = item.name();
1742 Q_ASSERT(!name.isEmpty()); // A KIO worker setting an empty UDS_NAME is utterly broken, fix the KIO worker!
1743
1744 // we duplicate the check for dotdot here, to avoid iterating over
1745 // all items again and checking in matchesFilter() that way.
1746 if (name.isEmpty() || name == QLatin1String("..")) {
1747 continue;
1748 }
1749
1750 if (name == QLatin1Char('.')) {
1751 // if the update was started before finishing the original listing
1752 // there is no root item yet
1753 if (dir->rootItem.isNull()) {
1754 dir->rootItem = item;
1755
1756 for (KCoreDirLister *kdl : listers) {
1757 if (kdl->d->rootFileItem.isNull() && kdl->d->url == jobUrl) {
1758 kdl->d->rootFileItem = dir->rootItem;
1759 }
1760 }
1761 }
1762 continue;
1763 } else {
1764 // get the names of the files listed in ".hidden", if it exists and is a local file
1765 if (!dotHiddenChecked) {
1766 const QString localPath = item.localPath();
1767 if (!localPath.isEmpty()) {
1768 const QString rootItemPath = QFileInfo(localPath).absolutePath();
1769 cachedHidden = cachedDotHiddenForDir(dir: rootItemPath);
1770 }
1771 dotHiddenChecked = true;
1772 }
1773 }
1774
1775 // hide file if listed in ".hidden"
1776 if (cachedHidden && cachedHidden->listedFiles.find(x: name) != cachedHidden->listedFiles.cend()) {
1777 item.setHidden();
1778 }
1779
1780 // Find this item
1781 FileItemHash::iterator fiit = fileItems.find(key: item.name());
1782 if (fiit != fileItems.end()) {
1783 const KFileItem tmp = fiit.value();
1784
1785 bool inPendingUpdates = false;
1786 bool inPendingRemoteUpdates = false;
1787
1788 std::set<QString>::iterator pu_it;
1789 std::set<KFileItem>::iterator pru_it;
1790 if (tmp.url().isLocalFile()) {
1791 pu_it = pendingUpdates.find(x: tmp.url().toLocalFile());
1792 inPendingUpdates = pu_it != pendingUpdates.end();
1793 } else {
1794 pru_it = pendingRemoteUpdates.find(x: tmp);
1795 inPendingRemoteUpdates = pru_it != pendingRemoteUpdates.end();
1796 }
1797
1798 // check if something changed for this file, using KFileItem::cmp()
1799 if (inPendingRemoteUpdates || inPendingUpdates || !tmp.cmp(item)) {
1800 if (inPendingRemoteUpdates) {
1801 pendingRemoteUpdates.erase(position: pru_it);
1802 }
1803 if (inPendingUpdates) {
1804 pendingUpdates.erase(position: pu_it);
1805 }
1806
1807 qCDebug(KIO_CORE_DIRLISTER) << "file changed:" << tmp.name();
1808
1809 reinsert(item, oldUrl: tmp.url());
1810 for (KCoreDirLister *kdl : listers) {
1811 kdl->d->addRefreshItem(directoryUrl: jobUrl, oldItem: tmp, item);
1812 }
1813 }
1814 // Seen, remove
1815 fileItems.erase(it: fiit);
1816 } else { // this is a new file
1817 qCDebug(KIO_CORE_DIRLISTER) << "new file:" << name;
1818 newItems.append(t: item);
1819 }
1820 }
1821
1822 // sort by url using KFileItem::operator<
1823 std::sort(first: newItems.begin(), last: newItems.end());
1824
1825 // Add the items sorted by url, needed by findByUrl
1826 dir->insertSortedItems(items: newItems);
1827
1828 for (KCoreDirLister *kdl : listers) {
1829 kdl->d->addNewItems(directoryUrl: jobUrl, items: newItems);
1830 }
1831
1832 runningListJobs.remove(key: job);
1833
1834 if (!fileItems.isEmpty()) {
1835 deleteUnmarkedItems(listers, lstItems&: dir->lstItems, itemsToDelete: fileItems);
1836 }
1837
1838 for (KCoreDirLister *kdl : listers) {
1839 kdl->d->emitItems();
1840
1841 kdl->d->jobDone(job);
1842 Q_EMIT kdl->listingDirCompleted(dirUrl: jobUrl);
1843 if (kdl->d->numJobs() == 0) {
1844 kdl->d->complete = true;
1845 Q_EMIT kdl->completed();
1846 }
1847 }
1848
1849 // TODO: hmm, if there was an error and job is a parent of one or more
1850 // of the pending urls we should cancel it/them as well
1851 processPendingUpdates();
1852
1853 if (job->property(name: "need_another_update").toBool()) {
1854 updateDirectory(dir: jobUrl);
1855 }
1856}
1857
1858// private
1859
1860KIO::ListJob *KCoreDirListerCache::jobForUrl(const QUrl &url, KIO::ListJob *not_job)
1861{
1862 for (auto it = runningListJobs.cbegin(); it != runningListJobs.cend(); ++it) {
1863 KIO::ListJob *job = it.key();
1864 const QUrl jobUrl = joburl(job).adjusted(options: QUrl::StripTrailingSlash);
1865
1866 if (jobUrl == url && job != not_job) {
1867 return job;
1868 }
1869 }
1870 return nullptr;
1871}
1872
1873const QUrl &KCoreDirListerCache::joburl(KIO::ListJob *job)
1874{
1875 if (job->redirectionUrl().isValid()) {
1876 return job->redirectionUrl();
1877 } else {
1878 return job->url();
1879 }
1880}
1881
1882void KCoreDirListerCache::killJob(KIO::ListJob *job)
1883{
1884 runningListJobs.remove(key: job);
1885 job->disconnect(receiver: this);
1886 job->kill();
1887}
1888
1889void KCoreDirListerCache::deleteUnmarkedItems(const QList<KCoreDirLister *> &listers,
1890 QList<KFileItem> &lstItems,
1891 const QHash<QString, KFileItem> &itemsToDelete)
1892{
1893 // Make list of deleted items (for emitting)
1894 KFileItemList deletedItems;
1895 deletedItems.reserve(asize: itemsToDelete.size());
1896 for (auto kit = itemsToDelete.cbegin(), endIt = itemsToDelete.cend(); kit != endIt; ++kit) {
1897 const KFileItem item = kit.value();
1898 deletedItems.append(t: item);
1899 qCDebug(KIO_CORE_DIRLISTER) << "deleted:" << item.name() << item;
1900 }
1901
1902 // Delete all remaining items
1903 auto it = std::remove_if(first: lstItems.begin(), last: lstItems.end(), pred: [&itemsToDelete](const KFileItem &item) {
1904 return itemsToDelete.contains(key: item.name());
1905 });
1906 lstItems.erase(abegin: it, aend: lstItems.end());
1907
1908 itemsDeleted(listers, deletedItems);
1909}
1910
1911void KCoreDirListerCache::itemsDeleted(const QList<KCoreDirLister *> &listers, const KFileItemList &deletedItems)
1912{
1913 for (KCoreDirLister *kdl : listers) {
1914 kdl->d->emitItemsDeleted(items: deletedItems);
1915 }
1916
1917 for (const KFileItem &item : deletedItems) {
1918 if (item.isDir()) {
1919 deleteDir(dirUrl: item.url());
1920 }
1921 }
1922}
1923
1924void KCoreDirListerCache::deleteDir(const QUrl &_dirUrl)
1925{
1926 qCDebug(KIO_CORE_DIRLISTER) << _dirUrl;
1927 // unregister and remove the children of the deleted item.
1928 // Idea: tell all the KCoreDirListers that they should forget the dir
1929 // and then remove it from the cache.
1930
1931 QUrl dirUrl(_dirUrl.adjusted(options: QUrl::StripTrailingSlash));
1932
1933 // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse)
1934 QList<QUrl> affectedItems;
1935
1936 auto itu = itemsInUse.cbegin();
1937 const auto ituend = itemsInUse.cend();
1938 for (; itu != ituend; ++itu) {
1939 const QUrl &deletedUrl = itu.key();
1940 if (dirUrl == deletedUrl || dirUrl.isParentOf(url: deletedUrl)) {
1941 affectedItems.append(t: deletedUrl);
1942 }
1943 }
1944
1945 for (const QUrl &deletedUrl : std::as_const(t&: affectedItems)) {
1946 // stop all jobs for deletedUrlStr
1947 auto dit = directoryData.constFind(key: deletedUrl);
1948 if (dit != directoryData.cend()) {
1949 // we need a copy because stop modifies the list
1950 const QList<KCoreDirLister *> listers = (*dit).listersCurrentlyListing;
1951 for (KCoreDirLister *kdl : listers) {
1952 stopListingUrl(lister: kdl, u: deletedUrl);
1953 }
1954 // tell listers holding deletedUrl to forget about it
1955 // this will stop running updates for deletedUrl as well
1956
1957 // we need a copy because forgetDirs modifies the list
1958 const QList<KCoreDirLister *> holders = (*dit).listersCurrentlyHolding;
1959 for (KCoreDirLister *kdl : holders) {
1960 // lister's root is the deleted item
1961 if (kdl->d->url == deletedUrl) {
1962 // tell the view first. It might need the subdirs' items (which forgetDirs will delete)
1963 if (!kdl->d->rootFileItem.isNull()) {
1964 Q_EMIT kdl->itemsDeleted(items: KFileItemList{kdl->d->rootFileItem});
1965 }
1966 forgetDirs(lister: kdl);
1967 kdl->d->rootFileItem = KFileItem();
1968 } else {
1969 const bool treeview = kdl->d->lstDirs.count() > 1;
1970 if (!treeview) {
1971 Q_EMIT kdl->clear();
1972 kdl->d->lstDirs.clear();
1973 } else {
1974 kdl->d->lstDirs.removeAll(t: deletedUrl);
1975 }
1976
1977 forgetDirs(lister: kdl, url: deletedUrl, notify: treeview);
1978 }
1979 }
1980 }
1981
1982 // delete the entry for deletedUrl - should not be needed, it's in
1983 // items cached now
1984 int count = itemsInUse.remove(key: deletedUrl);
1985 Q_ASSERT(count == 0);
1986 Q_UNUSED(count); // keep gcc "unused variable" complaining quiet when in release mode
1987 }
1988
1989 // remove the children from the cache
1990 removeDirFromCache(dir: dirUrl);
1991}
1992
1993// delayed updating of files, FAM is flooding us with events
1994void KCoreDirListerCache::processPendingUpdates()
1995{
1996 std::set<KCoreDirLister *> listers;
1997 QList<QUrl> removedUrls;
1998 for (const QString &file : pendingUpdates) { // always a local path
1999 qCDebug(KIO_CORE_DIRLISTER) << file;
2000 QUrl u = QUrl::fromLocalFile(localfile: file);
2001 KFileItem item = findByUrl(lister: nullptr, u: u); // search all items
2002 if (!item.isNull()) {
2003 // we need to refresh the item, because e.g. the permissions can have changed.
2004 KFileItem oldItem = item;
2005 item.refresh();
2006
2007 if (!oldItem.cmp(item)) {
2008 if (!item.exists()) {
2009 removedUrls.append(t: oldItem.url());
2010 } else {
2011 reinsert(item, oldUrl: oldItem.url());
2012 }
2013 listers.merge(source: emitRefreshItem(oldItem, fileitem: item));
2014 }
2015 }
2016 }
2017 pendingUpdates.clear();
2018 for (KCoreDirLister *kdl : listers) {
2019 kdl->d->emitItems();
2020 }
2021
2022 // clean orphan KFileItem, after events were emitted
2023 for (const auto &removedUrl : removedUrls) {
2024 remove(oldUrl: removedUrl);
2025 }
2026
2027 // Directories in need of updating
2028 for (const QString &dir : pendingDirectoryUpdates) {
2029 updateDirectory(dir: QUrl::fromLocalFile(localfile: dir));
2030 }
2031 pendingDirectoryUpdates.clear();
2032}
2033
2034#ifndef NDEBUG
2035void KCoreDirListerCache::printDebug()
2036{
2037 qCDebug(KIO_CORE_DIRLISTER) << "Items in use:";
2038 auto itu = itemsInUse.constBegin();
2039 const auto ituend = itemsInUse.constEnd();
2040 for (; itu != ituend; ++itu) {
2041 qCDebug(KIO_CORE_DIRLISTER) << " " << itu.key() << "URL:" << itu.value()->url
2042 << "rootItem:" << (!itu.value()->rootItem.isNull() ? itu.value()->rootItem.url() : QUrl())
2043 << "autoUpdates refcount:" << itu.value()->autoUpdates << "complete:" << itu.value()->complete
2044 << QStringLiteral("with %1 items.").arg(a: itu.value()->lstItems.count());
2045 }
2046
2047 QList<KCoreDirLister *> listersWithoutJob;
2048 qCDebug(KIO_CORE_DIRLISTER) << "Directory data:";
2049 auto dit = directoryData.constBegin();
2050 for (; dit != directoryData.constEnd(); ++dit) {
2051 QString list;
2052 const QList<KCoreDirLister *> listers = (*dit).listersCurrentlyListing;
2053 for (KCoreDirLister *listit : listers) {
2054 list += QLatin1String(" 0x") + QString::number(reinterpret_cast<qlonglong>(listit), base: 16);
2055 }
2056 qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << listers.count() << "listers:" << list;
2057 for (KCoreDirLister *listit : listers) {
2058 if (!listit->d->m_cachedItemsJobs.isEmpty()) {
2059 qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has CachedItemsJobs" << listit->d->m_cachedItemsJobs;
2060 } else if (KIO::ListJob *listJob = jobForUrl(url: dit.key())) {
2061 qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has ListJob" << listJob;
2062 } else {
2063 listersWithoutJob.append(t: listit);
2064 }
2065 }
2066
2067 list.clear();
2068 const QList<KCoreDirLister *> holders = (*dit).listersCurrentlyHolding;
2069 for (KCoreDirLister *listit : holders) {
2070 list += QLatin1String(" 0x") + QString::number(reinterpret_cast<qlonglong>(listit), base: 16);
2071 }
2072 qCDebug(KIO_CORE_DIRLISTER) << " " << dit.key() << holders.count() << "holders:" << list;
2073 }
2074
2075 QMap<KIO::ListJob *, KIO::UDSEntryList>::Iterator jit = runningListJobs.begin();
2076 qCDebug(KIO_CORE_DIRLISTER) << "Jobs:";
2077 for (; jit != runningListJobs.end(); ++jit) {
2078 qCDebug(KIO_CORE_DIRLISTER) << " " << jit.key() << "listing" << joburl(job: jit.key()) << ":" << (*jit).count() << "entries.";
2079 }
2080
2081 qCDebug(KIO_CORE_DIRLISTER) << "Items in cache:";
2082 const QList<QUrl> cachedDirs = itemsCached.keys();
2083 for (const QUrl &cachedDir : cachedDirs) {
2084 DirItem *dirItem = itemsCached.object(key: cachedDir);
2085 qCDebug(KIO_CORE_DIRLISTER) << " " << cachedDir
2086 << "rootItem:" << (!dirItem->rootItem.isNull() ? dirItem->rootItem.url().toString() : QStringLiteral("NULL")) << "with"
2087 << dirItem->lstItems.count() << "items.";
2088 }
2089
2090 // Abort on listers without jobs -after- showing the full dump. Easier debugging.
2091 for (KCoreDirLister *listit : std::as_const(t&: listersWithoutJob)) {
2092 qCWarning(KIO_CORE) << "Fatal Error: HUH? Lister" << listit << "is supposed to be listing, but has no job!";
2093 abort();
2094 }
2095}
2096#endif
2097
2098KCoreDirLister::KCoreDirLister(QObject *parent)
2099 : QObject(parent)
2100 , d(new KCoreDirListerPrivate(this))
2101{
2102 qCDebug(KIO_CORE_DIRLISTER) << "+KCoreDirLister";
2103
2104 d->complete = true;
2105
2106 setAutoUpdate(true);
2107 setDirOnlyMode(false);
2108 setShowHiddenFiles(false);
2109}
2110
2111KCoreDirLister::~KCoreDirLister()
2112{
2113 qCDebug(KIO_CORE_DIRLISTER) << "~KCoreDirLister" << this;
2114
2115 // Stop all running jobs, remove lister from lists
2116 if (!qApp->closingDown()) {
2117 stop();
2118 s_kDirListerCache.localData().forgetDirs(lister: this);
2119 }
2120}
2121
2122// TODO KF6: remove bool ret val, it's always true
2123bool KCoreDirLister::openUrl(const QUrl &_url, OpenUrlFlags _flags)
2124{
2125 // emit the current changes made to avoid an inconsistent treeview
2126 if (d->hasPendingChanges && (_flags & Keep)) {
2127 emitChanges();
2128 }
2129
2130 d->hasPendingChanges = false;
2131
2132 return s_kDirListerCache.localData().listDir(lister: this, dirUrl: _url, keep: _flags & Keep, reload: _flags & Reload);
2133}
2134
2135void KCoreDirLister::stop()
2136{
2137 s_kDirListerCache.localData().stop(lister: this);
2138}
2139
2140void KCoreDirLister::stop(const QUrl &_url)
2141{
2142 s_kDirListerCache.localData().stopListingUrl(lister: this, u: _url);
2143}
2144
2145void KCoreDirLister::forgetDirs(const QUrl &_url)
2146{
2147 s_kDirListerCache.localData().forgetDirs(lister: this, _url, notify: true);
2148}
2149
2150bool KCoreDirLister::autoUpdate() const
2151{
2152 return d->autoUpdate;
2153}
2154
2155void KCoreDirLister::setAutoUpdate(bool enable)
2156{
2157 if (d->autoUpdate == enable) {
2158 return;
2159 }
2160
2161 d->autoUpdate = enable;
2162 s_kDirListerCache.localData().setAutoUpdate(lister: this, enable);
2163}
2164
2165bool KCoreDirLister::showHiddenFiles() const
2166{
2167 return d->settings.isShowingDotFiles;
2168}
2169
2170void KCoreDirLister::setShowHiddenFiles(bool setShowHiddenFiles)
2171{
2172 if (d->settings.isShowingDotFiles == setShowHiddenFiles) {
2173 return;
2174 }
2175
2176 d->prepareForSettingsChange();
2177 d->settings.isShowingDotFiles = setShowHiddenFiles;
2178}
2179
2180bool KCoreDirLister::dirOnlyMode() const
2181{
2182 return d->settings.dirOnlyMode;
2183}
2184
2185void KCoreDirLister::setDirOnlyMode(bool dirsOnly)
2186{
2187 if (d->settings.dirOnlyMode == dirsOnly) {
2188 return;
2189 }
2190
2191 d->prepareForSettingsChange();
2192 d->settings.dirOnlyMode = dirsOnly;
2193}
2194
2195bool KCoreDirLister::requestMimeTypeWhileListing() const
2196{
2197 return d->requestMimeTypeWhileListing;
2198}
2199
2200void KCoreDirLister::setRequestMimeTypeWhileListing(bool request)
2201{
2202 if (d->requestMimeTypeWhileListing == request) {
2203 return;
2204 }
2205
2206 d->requestMimeTypeWhileListing = request;
2207 if (d->requestMimeTypeWhileListing) {
2208 // Changing from request off to on, clear any cached items associated
2209 // with this lister so we re-request them and get the mimetype as well.
2210 // If we do not, we risk caching items that have no mime type.
2211 s_kDirListerCache.localData().forgetDirs(lister: this);
2212 }
2213}
2214
2215QUrl KCoreDirLister::url() const
2216{
2217 return d->url;
2218}
2219
2220QList<QUrl> KCoreDirLister::directories() const
2221{
2222 return d->lstDirs;
2223}
2224
2225void KCoreDirLister::emitChanges()
2226{
2227 d->emitChanges();
2228}
2229
2230void KCoreDirListerPrivate::emitChanges()
2231{
2232 if (!hasPendingChanges) {
2233 return;
2234 }
2235
2236 // reset 'hasPendingChanges' now, in case of recursion
2237 // (testcase: enabling recursive scan in ktorrent, #174920)
2238 hasPendingChanges = false;
2239
2240 const KCoreDirListerPrivate::FilterSettings newSettings = settings;
2241 settings = oldSettings; // temporarily
2242
2243 // Fill hash with all items that are currently visible
2244 std::set<QString> oldVisibleItems;
2245 for (const QUrl &dir : std::as_const(t&: lstDirs)) {
2246 const QList<KFileItem> *itemList = s_kDirListerCache.localData().itemsForDir(dir);
2247 if (!itemList) {
2248 continue;
2249 }
2250
2251 for (const KFileItem &item : *itemList) {
2252 if (isItemVisible(item) && matchesMimeFilter(item)) {
2253 oldVisibleItems.insert(x: item.name());
2254 }
2255 }
2256 }
2257
2258 settings = newSettings;
2259
2260 const QList<QUrl> dirs = lstDirs;
2261 for (const QUrl &dir : dirs) {
2262 KFileItemList deletedItems;
2263
2264 const QList<KFileItem> *itemList = s_kDirListerCache.localData().itemsForDir(dir);
2265 if (!itemList) {
2266 continue;
2267 }
2268
2269 for (const auto &item : *itemList) {
2270 const QString text = item.text();
2271 if (text == QLatin1Char('.') || text == QLatin1String("..")) {
2272 continue;
2273 }
2274 const bool wasVisible = oldVisibleItems.find(x: item.name()) != oldVisibleItems.cend();
2275 const bool mimeFiltered = matchesMimeFilter(item);
2276 const bool nowVisible = isItemVisible(item) && mimeFiltered;
2277 if (nowVisible && !wasVisible) {
2278 addNewItem(directoryUrl: dir, item); // takes care of emitting newItem or itemsFilteredByMime
2279 } else if (!nowVisible && wasVisible) {
2280 if (!mimeFiltered) {
2281 lstMimeFilteredItems.append(t: item);
2282 }
2283 deletedItems.append(t: item);
2284 }
2285 }
2286 if (!deletedItems.isEmpty()) {
2287 Q_EMIT q->itemsDeleted(items: deletedItems);
2288 }
2289 emitItems();
2290 }
2291 oldSettings = settings;
2292}
2293
2294void KCoreDirLister::updateDirectory(const QUrl &dirUrl)
2295{
2296 s_kDirListerCache.localData().updateDirectory(dir: dirUrl);
2297}
2298
2299bool KCoreDirLister::isFinished() const
2300{
2301 return d->complete;
2302}
2303
2304KFileItem KCoreDirLister::rootItem() const
2305{
2306 return d->rootFileItem;
2307}
2308
2309KFileItem KCoreDirLister::findByUrl(const QUrl &url) const
2310{
2311 return s_kDirListerCache.localData().findByUrl(lister: this, u: url);
2312}
2313
2314KFileItem KCoreDirLister::findByName(const QString &name) const
2315{
2316 return s_kDirListerCache.localData().findByName(lister: this, name: name);
2317}
2318
2319// ================ public filter methods ================ //
2320
2321void KCoreDirLister::setNameFilter(const QString &nameFilter)
2322{
2323 if (d->nameFilter == nameFilter) {
2324 return;
2325 }
2326
2327 d->prepareForSettingsChange();
2328
2329 d->settings.lstFilters.clear();
2330 d->nameFilter = nameFilter;
2331 // Split on white space
2332 const QList<QStringView> list = QStringView(nameFilter).split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts);
2333 for (const QStringView filter : list) {
2334 d->settings.lstFilters.append(t: QRegularExpression(QRegularExpression::wildcardToRegularExpression(str: filter), QRegularExpression::CaseInsensitiveOption));
2335 }
2336}
2337
2338QString KCoreDirLister::nameFilter() const
2339{
2340 return d->nameFilter;
2341}
2342
2343void KCoreDirLister::setMimeFilter(const QStringList &mimeFilter)
2344{
2345 if (d->settings.mimeFilter == mimeFilter) {
2346 return;
2347 }
2348
2349 d->prepareForSettingsChange();
2350 if (mimeFilter.contains(str: QLatin1String("application/octet-stream")) || mimeFilter.contains(str: QLatin1String("all/allfiles"))) { // all files
2351 d->settings.mimeFilter.clear();
2352 } else {
2353 d->settings.mimeFilter = mimeFilter;
2354 }
2355}
2356
2357void KCoreDirLister::setMimeExcludeFilter(const QStringList &mimeExcludeFilter)
2358{
2359 if (d->settings.mimeExcludeFilter == mimeExcludeFilter) {
2360 return;
2361 }
2362
2363 d->prepareForSettingsChange();
2364 d->settings.mimeExcludeFilter = mimeExcludeFilter;
2365}
2366
2367void KCoreDirLister::clearMimeFilter()
2368{
2369 d->prepareForSettingsChange();
2370 d->settings.mimeFilter.clear();
2371 d->settings.mimeExcludeFilter.clear();
2372}
2373
2374QStringList KCoreDirLister::mimeFilters() const
2375{
2376 return d->settings.mimeFilter;
2377}
2378
2379// ================ protected methods ================ //
2380
2381bool KCoreDirListerPrivate::matchesFilter(const KFileItem &item) const
2382{
2383 Q_ASSERT(!item.isNull());
2384
2385 if (item.text() == QLatin1String("..")) {
2386 return false;
2387 }
2388
2389 if (!settings.isShowingDotFiles && item.isHidden()) {
2390 return false;
2391 }
2392
2393 if (item.isDir() || settings.lstFilters.isEmpty()) {
2394 return true;
2395 }
2396
2397 return std::any_of(first: settings.lstFilters.cbegin(), last: settings.lstFilters.cend(), pred: [&item](const QRegularExpression &filter) {
2398 return filter.match(subject: item.text()).hasMatch();
2399 });
2400}
2401
2402bool KCoreDirListerPrivate::matchesMimeFilter(const KFileItem &item) const
2403{
2404 Q_ASSERT(!item.isNull());
2405 // Don't lose time determining the MIME type if there is no filter
2406 if (settings.mimeFilter.isEmpty() && settings.mimeExcludeFilter.isEmpty()) {
2407 return true;
2408 }
2409 return doMimeFilter(mimeType: item.mimetype(), filters: settings.mimeFilter) && doMimeExcludeFilter(mimeExclude: item.mimetype(), filters: settings.mimeExcludeFilter);
2410}
2411
2412bool KCoreDirListerPrivate::doMimeFilter(const QString &mime, const QStringList &filters) const
2413{
2414 if (filters.isEmpty()) {
2415 return true;
2416 }
2417
2418 QMimeDatabase db;
2419 const QMimeType mimeptr = db.mimeTypeForName(nameOrAlias: mime);
2420 if (!mimeptr.isValid()) {
2421 return false;
2422 }
2423
2424 qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: investigating:" << mimeptr.name();
2425 return std::any_of(first: filters.cbegin(), last: filters.cend(), pred: [&mimeptr](const QString &filter) {
2426 return mimeptr.inherits(mimeTypeName: filter);
2427 });
2428}
2429
2430bool KCoreDirListerPrivate::doMimeExcludeFilter(const QString &mime, const QStringList &filters) const
2431{
2432 return !std::any_of(first: filters.cbegin(), last: filters.cend(), pred: [&mime](const QString &filter) {
2433 return mime == filter;
2434 });
2435}
2436
2437// ================= private methods ================= //
2438
2439void KCoreDirListerPrivate::addNewItem(const QUrl &directoryUrl, const KFileItem &item)
2440{
2441 if (!isItemVisible(item)) {
2442 return; // No reason to continue... bailing out here prevents a MIME type scan.
2443 }
2444
2445 qCDebug(KIO_CORE_DIRLISTER) << "in" << directoryUrl << "item:" << item.url();
2446
2447 if (matchesMimeFilter(item)) {
2448 Q_ASSERT(!item.isNull());
2449 lstNewItems[directoryUrl].append(t: item); // items not filtered
2450 } else {
2451 Q_ASSERT(!item.isNull());
2452 lstMimeFilteredItems.append(t: item); // only filtered by MIME type
2453 }
2454}
2455
2456void KCoreDirListerPrivate::addNewItems(const QUrl &directoryUrl, const QList<KFileItem> &items)
2457{
2458 // TODO: make this faster - test if we have a filter at all first
2459 // DF: was this profiled? The matchesFoo() functions should be fast, w/o filters...
2460 // Of course if there is no filter and we can do a range-insertion instead of a loop, that might be good.
2461 for (const auto &item : items) {
2462 addNewItem(directoryUrl, item);
2463 }
2464}
2465
2466void KCoreDirListerPrivate::addRefreshItem(const QUrl &directoryUrl, const KFileItem &oldItem, const KFileItem &item)
2467{
2468 // Refreshing the root item "." of a dirlister
2469 if (directoryUrl == item.url()) {
2470 lstRefreshItems.append(t: {oldItem, item});
2471 return;
2472 }
2473
2474 const bool refreshItemWasFiltered = !isItemVisible(item: oldItem) || !matchesMimeFilter(item: oldItem);
2475 if (item.exists() && isItemVisible(item) && matchesMimeFilter(item)) {
2476 if (refreshItemWasFiltered) {
2477 Q_ASSERT(!item.isNull());
2478 lstNewItems[directoryUrl].append(t: item);
2479 } else {
2480 Q_ASSERT(!item.isNull());
2481 lstRefreshItems.append(t: qMakePair(value1: oldItem, value2: item));
2482 }
2483 } else if (!refreshItemWasFiltered) {
2484 // notify the user that the MIME type of a file changed that doesn't match
2485 // a filter or does match an exclude filter
2486 // This also happens when renaming foo to .foo and dot files are hidden (#174721)
2487 Q_ASSERT(!oldItem.isNull());
2488 lstRemoveItems.append(t: oldItem);
2489 }
2490}
2491
2492void KCoreDirListerPrivate::emitItems()
2493{
2494 if (!lstNewItems.empty()) {
2495 for (auto it = lstNewItems.cbegin(); it != lstNewItems.cend(); ++it) {
2496 const auto &val = it.value();
2497 Q_EMIT q->itemsAdded(directoryUrl: it.key(), items: val);
2498 Q_EMIT q->newItems(items: val); // compat
2499 }
2500 lstNewItems.clear();
2501 }
2502
2503 if (!lstMimeFilteredItems.empty()) {
2504 Q_EMIT q->itemsFilteredByMime(items: lstMimeFilteredItems);
2505 lstMimeFilteredItems.clear();
2506 }
2507
2508 if (!lstRefreshItems.empty()) {
2509 Q_EMIT q->refreshItems(items: lstRefreshItems);
2510 lstRefreshItems.clear();
2511 }
2512
2513 if (!lstRemoveItems.empty()) {
2514 Q_EMIT q->itemsDeleted(items: lstRemoveItems);
2515 lstRemoveItems.clear();
2516 }
2517}
2518
2519bool KCoreDirListerPrivate::isItemVisible(const KFileItem &item) const
2520{
2521 // Note that this doesn't include MIME type filters, because
2522 // of the itemsFilteredByMime signal. Filtered-by-MIME-type items are
2523 // considered "visible", they are just visible via a different signal...
2524 return (!settings.dirOnlyMode || item.isDir()) && matchesFilter(item);
2525}
2526
2527void KCoreDirListerPrivate::emitItemsDeleted(const KFileItemList &itemsList)
2528{
2529 KFileItemList items;
2530 std::copy_if(first: itemsList.cbegin(), last: itemsList.cend(), result: std::back_inserter(x&: items), pred: [this](const KFileItem &item) {
2531 return isItemVisible(item) || matchesMimeFilter(item);
2532 });
2533 if (!items.isEmpty()) {
2534 Q_EMIT q->itemsDeleted(items);
2535 }
2536}
2537
2538KCoreDirListerPrivate::KCoreDirListerPrivate(KCoreDirLister *qq)
2539 : q(qq)
2540{
2541}
2542
2543// ================ private slots ================ //
2544
2545void KCoreDirListerPrivate::slotInfoMessage(KJob *, const QString &message)
2546{
2547 Q_EMIT q->infoMessage(msg: message);
2548}
2549
2550void KCoreDirListerPrivate::slotPercent(KJob *job, unsigned long pcnt)
2551{
2552 jobData[static_cast<KIO::ListJob *>(job)].percent = pcnt;
2553
2554 int result = 0;
2555
2556 KIO::filesize_t size = 0;
2557
2558 for (auto dataIt = jobData.cbegin(); dataIt != jobData.cend(); ++dataIt) {
2559 const JobData &data = dataIt.value();
2560 result += data.percent * data.totalSize;
2561 size += data.totalSize;
2562 }
2563
2564 if (size != 0) {
2565 result /= size;
2566 } else {
2567 result = 100;
2568 }
2569 Q_EMIT q->percent(percent: result);
2570}
2571
2572void KCoreDirListerPrivate::slotTotalSize(KJob *job, qulonglong size)
2573{
2574 jobData[static_cast<KIO::ListJob *>(job)].totalSize = size;
2575
2576 KIO::filesize_t result = 0;
2577 for (auto dataIt = jobData.cbegin(); dataIt != jobData.cend(); ++dataIt) {
2578 result += dataIt.value().totalSize;
2579 }
2580
2581 Q_EMIT q->totalSize(size: result);
2582}
2583
2584void KCoreDirListerPrivate::slotProcessedSize(KJob *job, qulonglong size)
2585{
2586 jobData[static_cast<KIO::ListJob *>(job)].processedSize = size;
2587
2588 KIO::filesize_t result = 0;
2589 for (auto dataIt = jobData.cbegin(); dataIt != jobData.cend(); ++dataIt) {
2590 result += dataIt.value().processedSize;
2591 }
2592
2593 Q_EMIT q->processedSize(size: result);
2594}
2595
2596void KCoreDirListerPrivate::slotSpeed(KJob *job, unsigned long spd)
2597{
2598 jobData[static_cast<KIO::ListJob *>(job)].speed = spd;
2599
2600 int result = 0;
2601 for (auto dataIt = jobData.cbegin(); dataIt != jobData.cend(); ++dataIt) {
2602 result += dataIt.value().speed;
2603 }
2604
2605 Q_EMIT q->speed(bytes_per_second: result);
2606}
2607
2608uint KCoreDirListerPrivate::numJobs()
2609{
2610#ifdef DEBUG_CACHE
2611 // This code helps detecting stale entries in the jobData map.
2612 qCDebug(KIO_CORE_DIRLISTER) << q << "numJobs:" << jobData.count();
2613 for (auto it = jobData.cbegin(); it != jobData.cend(); ++it) {
2614 qCDebug(KIO_CORE_DIRLISTER) << (void *)it.key();
2615 qCDebug(KIO_CORE_DIRLISTER) << it.key();
2616 }
2617#endif
2618
2619 return jobData.count();
2620}
2621
2622void KCoreDirListerPrivate::jobDone(KIO::ListJob *job)
2623{
2624 jobData.remove(key: job);
2625}
2626
2627void KCoreDirLister::jobStarted(KIO::ListJob *job)
2628{
2629 KCoreDirListerPrivate::JobData data;
2630 data.speed = 0;
2631 data.percent = 0;
2632 data.processedSize = 0;
2633 data.totalSize = 0;
2634
2635 d->jobData.insert(key: job, value: data);
2636 d->complete = false;
2637}
2638
2639void KCoreDirListerPrivate::connectJob(KIO::ListJob *job)
2640{
2641 q->connect(sender: job, signal: &KJob::infoMessage, context: q, slot: [this](KJob *job, const QString &plain) {
2642 slotInfoMessage(job, message: plain);
2643 });
2644
2645 q->connect(sender: job, signal: &KJob::percentChanged, context: q, slot: [this](KJob *job, ulong _percent) {
2646 slotPercent(job, pcnt: _percent);
2647 });
2648
2649 q->connect(sender: job, signal: &KJob::totalSize, context: q, slot: [this](KJob *job, qulonglong _size) {
2650 slotTotalSize(job, size: _size);
2651 });
2652 q->connect(sender: job, signal: &KJob::processedSize, context: q, slot: [this](KJob *job, qulonglong _psize) {
2653 slotProcessedSize(job, size: _psize);
2654 });
2655 q->connect(sender: job, signal: &KJob::speed, context: q, slot: [this](KJob *job, qulonglong _speed) {
2656 slotSpeed(job, spd: _speed);
2657 });
2658}
2659
2660KFileItemList KCoreDirLister::items(WhichItems which) const
2661{
2662 return itemsForDir(dirUrl: url(), which);
2663}
2664
2665KFileItemList KCoreDirLister::itemsForDir(const QUrl &dir, WhichItems which) const
2666{
2667 QList<KFileItem> *allItems = s_kDirListerCache.localData().itemsForDir(dir);
2668 KFileItemList result;
2669 if (!allItems) {
2670 return result;
2671 }
2672
2673 if (which == AllItems) {
2674 return KFileItemList(*allItems);
2675 } else { // only items passing the filters
2676 std::copy_if(first: allItems->cbegin(), last: allItems->cend(), result: std::back_inserter(x&: result), pred: [this](const KFileItem &item) {
2677 return d->isItemVisible(item) && d->matchesMimeFilter(item);
2678 });
2679 }
2680 return result;
2681}
2682
2683bool KCoreDirLister::delayedMimeTypes() const
2684{
2685 return d->delayedMimeTypes;
2686}
2687
2688void KCoreDirLister::setDelayedMimeTypes(bool delayedMimeTypes)
2689{
2690 d->delayedMimeTypes = delayedMimeTypes;
2691}
2692
2693// called by KCoreDirListerCache::slotRedirection
2694void KCoreDirListerPrivate::redirect(const QUrl &oldUrl, const QUrl &newUrl, bool keepItems)
2695{
2696 if (url.matches(url: oldUrl, options: QUrl::StripTrailingSlash)) {
2697 if (!keepItems) {
2698 rootFileItem = KFileItem();
2699 } else {
2700 rootFileItem.setUrl(newUrl);
2701 }
2702 url = newUrl;
2703 }
2704
2705 const int idx = lstDirs.indexOf(t: oldUrl);
2706 if (idx == -1) {
2707 qCWarning(KIO_CORE) << "Unexpected redirection from" << oldUrl << "to" << newUrl << "but this dirlister is currently listing/holding" << lstDirs;
2708 } else {
2709 lstDirs[idx] = newUrl;
2710 }
2711
2712 if (lstDirs.count() == 1) {
2713 if (!keepItems) {
2714 Q_EMIT q->clear();
2715 }
2716 } else {
2717 if (!keepItems) {
2718 Q_EMIT q->clearDir(dirUrl: oldUrl);
2719 }
2720 }
2721 Q_EMIT q->redirection(oldUrl, newUrl);
2722}
2723
2724void KCoreDirListerCacheDirectoryData::moveListersWithoutCachedItemsJob(const QUrl &url)
2725{
2726 // Move dirlisters from listersCurrentlyListing to listersCurrentlyHolding,
2727 // but not those that are still waiting on a CachedItemsJob...
2728 // Unit-testing note:
2729 // Run kdirmodeltest in valgrind to hit the case where an update
2730 // is triggered while a lister has a CachedItemsJob (different timing...)
2731 QMutableListIterator<KCoreDirLister *> lister_it(listersCurrentlyListing);
2732 while (lister_it.hasNext()) {
2733 KCoreDirLister *kdl = lister_it.next();
2734 if (!kdl->d->cachedItemsJobForUrl(url)) {
2735 // OK, move this lister from "currently listing" to "currently holding".
2736
2737 // Huh? The KCoreDirLister was present twice in listersCurrentlyListing, or was in both lists?
2738 Q_ASSERT(!listersCurrentlyHolding.contains(kdl));
2739 if (!listersCurrentlyHolding.contains(t: kdl)) {
2740 listersCurrentlyHolding.append(t: kdl);
2741 }
2742 lister_it.remove();
2743 } else {
2744 qCDebug(KIO_CORE_DIRLISTER) << "Not moving" << kdl << "to listersCurrentlyHolding because it still has job" << kdl->d->m_cachedItemsJobs;
2745 }
2746 }
2747}
2748
2749KFileItem KCoreDirLister::cachedItemForUrl(const QUrl &url)
2750{
2751 if (s_kDirListerCache.hasLocalData()) {
2752 return s_kDirListerCache.localData().itemForUrl(url);
2753 } else {
2754 return {};
2755 }
2756}
2757
2758bool KCoreDirLister::autoErrorHandlingEnabled() const
2759{
2760 return d->m_autoErrorHandling;
2761}
2762
2763void KCoreDirLister::setAutoErrorHandlingEnabled(bool enable)
2764{
2765 d->m_autoErrorHandling = enable;
2766}
2767
2768KCoreDirListerCache::CacheHiddenFile *KCoreDirListerCache::cachedDotHiddenForDir(const QString &dir)
2769{
2770 const QString path = dir + QLatin1String("/.hidden");
2771 QFile dotHiddenFile(path);
2772
2773 if (dotHiddenFile.exists()) {
2774 const QDateTime mtime = QFileInfo(dotHiddenFile).lastModified();
2775 CacheHiddenFile *cachedDotHiddenFile = m_cacheHiddenFiles.object(key: path);
2776
2777 if (cachedDotHiddenFile && mtime <= cachedDotHiddenFile->mtime) {
2778 // ".hidden" is in cache and still valid (the file was not modified since then),
2779 // so return it
2780 return cachedDotHiddenFile;
2781 } else {
2782 // read the ".hidden" file, then cache it and return it
2783 if (dotHiddenFile.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
2784 std::set<QString> filesToHide;
2785 QTextStream stream(&dotHiddenFile);
2786 while (!stream.atEnd()) {
2787 QString name = stream.readLine();
2788 if (!name.isEmpty()) {
2789 filesToHide.insert(x: name);
2790 }
2791 }
2792
2793 m_cacheHiddenFiles.insert(key: path, object: new CacheHiddenFile(mtime, std::move(filesToHide)));
2794
2795 return m_cacheHiddenFiles.object(key: path);
2796 }
2797 }
2798 }
2799
2800 return {};
2801}
2802
2803#include "moc_kcoredirlister.cpp"
2804#include "moc_kcoredirlister_p.cpp"
2805

source code of kio/src/core/kcoredirlister.cpp