KIO

kcoredirlister.cpp
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
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(&pendingUpdateTimer, &QTimer::timeout, this, &KCoreDirListerCache::processPendingUpdates);
53 pendingUpdateTimer.setSingleShot(true);
54
55 connect(KDirWatch::self(), &KDirWatch::dirty, this, &KCoreDirListerCache::slotFileDirty);
56 connect(KDirWatch::self(), &KDirWatch::created, this, &KCoreDirListerCache::slotFileCreated);
57 connect(KDirWatch::self(), &KDirWatch::deleted, this, &KCoreDirListerCache::slotFileDeleted);
58
59#ifdef WITH_QTDBUS
60 kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this);
61 connect(kdirnotify, &org::kde::KDirNotify::FileRenamedWithLocalPath, this, &KCoreDirListerCache::slotFileRenamed);
62 connect(kdirnotify, &org::kde::KDirNotify::FilesAdded, this, &KCoreDirListerCache::slotFilesAdded);
63 connect(kdirnotify, &org::kde::KDirNotify::FilesChanged, this, &KCoreDirListerCache::slotFilesChanged);
64 connect(kdirnotify, &org::kde::KDirNotify::FilesRemoved, this, qOverload<const QStringList &>(&KCoreDirListerCache::slotFilesRemoved));
65#endif
66}
67
68KCoreDirListerCache::~KCoreDirListerCache()
69{
70 qCDebug(KIO_CORE_DIRLISTER);
71
72 qDeleteAll(itemsInUse);
73 itemsInUse.clear();
74
75 itemsCached.clear();
76 directoryData.clear();
77 m_cacheHiddenFiles.clear();
78
79 if (KDirWatch::exists()) {
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(QDir::cleanPath(_url.path())); // kill consecutive slashes
90
91 // like this we don't have to worry about trailing slashes any further
92 _url = _url.adjusted(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(resolved)].append(_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, 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(_url)) {
122 // stop the job listing _url for this lister
123 stopListingUrl(lister, _url, 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(_url);
129
130 // clear _url for lister
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(_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(_url);
147
148 KCoreDirListerCacheDirectoryData &dirData = directoryData[_url]; // find or insert
149
150 if (dirData.listerCountByStatus(ListerStatus::Listing) == 0) {
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.insertOrModifyLister(lister, ListerStatus::Listing);
155
156 DirItem *itemFromCache = nullptr;
157 if (itemU || (!_reload && (itemFromCache = itemsCached.take(_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(_url, 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(_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(_url);
186 } else {
187 qCDebug(KIO_CORE_DIRLISTER) << "Listing directory:" << _url;
188 }
189
190 itemU = new DirItem(_url, resolved);
191 itemsInUse.insert(_url, itemU);
192 if (lister->d->autoUpdate) {
193 itemU->incAutoUpdate();
194 }
195
197 if (lister->requestMimeTypeWhileListing()) {
198 job->addMetaData(QStringLiteral("details"), QString::number(KIO::StatDefaultDetails | KIO::StatMimeType));
199 }
200 runningListJobs.insert(job, KIO::UDSEntryList());
201
202 lister->jobStarted(job);
203 lister->d->connectJob(job);
204
205 connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotEntries);
206 connect(job, &KJob::result, this, &KCoreDirListerCache::slotResult);
207 connect(job, &KIO::ListJob::redirection, this, &KCoreDirListerCache::slotRedirection);
208
209 Q_EMIT lister->started(_url);
210
211 qCDebug(KIO_CORE_DIRLISTER) << "Entry now being listed by" << dirData.listersByStatus(ListerStatus::Listing);
212 }
213 } else {
214 qCDebug(KIO_CORE_DIRLISTER) << "Entry currently being listed:" << _url << "by" << dirData.listersByStatus(ListerStatus::Listing);
215#ifdef DEBUG_CACHE
216 printDebug();
217#endif
218
219 Q_EMIT lister->started(_url);
220
221 Q_ASSERT(!dirData.contains(lister, ListerStatus::Listing));
222 dirData.insertOrModifyLister(lister, ListerStatus::Listing);
223
224 // a new lister is listing this _url, incr watch refcount
225 if (lister->d->autoUpdate) {
226 itemU->incAutoUpdate();
227 }
228
229 KIO::ListJob *job = jobForUrl(_url);
230 // job will be 0 if we were listing from cache rather than listing from a kio job.
231 if (job) {
232 lister->jobStarted(job);
233 lister->d->connectJob(job);
234 }
235 Q_ASSERT(itemU);
236
237 // List existing items in a delayed manner, just like things would happen
238 // if we were not using the cache.
239 qCDebug(KIO_CORE_DIRLISTER) << "Listing" << itemU->lstItems.count() << "cached items soon";
240 auto *cachedItemsJob = new KCoreDirListerPrivate::CachedItemsJob(lister, _url, _reload);
241 if (job) {
242 // The ListJob will take care of emitting completed.
243 // ### If it finishes before the CachedItemsJob, then we'll emit cached items after completed(), not sure how bad this is.
244 cachedItemsJob->setEmitCompleted(false);
245 }
246
247#ifdef DEBUG_CACHE
248 printDebug();
249#endif
250 }
251
252 return true;
253}
254
255KCoreDirListerPrivate::CachedItemsJob *KCoreDirListerPrivate::cachedItemsJobForUrl(const QUrl &url) const
256{
257 for (CachedItemsJob *job : m_cachedItemsJobs) {
258 if (job->url() == url) {
259 return job;
260 }
261 }
262 return nullptr;
263}
264
265KCoreDirListerPrivate::CachedItemsJob::CachedItemsJob(KCoreDirLister *lister, const QUrl &url, bool reload)
266 : KJob(lister)
267 , m_lister(lister)
268 , m_url(url)
269 , m_reload(reload)
270 , m_emitCompleted(true)
271{
272 qCDebug(KIO_CORE_DIRLISTER) << "Creating CachedItemsJob" << this << "for lister" << lister << url;
273 if (lister->d->cachedItemsJobForUrl(url)) {
274 qCWarning(KIO_CORE) << "Lister" << lister << "has a cached items job already for" << url;
275 }
276 lister->d->m_cachedItemsJobs.append(this);
277 setAutoDelete(true);
278 start();
279}
280
281// Called by start() via QueuedConnection
282void KCoreDirListerPrivate::CachedItemsJob::done()
283{
284 if (!m_lister) { // job was already killed, but waiting deletion due to deleteLater
285 return;
286 }
287 s_kDirListerCache.localData().emitItemsFromCache(this, m_lister, m_url, m_reload, m_emitCompleted);
288 emitResult();
289}
290
291bool KCoreDirListerPrivate::CachedItemsJob::doKill()
292{
293 qCDebug(KIO_CORE_DIRLISTER) << this;
294 s_kDirListerCache.localData().forgetCachedItemsJob(this, m_lister, m_url);
295 if (!property("_kdlc_silent").toBool()) {
296 Q_EMIT m_lister->listingDirCanceled(m_url);
297
298 Q_EMIT m_lister->canceled();
299 }
300 m_lister = nullptr;
301 return true;
302}
303
304void KCoreDirListerCache::emitItemsFromCache(KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob,
305 KCoreDirLister *lister,
306 const QUrl &_url,
307 bool _reload,
308 bool _emitCompleted)
309{
310 lister->d->complete = false;
311
312 DirItem *itemU = s_kDirListerCache.localData().itemsInUse.value(_url);
313 if (!itemU) {
314 qCWarning(KIO_CORE) << "Can't find item for directory" << _url << "anymore";
315 } else {
316 const QList<KFileItem> items = itemU->lstItems;
317 const KFileItem rootItem = itemU->rootItem;
318 _reload = _reload || !itemU->complete;
319
320 if (lister->d->rootFileItem.isNull() && !rootItem.isNull() && lister->d->url == _url) {
321 lister->d->rootFileItem = rootItem;
322 }
323 if (!items.isEmpty()) {
324 qCDebug(KIO_CORE_DIRLISTER) << "emitting" << items.count() << "for lister" << lister;
325 lister->d->addNewItems(_url, items);
326 lister->d->emitItems();
327 }
328 }
329
330 forgetCachedItemsJob(cachedItemsJob, lister, _url);
331
332 // Emit completed, unless we were told not to,
333 // or if listDir() was called while another directory listing for this dir was happening,
334 // so we "joined" it. We detect that using jobForUrl to ensure it's a real ListJob,
335 // not just a lister-specific CachedItemsJob (which wouldn't emit completed for us).
336 if (_emitCompleted) {
337 lister->d->complete = true;
338
339 Q_EMIT lister->listingDirCompleted(_url);
340 Q_EMIT lister->completed();
341
342 if (_reload) {
343 updateDirectory(_url);
344 }
345 }
346}
347
348void KCoreDirListerCache::forgetCachedItemsJob(KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob, KCoreDirLister *lister, const QUrl &_url)
349{
350 // Modifications to data structures only below this point;
351 // so that addNewItems is called with a consistent state
352
353 lister->d->m_cachedItemsJobs.removeAll(cachedItemsJob);
354
355 KCoreDirListerCacheDirectoryData &dirData = directoryData[_url];
356 Q_ASSERT(dirData.contains(lister, ListerStatus::Listing));
357
358 KIO::ListJob *listJob = jobForUrl(_url);
359 if (!listJob) {
360 Q_ASSERT(!dirData.contains(lister, ListerStatus::Holding));
361 qCDebug(KIO_CORE_DIRLISTER) << "Moving from listing to holding, because no more job" << lister << _url;
362 dirData.insertOrModifyLister(lister, ListerStatus::Holding);
363 } else {
364 qCDebug(KIO_CORE_DIRLISTER) << "Still having a listjob" << listJob << ", so not moving to currently-holding.";
365 }
366}
367
368void KCoreDirListerCache::stop(KCoreDirLister *lister, bool silent)
369{
370 qCDebug(KIO_CORE_DIRLISTER) << "lister:" << lister << "silent=" << silent;
371
372 const QList<QUrl> urls = lister->d->lstDirs;
373 for (const QUrl &url : urls) {
374 stopListingUrl(lister, url, silent);
375 }
376}
377
378void KCoreDirListerCache::stopListingUrl(KCoreDirLister *lister, const QUrl &_u, bool silent)
379{
380 QUrl url(_u);
382
383 KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(url);
384 if (cachedItemsJob) {
385 if (silent) {
386 cachedItemsJob->setProperty("_kdlc_silent", true);
387 }
388 cachedItemsJob->kill(); // removes job from list, too
389 }
390
391 // TODO: consider to stop all the "child jobs" of url as well
392 qCDebug(KIO_CORE_DIRLISTER) << lister << " url=" << url;
393
394 const auto dirit = directoryData.find(url);
395 if (dirit == directoryData.end()) {
396 return;
397 }
398 KCoreDirListerCacheDirectoryData &dirData = dirit.value();
399 auto listersCurrentlyListing = dirData.listersByStatus(ListerStatus::Listing);
400 if (listersCurrentlyListing.contains(lister)) {
401 qCDebug(KIO_CORE_DIRLISTER) << " found lister" << lister << "in list - for" << url;
402 if (listersCurrentlyListing.count() == 1) {
403 // This was the only dirlister interested in the list job -> kill the job
404 stopListJob(url, silent);
405 } else {
406 // Leave the job running for the other dirlisters, just unsubscribe us.
407 dirData.removeLister(lister);
408 if (!silent) {
409 Q_EMIT lister->canceled();
410 Q_EMIT lister->listingDirCanceled(url);
411 }
412 }
413 }
414}
415
416// Helper for stop() and stopListingUrl()
417void KCoreDirListerCache::stopListJob(const QUrl &url, bool silent)
418{
419 // Old idea: if it's an update job, let's just leave the job running.
420 // After all, update jobs do run for "listersCurrentlyHolding",
421 // so there's no reason to kill them just because @p lister is now a holder.
422
423 // However it could be a long-running non-local job (e.g. filenamesearch), which
424 // the user wants to abort, and which will never be used for updating...
425 // And in any case slotEntries/slotResult is not meant to be called by update jobs.
426 // So, change of plan, let's kill it after all, in a way that triggers slotResult/slotUpdateResult.
427
428 KIO::ListJob *job = jobForUrl(url);
429 if (job) {
430 qCDebug(KIO_CORE_DIRLISTER) << "Killing list job" << job << "for" << url;
431 if (silent) {
432 job->setProperty("_kdlc_silent", true);
433 }
434 job->kill(KJob::EmitResult);
435 }
436}
437
438void KCoreDirListerCache::setAutoUpdate(KCoreDirLister *lister, bool enable)
439{
440 // IMPORTANT: this method does not check for the current autoUpdate state!
441
442 for (const QUrl &url : std::as_const(lister->d->lstDirs)) {
443 DirItem *dirItem = itemsInUse.value(url);
444 Q_ASSERT(dirItem);
445 if (enable) {
446 dirItem->incAutoUpdate();
447 } else {
448 dirItem->decAutoUpdate();
449 }
450 }
451}
452
453void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister)
454{
455 qCDebug(KIO_CORE_DIRLISTER) << lister;
456
457 Q_EMIT lister->clear();
458 // clear lister->d->lstDirs before calling forgetDirs(), so that
459 // it doesn't contain things that itemsInUse doesn't. When emitting
460 // the canceled signals, lstDirs must not contain anything that
461 // itemsInUse does not contain. (otherwise it might crash in findByName()).
462 const QList<QUrl> lstDirsCopy = lister->d->lstDirs;
463 lister->d->lstDirs.clear();
464
465 qCDebug(KIO_CORE_DIRLISTER) << "Iterating over dirs" << lstDirsCopy;
466 const auto possibleMountPoints = KMountPoint::possibleMountPoints(KMountPoint::NeedMountOptions);
467 for (const QUrl &dir : lstDirsCopy) {
468 forgetDirs(lister, dir, false, possibleMountPoints);
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(QLatin1String("noauto"));
483}
484
485void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, bool notify, const KMountPoint::List &possibleMountPoints)
486{
487 qCDebug(KIO_CORE_DIRLISTER) << lister << " _url: " << _url;
488
489 const QUrl url = _url.adjusted(QUrl::StripTrailingSlash);
490
491 DirectoryDataHash::iterator dit = directoryData.find(url);
492 if (dit == directoryData.end()) {
493 return;
494 }
495 KCoreDirListerCacheDirectoryData &dirData = *dit;
496 dirData.removeLister(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(url);
505 Q_ASSERT(item);
506 bool insertIntoCache = false;
507
508 if (dirData.totaListerCount() == 0) {
509 // item not in use anymore -> move into cache if complete
510 directoryData.erase(dit);
511 itemsInUse.remove(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(url);
530 Q_EMIT lister->clearDir(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
540 // Should we forget the dir for good, or keep a watch on it?
541 // Generally keep a watch, except when it would prevent
542 // unmounting a removable device (#37780)
543 const bool isLocal = item->url.isLocalFile();
544 bool isManuallyMounted = false;
545 bool containsManuallyMounted = false;
546 if (isLocal) {
547 isManuallyMounted = manually_mounted(item->url.toLocalFile(), possibleMountPoints);
548 if (!isManuallyMounted) {
549 // Look for a manually-mounted directory inside
550 // If there's one, we can't keep a watch either, FAM would prevent unmounting the CDROM
551 // I hope this isn't too slow
552 auto kit = item->lstItems.constBegin();
553 const auto kend = item->lstItems.constEnd();
554 for (; kit != kend && !containsManuallyMounted; ++kit) {
555 if ((*kit).isDir() && manually_mounted((*kit).url().toLocalFile(), possibleMountPoints)) {
556 containsManuallyMounted = true;
557 }
558 }
559 }
560 }
561
562 if (isManuallyMounted || containsManuallyMounted) { // [**]
563 qCDebug(KIO_CORE_DIRLISTER) << "Not adding a watch on " << item->url << " because it "
564 << (isManuallyMounted ? "is manually mounted" : "contains a manually mounted subdir");
565 item->complete = false; // set to "dirty"
566 } else {
567 item->incAutoUpdate(); // keep watch, incr refcount to account for cache
568 item->watchedWhileInCache = true;
569 }
570 } else {
571 delete item;
572 item = nullptr;
573 }
574 }
575
576 if (item && lister->d->autoUpdate) {
577 item->decAutoUpdate();
578 }
579
580 // Inserting into QCache must be done last, since it might delete the item
581 if (item && insertIntoCache) {
582 qCDebug(KIO_CORE_DIRLISTER) << lister << "item moved into cache:" << url;
583 itemsCached.insert(url, item);
584 }
585}
586
587void KCoreDirListerCache::updateDirectory(const QUrl &_dir)
588{
589 qCDebug(KIO_CORE_DIRLISTER) << _dir;
590
592 if (!checkUpdate(dir)) {
593 auto parentDir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
594 if (checkUpdate(parentDir)) {
595 // if the parent is in use, update it instead
596 dir = parentDir;
597 } else {
598 return;
599 }
600 }
601
602 // A job can be running to
603 // - only list a new directory: the listers are in listersCurrentlyListing
604 // - only update a directory: the listers are in listersCurrentlyHolding
605 // - update a currently running listing: the listers are in both
606
607 KCoreDirListerCacheDirectoryData &dirData = directoryData[dir];
608 const QList<KCoreDirLister *> listers = dirData.listersByStatus(ListerStatus::Listing);
609 const QList<KCoreDirLister *> holders = dirData.listersByStatus(ListerStatus::Holding);
610
611 qCDebug(KIO_CORE_DIRLISTER) << dir << "listers=" << listers << "holders=" << holders;
612
613 bool killed = false;
614 KIO::ListJob *job = jobForUrl(dir);
615 if (job) {
616 // the job is running already, tell it to do another update at the end
617 // (don't kill it, we would keep doing that during a long download to a slow sshfs mount)
618 job->setProperty("need_another_update", true);
619 return;
620 } else {
621 // Emit any cached items.
622 // updateDirectory() is about the diff compared to the cached items...
623 for (const KCoreDirLister *lister : listers) {
624 KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(dir);
625 if (cachedItemsJob) {
626 cachedItemsJob->setEmitCompleted(false);
627 cachedItemsJob->done(); // removes from cachedItemsJobs list
628 delete cachedItemsJob;
629 killed = true;
630 }
631 }
632 }
633 qCDebug(KIO_CORE_DIRLISTER) << "Killed=" << killed;
634
635 // we don't need to emit canceled signals since we only replaced the job,
636 // the listing is continuing.
637
638 if (!(listers.isEmpty() || killed)) {
639 qCWarning(KIO_CORE) << "The unexpected happened.";
640 qCWarning(KIO_CORE) << "listers for" << dir << "=" << listers;
641 qCWarning(KIO_CORE) << "job=" << job;
642 for (const KCoreDirLister *lister : listers) {
643 qCDebug(KIO_CORE_DIRLISTER) << "lister" << lister << "m_cachedItemsJobs=" << lister->d->m_cachedItemsJobs;
644 }
645#ifndef NDEBUG
646 printDebug();
647#endif
648 }
649 Q_ASSERT(listers.isEmpty() || killed);
650
652 runningListJobs.insert(job, KIO::UDSEntryList());
653
654 const bool requestFromListers = std::any_of(listers.cbegin(), listers.cend(), [](KCoreDirLister *lister) {
655 return lister->requestMimeTypeWhileListing();
656 });
657 const bool requestFromholders = std::any_of(holders.cbegin(), holders.cend(), [](KCoreDirLister *lister) {
658 return lister->requestMimeTypeWhileListing();
659 });
660
661 if (requestFromListers || requestFromholders) {
662 job->addMetaData(QStringLiteral("details"), QString::number(KIO::StatDefaultDetails | KIO::StatMimeType));
663 }
664
665 connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries);
666 connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult);
667
668 qCDebug(KIO_CORE_DIRLISTER) << "update started in" << dir;
669
670 for (KCoreDirLister *lister : listers) {
671 lister->jobStarted(job);
672 }
673
674 if (!holders.isEmpty()) {
675 if (!killed) {
676 for (KCoreDirLister *holder : holders) {
677 holder->jobStarted(job);
678 Q_EMIT holder->started(dir);
679 }
680 } else {
681 for (KCoreDirLister *holder : holders) {
682 holder->jobStarted(job);
683 }
684 }
685 }
686}
687
688bool KCoreDirListerCache::checkUpdate(const QUrl &_dir)
689{
690 if (!itemsInUse.contains(_dir)) {
691 DirItem *item = itemsCached[_dir];
692 if (item && item->complete) {
693 item->complete = false;
694 item->watchedWhileInCache = false;
695 item->decAutoUpdate();
696 qCDebug(KIO_CORE_DIRLISTER) << "directory " << _dir << " not in use, marked dirty.";
697 }
698 // else
699 qCDebug(KIO_CORE_DIRLISTER) << "aborted, directory " << _dir << " not in cache.";
700 return false;
701 } else {
702 return true;
703 }
704}
705
706KFileItem KCoreDirListerCache::itemForUrl(const QUrl &url) const
707{
708 return findByUrl(nullptr, url);
709}
710
711KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir) const
712{
713 const QUrl url = dir.adjusted(QUrl::StripTrailingSlash);
714 DirItem *item = itemsInUse.value(url);
715 if (!item) {
716 item = itemsCached[url];
717 }
718 return item;
719}
720
721QList<KFileItem> *KCoreDirListerCache::itemsForDir(const QUrl &dir) const
722{
723 DirItem *item = dirItemForUrl(dir);
724 return item ? &item->lstItems : nullptr;
725}
726
727KFileItem KCoreDirListerCache::findByName(const KCoreDirLister *lister, const QString &_name) const
728{
729 Q_ASSERT(lister);
730
731 auto isMatch = [&_name](const KFileItem &item) {
732 return _name == item.name();
733 };
734
735 for (const auto &dirUrl : std::as_const(lister->d->lstDirs)) {
736 DirItem *dirItem = itemsInUse.value(dirUrl);
737 Q_ASSERT(dirItem);
738
739 auto it = std::find_if(dirItem->lstItems.cbegin(), dirItem->lstItems.cend(), isMatch);
740 if (it != dirItem->lstItems.cend()) {
741 return *it;
742 }
743 }
744
745 return {};
746}
747
748KFileItem KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const
749{
750 QUrl url(_u);
752
754 DirItem *dirItem = dirItemForUrl(parentDir);
755 if (dirItem) {
756 // If lister is set, check that it contains this dir
757 if (!lister || lister->d->lstDirs.contains(parentDir)) {
758 // Binary search
759 auto it = std::lower_bound(dirItem->lstItems.begin(), dirItem->lstItems.end(), url);
760 if (it != dirItem->lstItems.end() && it->url() == url) {
761 return *it;
762 }
763 }
764 }
765
766 // Maybe _u is a directory itself? (see KDirModelTest::testChmodDirectory)
767 // We check this last, though, we prefer returning a kfileitem with an actual
768 // name if possible (and we make it '.' for root items later).
769 dirItem = dirItemForUrl(url);
770 if (dirItem && !dirItem->rootItem.isNull() && dirItem->rootItem.url() == url) {
771 // If lister is set, check that it contains this dir
772 if (!lister || lister->d->lstDirs.contains(url)) {
773 return dirItem->rootItem;
774 }
775 }
776
777 return KFileItem();
778}
779
780void KCoreDirListerCache::slotFilesAdded(const QString &dir /*url*/) // from KDirNotify signals
781{
782 QUrl urlDir(dir);
783 itemsAddedInDirectory(urlDir);
784}
785
786void KCoreDirListerCache::itemsAddedInDirectory(const QUrl &urlDir)
787{
788 qCDebug(KIO_CORE_DIRLISTER) << urlDir;
789 const QList<QUrl> urls = directoriesForCanonicalPath(urlDir);
790 for (const QUrl &u : urls) {
791 updateDirectory(u);
792 }
793}
794
795void KCoreDirListerCache::slotFilesRemoved(const QStringList &fileList) // from KDirNotify signals
796{
797 slotFilesRemoved(QUrl::fromStringList(fileList));
798}
799
800void KCoreDirListerCache::slotFilesRemoved(const QList<QUrl> &fileList)
801{
802 qCDebug(KIO_CORE_DIRLISTER) << fileList.count();
803 // Group notifications by parent dirs (usually there would be only one parent dir)
804 QMap<QUrl, KFileItemList> removedItemsByDir;
805 QList<QUrl> deletedSubdirs;
806
807 for (const QUrl &url : fileList) {
808 const QList<QUrl> dirUrls = directoriesForCanonicalPath(url);
809 for (const QUrl &dir : dirUrls) {
810 DirItem *dirItem = dirItemForUrl(dir); // is it a listed directory?
811 if (dirItem) {
812 deletedSubdirs.append(dir);
813 if (!dirItem->rootItem.isNull()) {
814 removedItemsByDir[url].append(dirItem->rootItem);
815 }
816 }
817 }
818
820 const QList<QUrl> parentDirUrls = directoriesForCanonicalPath(parentDir);
821 for (const QUrl &dir : parentDirUrls) {
822 DirItem *dirItem = dirItemForUrl(dir);
823 if (!dirItem) {
824 continue;
825 }
826
827 const auto dirItemIt = std::find_if(dirItem->lstItems.cbegin(), dirItem->lstItems.cend(), [&url](const KFileItem &fitem) {
828 return fitem.name() == url.fileName();
829 });
830 if (dirItemIt != dirItem->lstItems.cend()) {
831 const KFileItem fileitem = *dirItemIt;
832 removedItemsByDir[dir].append(fileitem);
833 // If we found a fileitem, we can test if it's a dir. If not, we'll go to deleteDir just in case.
834 if (fileitem.isNull() || fileitem.isDir()) {
835 deletedSubdirs.append(url);
836 }
837 dirItem->lstItems.erase(dirItemIt); // remove fileitem from list
838 }
839 }
840 }
841
842 for (auto rit = removedItemsByDir.constBegin(), cend = removedItemsByDir.constEnd(); rit != cend; ++rit) {
843 // Tell the views about it before calling deleteDir.
844 // They might need the subdirs' file items (see the dirtree).
845 auto dit = directoryData.constFind(rit.key());
846 if (dit != directoryData.constEnd()) {
847 itemsDeleted((*dit).listersByStatus(ListerStatus::Holding), rit.value());
848 }
849 }
850
851 for (const QUrl &url : std::as_const(deletedSubdirs)) {
852 // in case of a dir, check if we have any known children, there's much to do in that case
853 // (stopping jobs, removing dirs from cache etc.)
854 deleteDir(url);
855 }
856}
857
858void KCoreDirListerCache::slotFilesChanged(const QStringList &fileList) // from KDirNotify signals
859{
860 qCDebug(KIO_CORE_DIRLISTER) << fileList;
861 QList<QUrl> dirsToUpdate;
862 for (const QString &fileUrl : fileList) {
863 const QUrl url(fileUrl);
864 const KFileItem &fileitem = findByUrl(nullptr, url);
865 if (fileitem.isNull()) {
866 qCDebug(KIO_CORE_DIRLISTER) << "item not found for" << url;
867 continue;
868 }
869 if (url.isLocalFile()) {
870 pendingUpdates.insert(url.toLocalFile()); // delegate the work to processPendingUpdates
871 } else {
872 pendingRemoteUpdates.insert(fileitem);
873 // For remote files, we won't be able to figure out the new information,
874 // we have to do a update (directory listing)
876 if (!dirsToUpdate.contains(dir)) {
877 dirsToUpdate.prepend(dir);
878 }
879 }
880 }
881
882 for (const QUrl &dirUrl : std::as_const(dirsToUpdate)) {
883 updateDirectory(dirUrl);
884 }
885 // ## TODO problems with current jobs listing/updating that dir
886 // ( see kde-2.2.2's kdirlister )
887
888 processPendingUpdates();
889}
890
891void KCoreDirListerCache::slotFileRenamed(const QString &_src, const QString &_dst, const QString &dstPath) // from KDirNotify signals
892{
893 QUrl src(_src);
894 QUrl dst(_dst);
895 qCDebug(KIO_CORE_DIRLISTER) << src << "->" << dst;
896#ifdef DEBUG_CACHE
897 printDebug();
898#endif
899
901 KFileItem fileitem = findByUrl(nullptr, oldurl);
902 if (fileitem.isNull()) {
903 qCDebug(KIO_CORE_DIRLISTER) << "Item not found:" << oldurl;
904 return;
905 }
906
907 const KFileItem oldItem = fileitem;
908
909 // Dest already exists? Was overwritten then (testcase: #151851)
910 // We better emit it as deleted -before- doing the renaming, otherwise
911 // the "update" mechanism will emit the old one as deleted and
912 // kdirmodel will delete the new (renamed) one!
913 const KFileItem &existingDestItem = findByUrl(nullptr, dst);
914 if (!existingDestItem.isNull()) {
915 qCDebug(KIO_CORE_DIRLISTER) << dst << "already existed, let's delete it";
916 slotFilesRemoved(QList<QUrl>{dst});
917 }
918
919 // If the item had a UDS_URL as well as UDS_NAME set, the user probably wants
920 // to be updating the name only (since they can't see the URL).
921 // Check to see if a URL exists, and if so, if only the file part has changed,
922 // only update the name and not the underlying URL.
923 bool nameOnly = !fileitem.entry().stringValue(KIO::UDSEntry::UDS_URL).isEmpty();
924 nameOnly = nameOnly && src.adjusted(QUrl::RemoveFilename) == dst.adjusted(QUrl::RemoveFilename);
925
926 if (!nameOnly && fileitem.isDir()) {
927 renameDir(oldurl, dst);
928 // #172945 - if the fileitem was the root item of a DirItem that was just removed from the cache,
929 // then it's a dangling pointer now...
930 fileitem = findByUrl(nullptr, oldurl);
931 if (fileitem.isNull()) { // deleted from cache altogether, #188807
932 return;
933 }
934 }
935
936 // Now update the KFileItem representing that file or dir (not exclusive with the above!)
937 if (!oldItem.isLocalFile() && !oldItem.localPath().isEmpty()
938 && dstPath.isEmpty()) { // it uses UDS_LOCAL_PATH and we don't know the new path? needs an update then
939 slotFilesChanged(QStringList{src.toString()});
940 } else {
941 const QUrl &itemOldUrl = fileitem.url();
942 if (nameOnly) {
943 fileitem.setName(dst.fileName());
944 } else {
945 fileitem.setUrl(dst);
946 }
947
948 if (!dstPath.isEmpty()) {
949 fileitem.setLocalPath(dstPath);
950 }
951
952 fileitem.refreshMimeType();
953 fileitem.determineMimeType();
954 reinsert(fileitem, itemOldUrl);
955
956 const std::set<KCoreDirLister *> listers = emitRefreshItem(oldItem, fileitem);
957 for (KCoreDirLister *lister : listers) {
958 lister->d->emitItems();
959 }
960 }
961
962#ifdef DEBUG_CACHE
963 printDebug();
964#endif
965}
966
967std::set<KCoreDirLister *> KCoreDirListerCache::emitRefreshItem(const KFileItem &oldItem, const KFileItem &fileitem)
968{
969 qCDebug(KIO_CORE_DIRLISTER) << "old:" << oldItem.name() << oldItem.url() << "new:" << fileitem.name() << fileitem.url();
970 // Look whether this item was shown in any view, i.e. held by any dirlister
971 const QUrl parentDir = oldItem.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
972 DirectoryDataHash::iterator dit = directoryData.find(parentDir);
973 std::set<KCoreDirLister *> listers;
974 if (dit != directoryData.end()) {
975 for (auto *lister : (*dit).allListers()) {
976 listers.insert(lister);
977 }
978 }
979 if (oldItem.isDir()) {
980 // For a directory, look for dirlisters where it's the root item.
981 dit = directoryData.find(oldItem.url());
982 if (dit != directoryData.end()) {
983 for (auto *lister : (*dit).allListers()) {
984 listers.insert(lister);
985 }
986 }
987 }
988
989 for (KCoreDirLister *lister : std::as_const(listers)) {
990 // For a directory, look for dirlisters where it's the root item.
991 QUrl directoryUrl(oldItem.url());
992 if (oldItem.isDir() && lister->d->rootFileItem == oldItem) {
993 const KFileItem oldRootItem = lister->d->rootFileItem;
994 lister->d->rootFileItem = fileitem;
995 lister->d->addRefreshItem(directoryUrl, oldRootItem, fileitem);
996 } else {
997 directoryUrl = directoryUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
998 lister->d->addRefreshItem(directoryUrl, oldItem, fileitem);
999 }
1000 }
1001 return listers;
1002}
1003
1004QList<QUrl> KCoreDirListerCache::directoriesForCanonicalPath(const QUrl &dir) const
1005{
1006 QList<QUrl> urlList = canonicalUrls.value(dir);
1007 // make unique
1008 if (urlList.size() > 1) {
1009 std::sort(urlList.begin(), urlList.end());
1010 auto end_unique = std::unique(urlList.begin(), urlList.end());
1011 urlList.erase(end_unique, urlList.end());
1012 }
1013
1014 QList<QUrl> dirs({dir});
1015 dirs.append(urlList);
1016
1017 if (dirs.count() > 1) {
1018 qCDebug(KIO_CORE_DIRLISTER) << dir << "known as" << dirs;
1019 }
1020 return dirs;
1021}
1022
1023// private slots
1024
1025// Called by KDirWatch - usually when a dir we're watching has been modified,
1026// but it can also be called for a file.
1027void KCoreDirListerCache::slotFileDirty(const QString &path)
1028{
1029 qCDebug(KIO_CORE_DIRLISTER) << path;
1031 // File or dir?
1032 bool isDir;
1033 const KFileItem item = itemForUrl(url);
1034
1035 if (!item.isNull()) {
1036 isDir = item.isDir();
1037 } else {
1038 QFileInfo info(path);
1039 if (!info.exists()) {
1040 return; // error
1041 }
1042 isDir = info.isDir();
1043 }
1044
1045 if (isDir) {
1046 const QList<QUrl> urls = directoriesForCanonicalPath(url);
1047 for (const QUrl &dir : urls) {
1048 handleDirDirty(dir);
1049 }
1050 }
1051 // Also do this for dirs, e.g. to handle permission changes
1052 const QList<QUrl> urls = directoriesForCanonicalPath(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1053 for (const QUrl &dir : urls) {
1054 QUrl aliasUrl(dir);
1055 aliasUrl.setPath(Utils::concatPaths(aliasUrl.path(), url.fileName()));
1056 handleFileDirty(aliasUrl);
1057 }
1058}
1059
1060// Called by slotFileDirty
1061void KCoreDirListerCache::handleDirDirty(const QUrl &url)
1062{
1063 // A dir: launch an update job if anyone cares about it
1064
1065 // This also means we can forget about pending updates to individual files in that dir
1066 const QString dir = url.toLocalFile();
1067 const QString dirPath = Utils::slashAppended(dir);
1068
1069 for (auto pendingIt = pendingUpdates.cbegin(); pendingIt != pendingUpdates.cend(); /* */) {
1070 const QString updPath = *pendingIt;
1071 qCDebug(KIO_CORE_DIRLISTER) << "had pending update" << updPath;
1072 if (updPath.startsWith(dirPath) && updPath.indexOf(QLatin1Char('/'), dirPath.length()) == -1) { // direct child item
1073 qCDebug(KIO_CORE_DIRLISTER) << "forgetting about individual update to" << updPath;
1074 pendingIt = pendingUpdates.erase(pendingIt);
1075 } else {
1076 ++pendingIt;
1077 }
1078 }
1079
1080 if (checkUpdate(url)) {
1081 const auto [it, isInserted] = pendingDirectoryUpdates.insert(dir);
1082 if (isInserted && !pendingUpdateTimer.isActive()) {
1083 pendingUpdateTimer.start(200);
1084 }
1085 }
1086}
1087
1088// Called by slotFileDirty, for every alias of <url>
1089void KCoreDirListerCache::handleFileDirty(const QUrl &url)
1090{
1091 // A file: do we know about it already?
1092 const KFileItem &existingItem = findByUrl(nullptr, url);
1094 if (existingItem.isNull()) {
1095 // No - update the parent dir then
1096 handleDirDirty(dir);
1097 }
1098
1099 // Delay updating the file, FAM is flooding us with events
1100 if (checkUpdate(dir)) {
1101 const QString filePath = url.toLocalFile();
1102 const auto [it, isInserted] = pendingUpdates.insert(filePath);
1103 if (isInserted && !pendingUpdateTimer.isActive()) {
1104 pendingUpdateTimer.start(200);
1105 }
1106 }
1107}
1108
1109void KCoreDirListerCache::slotFileCreated(const QString &path) // from KDirWatch
1110{
1111 qCDebug(KIO_CORE_DIRLISTER) << path;
1112 // XXX: how to avoid a complete rescan here?
1113 // We'd need to stat that one file separately and refresh the item(s) for it.
1114 QUrl fileUrl(QUrl::fromLocalFile(path));
1115 itemsAddedInDirectory(fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1116}
1117
1118void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch
1119{
1120 qCDebug(KIO_CORE_DIRLISTER) << path;
1121 const QString fileName = QFileInfo(path).fileName();
1122 QUrl dirUrl(QUrl::fromLocalFile(path));
1123 QStringList fileUrls;
1124 const QList<QUrl> urls = directoriesForCanonicalPath(dirUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1125 for (const QUrl &url : urls) {
1126 QUrl urlInfo(url);
1127 urlInfo.setPath(Utils::concatPaths(urlInfo.path(), fileName));
1128 fileUrls << urlInfo.toString();
1129 }
1130 slotFilesRemoved(fileUrls);
1131}
1132
1133void KCoreDirListerCache::slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries)
1134{
1135 QUrl url(joburl(static_cast<KIO::ListJob *>(job)));
1137
1138 qCDebug(KIO_CORE_DIRLISTER) << "new entries for " << url;
1139
1140 DirItem *dir = itemsInUse.value(url);
1141 if (!dir) {
1142 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but itemsInUse only knows about" << itemsInUse.keys();
1143 Q_ASSERT(dir);
1144 return;
1145 }
1146
1147 DirectoryDataHash::iterator dit = directoryData.find(url);
1148 if (dit == directoryData.end()) {
1149 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData doesn't know about that url, only about:" << directoryData.keys();
1150 Q_ASSERT(dit != directoryData.end());
1151 return;
1152 }
1153 KCoreDirListerCacheDirectoryData &dirData = *dit;
1154 const QList<KCoreDirLister *> listers = dirData.listersByStatus(ListerStatus::Listing);
1155 if (listers.isEmpty()) {
1156 qCWarning(KIO_CORE) << "Internal error: job is listing" << url << "but directoryData says no listers are currently listing " << url;
1157#ifndef NDEBUG
1158 printDebug();
1159#endif
1160 Q_ASSERT(!listers.isEmpty());
1161 return;
1162 }
1163
1164 // check if anyone wants the MIME types immediately
1165 bool delayedMimeTypes = true;
1166 for (const KCoreDirLister *lister : listers) {
1167 delayedMimeTypes &= lister->d->delayedMimeTypes;
1168 }
1169
1170 CacheHiddenFile *cachedHidden = nullptr;
1171 bool dotHiddenChecked = false;
1172 KFileItemList newItems;
1173 for (const auto &entry : entries) {
1174 const QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1175
1176 Q_ASSERT(!name.isEmpty());
1177 if (name.isEmpty()) {
1178 continue;
1179 }
1180
1181 if (name == QLatin1Char('.')) {
1182 // Try to reuse an existing KFileItem (if we listed the parent dir)
1183 // rather than creating a new one. There are many reasons:
1184 // 1) renames and permission changes to the item would have to emit the signals
1185 // twice, otherwise, so that both views manage to recognize the item.
1186 // 2) with kio_ftp we can only know that something is a symlink when
1187 // listing the parent, so prefer that item, which has more info.
1188 // Note that it gives a funky name() to the root item, rather than "." ;)
1189 dir->rootItem = itemForUrl(url);
1190 if (dir->rootItem.isNull()) {
1191 dir->rootItem = KFileItem(entry, url, delayedMimeTypes, true);
1192 }
1193
1194 for (KCoreDirLister *lister : listers) {
1195 if (lister->d->rootFileItem.isNull() && lister->d->url == url) {
1196 lister->d->rootFileItem = dir->rootItem;
1197 }
1198 }
1199 } else if (name != QLatin1String("..")) {
1200 KFileItem item(entry, url, delayedMimeTypes, true);
1201
1202 // get the names of the files listed in ".hidden", if it exists and is a local file
1203 if (!dotHiddenChecked) {
1204 const QString localPath = item.localPath();
1205 if (!localPath.isEmpty()) {
1206 const QString rootItemPath = QFileInfo(localPath).absolutePath();
1207 cachedHidden = cachedDotHiddenForDir(rootItemPath);
1208 }
1209 dotHiddenChecked = true;
1210 }
1211
1212 // hide file if listed in ".hidden"
1213 if (cachedHidden && cachedHidden->listedFiles.find(name) != cachedHidden->listedFiles.cend()) {
1214 item.setHidden();
1215 }
1216
1217 qCDebug(KIO_CORE_DIRLISTER) << "Adding item: " << item.url();
1218 newItems.append(item);
1219 }
1220 }
1221
1222 // sort by url using KFileItem::operator<
1223 std::sort(newItems.begin(), newItems.end());
1224
1225 // Add the items sorted by url, needed by findByUrl
1226 dir->insertSortedItems(newItems);
1227
1228 for (KCoreDirLister *lister : listers) {
1229 lister->d->addNewItems(url, newItems);
1230 }
1231
1232 for (KCoreDirLister *lister : listers) {
1233 lister->d->emitItems();
1234 }
1235}
1236
1237void KCoreDirListerCache::slotResult(KJob *j)
1238{
1239#ifdef DEBUG_CACHE
1240 // printDebug();
1241#endif
1242
1243 Q_ASSERT(j);
1244 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1245 runningListJobs.remove(job);
1246
1247 QUrl jobUrl(joburl(job));
1248 jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
1249
1250 qCDebug(KIO_CORE_DIRLISTER) << "finished listing" << jobUrl;
1251
1252 const auto dit = directoryData.find(jobUrl);
1253 if (dit == directoryData.end()) {
1254 qCWarning(KIO_CORE) << "Nothing found in directoryData for URL" << jobUrl;
1255#ifndef NDEBUG
1256 printDebug();
1257#endif
1258 Q_ASSERT(dit != directoryData.end());
1259 return;
1260 }
1261 KCoreDirListerCacheDirectoryData &dirData = *dit;
1262 if (dirData.listerCountByStatus(ListerStatus::Listing) == 0) {
1263 qCWarning(KIO_CORE) << "OOOOPS, nothing in directoryData.listersCurrentlyListing for" << jobUrl;
1264 // We're about to assert; dump the current state...
1265#ifndef NDEBUG
1266 printDebug();
1267#endif
1268 Q_ASSERT(dirData.listerCountByStatus(ListerStatus::Listing) != 0);
1269 }
1270 const QList<KCoreDirLister *> listers = dirData.listersByStatus(ListerStatus::Listing);
1271
1272 // move all listers to the holding list, do it before emitting
1273 // the signals to make sure it exists in KCoreDirListerCache in case someone
1274 // calls listDir during the signal emission
1275 Q_ASSERT(dirData.listerCountByStatus(ListerStatus::Holding) == 0);
1276 dirData.moveListersWithoutCachedItemsJob(jobUrl);
1277
1278 if (job->error()) {
1279 bool errorShown = false;
1280 for (KCoreDirLister *lister : listers) {
1281 lister->d->jobDone(job);
1282 if (job->error() != KJob::KilledJobError) {
1283 Q_EMIT lister->jobError(job);
1284 if (lister->d->m_autoErrorHandling && !errorShown) {
1285 errorShown = true; // do it only once
1286 if (job->uiDelegate()) {
1287 job->uiDelegate()->showErrorMessage();
1288 }
1289 }
1290 }
1291 const bool silent = job->property("_kdlc_silent").toBool();
1292 if (!silent) {
1293 Q_EMIT lister->listingDirCanceled(jobUrl);
1294 }
1295
1296 if (lister->d->numJobs() == 0) {
1297 lister->d->complete = true;
1298 if (!silent) {
1299 Q_EMIT lister->canceled();
1300 }
1301 }
1302 }
1303 } else {
1304 DirItem *dir = itemsInUse.value(jobUrl);
1305 Q_ASSERT(dir);
1306 dir->complete = true;
1307
1308 for (KCoreDirLister *lister : listers) {
1309 lister->d->jobDone(job);
1310 Q_EMIT lister->listingDirCompleted(jobUrl);
1311 if (lister->d->numJobs() == 0) {
1312 lister->d->complete = true;
1313 Q_EMIT lister->completed();
1314 }
1315 }
1316 }
1317
1318 // TODO: hmm, if there was an error and job is a parent of one or more
1319 // of the pending urls we should cancel it/them as well
1320 processPendingUpdates();
1321
1322 if (job->property("need_another_update").toBool()) {
1323 updateDirectory(jobUrl);
1324 }
1325
1326#ifdef DEBUG_CACHE
1327 printDebug();
1328#endif
1329}
1330
1331void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url)
1332{
1333 Q_ASSERT(j);
1334 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1335
1336 QUrl oldUrl(job->url()); // here we really need the old url!
1337 QUrl newUrl(url);
1338
1339 // strip trailing slashes
1340 oldUrl = oldUrl.adjusted(QUrl::StripTrailingSlash);
1341 newUrl = newUrl.adjusted(QUrl::StripTrailingSlash);
1342
1343 if (oldUrl == newUrl) {
1344 qCDebug(KIO_CORE_DIRLISTER) << "New redirection url same as old, giving up.";
1345 return;
1346 } else if (newUrl.isEmpty()) {
1347 qCDebug(KIO_CORE_DIRLISTER) << "New redirection url is empty, giving up.";
1348 return;
1349 }
1350
1351 qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl;
1352
1353#ifdef DEBUG_CACHE
1354 // Can't do that here. KCoreDirListerCache::joburl() will use the new url already,
1355 // while our data structures haven't been updated yet -> assert fail.
1356 // printDebug();
1357#endif
1358
1359 // I don't think there can be dirItems that are children of oldUrl.
1360 // Am I wrong here? And even if so, we don't need to delete them, right?
1361 // DF: redirection happens before listDir emits any item. Makes little sense otherwise.
1362
1363 // oldUrl cannot be in itemsCached because only completed items are moved there
1364 DirItem *dir = itemsInUse.take(oldUrl);
1365 Q_ASSERT(dir);
1366
1367 DirectoryDataHash::iterator dit = directoryData.find(oldUrl);
1368 Q_ASSERT(dit != directoryData.end());
1369 KCoreDirListerCacheDirectoryData oldDirData = *dit;
1370 directoryData.erase(dit);
1371 Q_ASSERT(oldDirData.listerCountByStatus(ListerStatus::Listing) != 0);
1372 const QList<KCoreDirLister *> listers = oldDirData.listersByStatus(ListerStatus::Listing);
1373 Q_ASSERT(!listers.isEmpty());
1374
1375 for (KCoreDirLister *lister : listers) {
1376 lister->d->redirect(oldUrl, newUrl, false /*clear items*/);
1377 }
1378
1379 // when a lister was stopped before the job emits the redirection signal, the old url will
1380 // also be in listersCurrentlyHolding
1381 const QList<KCoreDirLister *> holders = oldDirData.listersByStatus(ListerStatus::Holding);
1382 for (KCoreDirLister *holder : holders) {
1383 holder->jobStarted(job);
1384 // do it like when starting a new list-job that will redirect later
1385 // TODO: maybe don't emit started if there's an update running for newUrl already?
1386 Q_EMIT holder->started(oldUrl);
1387
1388 holder->d->redirect(oldUrl, newUrl, false /*clear items*/);
1389 }
1390
1391 const QList<KCoreDirLister *> allListers = listers + holders;
1392
1393 DirItem *newDir = itemsInUse.value(newUrl);
1394 if (newDir) {
1395 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "already in use";
1396
1397 // only in this case there can newUrl already be in listersCurrentlyListing or listersCurrentlyHolding
1398 delete dir;
1399
1400 // get the job if one's running for newUrl already (can be a list-job or an update-job), but
1401 // do not return this 'job', which would happen because of the use of redirectionURL()
1402 KIO::ListJob *oldJob = jobForUrl(newUrl, job);
1403
1404 // listers of newUrl with oldJob: forget about the oldJob and use the already running one
1405 // which will be converted to an updateJob
1406 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1407
1408 QList<KCoreDirLister *> curListers = newDirData.listersByStatus(ListerStatus::Listing);
1409 if (!curListers.isEmpty()) {
1410 qCDebug(KIO_CORE_DIRLISTER) << "and it is currently listed";
1411
1412 Q_ASSERT(oldJob); // ?!
1413
1414 for (KCoreDirLister *lister : std::as_const(curListers)) { // listers of newUrl
1415 lister->d->jobDone(oldJob);
1416
1417 lister->jobStarted(job);
1418 lister->d->connectJob(job);
1419 }
1420
1421 // append listers of oldUrl with newJob to listers of newUrl with oldJob
1422 for (KCoreDirLister *lister : listers) {
1423 curListers.append(lister);
1424 }
1425 } else {
1426 curListers = listers;
1427 }
1428
1429 if (oldJob) { // kill the old job, be it a list-job or an update-job
1430 killJob(oldJob);
1431 }
1432
1433 // holders of newUrl: use the already running job which will be converted to an updateJob
1434 QList<KCoreDirLister *> curHolders = newDirData.listersByStatus(ListerStatus::Holding);
1435 if (!curHolders.isEmpty()) {
1436 qCDebug(KIO_CORE_DIRLISTER) << "and it is currently held.";
1437
1438 for (KCoreDirLister *holder : std::as_const(curHolders)) { // holders of newUrl
1439 holder->jobStarted(job);
1440 Q_EMIT holder->started(newUrl);
1441 }
1442
1443 // append holders of oldUrl to holders of newUrl
1444 for (KCoreDirLister *holder : holders) {
1445 curHolders.append(holder);
1446 }
1447 } else {
1448 curHolders = holders;
1449 }
1450
1451 // emit old items: listers, holders. NOT: newUrlListers/newUrlHolders, they already have them listed
1452 // TODO: make this a separate method?
1453 for (KCoreDirLister *lister : allListers) {
1454 if (lister->d->rootFileItem.isNull() && lister->d->url == newUrl) {
1455 lister->d->rootFileItem = newDir->rootItem;
1456 }
1457
1458 lister->d->addNewItems(newUrl, newDir->lstItems);
1459 lister->d->emitItems();
1460 }
1461 } else if ((newDir = itemsCached.take(newUrl))) {
1462 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "is unused, but already in the cache.";
1463
1464 delete dir;
1465 itemsInUse.insert(newUrl, newDir);
1466 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1467 newDirData.insertOrModifyListers(listers, ListerStatus::Listing);
1468 newDirData.insertOrModifyListers(holders, ListerStatus::Holding);
1469
1470 // emit old items: listers, holders
1471 for (KCoreDirLister *lister : allListers) {
1472 if (lister->d->rootFileItem.isNull() && lister->d->url == newUrl) {
1473 lister->d->rootFileItem = newDir->rootItem;
1474 }
1475
1476 lister->d->addNewItems(newUrl, newDir->lstItems);
1477 lister->d->emitItems();
1478 }
1479 } else {
1480 qCDebug(KIO_CORE_DIRLISTER) << newUrl << "has not been listed yet.";
1481
1482 dir->rootItem = KFileItem();
1483 dir->lstItems.clear();
1484 dir->redirect(newUrl);
1485 itemsInUse.insert(newUrl, dir);
1486 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1487 newDirData.insertOrModifyListers(listers, ListerStatus::Listing);
1488 newDirData.insertOrModifyListers(holders, ListerStatus::Holding);
1489
1490 if (holders.isEmpty()) {
1491#ifdef DEBUG_CACHE
1492 printDebug();
1493#endif
1494 return; // only in this case the job doesn't need to be converted,
1495 }
1496 }
1497
1498 // make the job an update job
1499 job->disconnect(this);
1500
1501 connect(job, &KIO::ListJob::entries, this, &KCoreDirListerCache::slotUpdateEntries);
1502 connect(job, &KJob::result, this, &KCoreDirListerCache::slotUpdateResult);
1503
1504 // FIXME: autoUpdate-Counts!!
1505
1506#ifdef DEBUG_CACHE
1507 printDebug();
1508#endif
1509}
1510
1511struct KCoreDirListerCache::ItemInUseChange {
1512 ItemInUseChange(const QUrl &old, const QUrl &newU, DirItem *di)
1513 : oldUrl(old)
1514 , newUrl(newU)
1515 , dirItem(di)
1516 {
1517 }
1518 QUrl oldUrl;
1519 QUrl newUrl;
1520 DirItem *dirItem;
1521};
1522
1523void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl)
1524{
1525 qCDebug(KIO_CORE_DIRLISTER) << oldUrl << "->" << newUrl;
1526
1527 std::vector<ItemInUseChange> itemsToChange;
1528 std::set<KCoreDirLister *> listers;
1529
1530 // Look at all dirs being listed/shown
1531 for (auto itu = itemsInUse.begin(), ituend = itemsInUse.end(); itu != ituend; ++itu) {
1532 DirItem *dir = itu.value();
1533 const QUrl &oldDirUrl = itu.key();
1534 qCDebug(KIO_CORE_DIRLISTER) << "itemInUse:" << oldDirUrl;
1535 // Check if this dir is oldUrl, or a subfolder of it
1536 if (oldDirUrl == oldUrl || oldUrl.isParentOf(oldDirUrl)) {
1537 // TODO should use KUrl::cleanpath like isParentOf does
1538 QString relPath = oldDirUrl.path().mid(oldUrl.path().length() + 1);
1539
1540 QUrl newDirUrl(newUrl); // take new base
1541 if (!relPath.isEmpty()) {
1542 newDirUrl.setPath(Utils::concatPaths(newDirUrl.path(), relPath)); // add unchanged relative path
1543 }
1544 qCDebug(KIO_CORE_DIRLISTER) << "new url=" << newDirUrl;
1545
1546 // Update URL in dir item and in itemsInUse
1547 dir->redirect(newDirUrl);
1548
1549 itemsToChange.emplace_back(oldDirUrl.adjusted(QUrl::StripTrailingSlash), newDirUrl.adjusted(QUrl::StripTrailingSlash), dir);
1550 // Rename all items under that dir
1551 // If all items of the directory change the same part of their url, the order is not
1552 // changed, therefore just change it in the list.
1553 for (KFileItem &item : dir->lstItems) {
1554 const KFileItem oldItem = item;
1555 KFileItem newItem = oldItem;
1556 const QUrl &oldItemUrl = oldItem.url();
1557 QUrl newItemUrl(oldItemUrl);
1558 newItemUrl.setPath(Utils::concatPaths(newDirUrl.path(), oldItemUrl.fileName()));
1559 qCDebug(KIO_CORE_DIRLISTER) << "renaming" << oldItemUrl << "to" << newItemUrl;
1560 newItem.setUrl(newItemUrl);
1561
1562 listers.merge(emitRefreshItem(oldItem, newItem));
1563 // Change the item
1564 item.setUrl(newItemUrl);
1565 }
1566 }
1567 }
1568
1569 for (KCoreDirLister *lister : listers) {
1570 lister->d->emitItems();
1571 }
1572
1573 // Do the changes to itemsInUse out of the loop to avoid messing up iterators,
1574 // and so that emitRefreshItem can find the stuff in the hash.
1575 for (const ItemInUseChange &i : itemsToChange) {
1576 itemsInUse.remove(i.oldUrl);
1577 itemsInUse.insert(i.newUrl, i.dirItem);
1578 }
1579 // Now that all the caches are updated and consistent, emit the redirection.
1580 for (const ItemInUseChange &i : itemsToChange) {
1581 emitRedirections(QUrl(i.oldUrl), QUrl(i.newUrl));
1582 }
1583 // Is oldUrl a directory in the cache?
1584 // Remove any child of oldUrl from the cache - even if the renamed dir itself isn't in it!
1585 removeDirFromCache(oldUrl);
1586 // TODO rename, instead.
1587}
1588
1589// helper for renameDir, not used for redirections from KIO::listDir().
1590void KCoreDirListerCache::emitRedirections(const QUrl &_oldUrl, const QUrl &_newUrl)
1591{
1592 qCDebug(KIO_CORE_DIRLISTER) << _oldUrl << "->" << _newUrl;
1593 const QUrl oldUrl = _oldUrl.adjusted(QUrl::StripTrailingSlash);
1594 const QUrl newUrl = _newUrl.adjusted(QUrl::StripTrailingSlash);
1595
1596 KIO::ListJob *job = jobForUrl(oldUrl);
1597 if (job) {
1598 killJob(job);
1599 }
1600
1601 // Check if we were listing this dir. Need to abort and restart with new name in that case.
1602 DirectoryDataHash::iterator dit = directoryData.find(oldUrl);
1603 if (dit == directoryData.end()) {
1604 return;
1605 }
1606 const QList<KCoreDirLister *> listers = (*dit).listersByStatus(ListerStatus::Listing);
1607 const QList<KCoreDirLister *> holders = (*dit).listersByStatus(ListerStatus::Holding);
1608
1609 KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
1610
1611 // Tell the world that the job listing the old url is dead.
1612 for (KCoreDirLister *lister : listers) {
1613 if (job) {
1614 lister->d->jobDone(job);
1615 }
1616 Q_EMIT lister->listingDirCanceled(oldUrl);
1617 }
1618 newDirData.insertOrModifyListers(listers, ListerStatus::Listing);
1619
1620 // Check if we are currently displaying this directory (odds opposite wrt above)
1621 for (KCoreDirLister *holder : holders) {
1622 if (job) {
1623 holder->d->jobDone(job);
1624 }
1625 }
1626 newDirData.insertOrModifyListers(holders, ListerStatus::Holding);
1627
1628 directoryData.erase(dit);
1629
1630 if (!listers.isEmpty()) {
1631 updateDirectory(newUrl);
1632
1633 // Tell the world about the new url
1634 for (KCoreDirLister *lister : listers) {
1635 Q_EMIT lister->started(newUrl);
1636 }
1637 }
1638
1639 // And notify the dirlisters of the redirection
1640 for (KCoreDirLister *holder : holders) {
1641 holder->d->redirect(oldUrl, newUrl, true /*keep items*/);
1642 }
1643}
1644
1645void KCoreDirListerCache::removeDirFromCache(const QUrl &dir)
1646{
1647 qCDebug(KIO_CORE_DIRLISTER) << dir;
1648 const QList<QUrl> cachedDirs = itemsCached.keys(); // seems slow, but there's no qcache iterator...
1649 for (const QUrl &cachedDir : cachedDirs) {
1650 if (dir == cachedDir || dir.isParentOf(cachedDir)) {
1651 itemsCached.remove(cachedDir);
1652 }
1653 }
1654}
1655
1656void KCoreDirListerCache::slotUpdateEntries(KIO::Job *job, const KIO::UDSEntryList &list)
1657{
1658 runningListJobs[static_cast<KIO::ListJob *>(job)] += list;
1659}
1660
1661void KCoreDirListerCache::slotUpdateResult(KJob *j)
1662{
1663 Q_ASSERT(j);
1664 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1665
1666 QUrl jobUrl(joburl(job));
1667 jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
1668
1669 qCDebug(KIO_CORE_DIRLISTER) << "finished update" << jobUrl;
1670
1671 KCoreDirListerCacheDirectoryData &dirData = directoryData[jobUrl];
1672 // Collect the dirlisters which were listing the URL using that ListJob
1673 // plus those that were already holding that URL - they all get updated.
1674 dirData.moveListersWithoutCachedItemsJob(jobUrl);
1675 const QList<KCoreDirLister *> listers = dirData.allListers();
1676
1677 // once we are updating dirs that are only in the cache this will fail!
1678 Q_ASSERT(!listers.isEmpty());
1679
1680 if (job->error()) {
1681 for (KCoreDirLister *lister : listers) {
1682 lister->d->jobDone(job);
1683
1684 // don't bother the user: no jobError signal emitted
1685
1686 const bool silent = job->property("_kdlc_silent").toBool();
1687 if (!silent) {
1688 Q_EMIT lister->listingDirCanceled(jobUrl);
1689 }
1690 if (lister->d->numJobs() == 0) {
1691 lister->d->complete = true;
1692 if (!silent) {
1693 Q_EMIT lister->canceled();
1694 }
1695 }
1696 }
1697
1698 runningListJobs.remove(job);
1699
1700 // TODO: if job is a parent of one or more
1701 // of the pending urls we should cancel them
1702 processPendingUpdates();
1703 return;
1704 }
1705
1706 DirItem *dir = itemsInUse.value(jobUrl, nullptr);
1707 if (!dir) {
1708 qCWarning(KIO_CORE) << "Internal error: itemsInUse did not contain" << jobUrl;
1709#ifndef NDEBUG
1710 printDebug();
1711#endif
1712 Q_ASSERT(dir);
1713 } else {
1714 dir->complete = true;
1715 }
1716
1717 // check if anyone wants the MIME types immediately
1718 bool delayedMimeTypes = true;
1719 for (const KCoreDirLister *lister : listers) {
1720 delayedMimeTypes &= lister->d->delayedMimeTypes;
1721 }
1722
1723 typedef QHash<QString, KFileItem> FileItemHash; // fileName -> KFileItem
1724 FileItemHash fileItems;
1725
1726 // Fill the hash from the old list of items. We'll remove entries as we see them
1727 // in the new listing, and the resulting hash entries will be the deleted items.
1728 for (const KFileItem &item : std::as_const(dir->lstItems)) {
1729 fileItems.insert(item.name(), item);
1730 }
1731
1732 CacheHiddenFile *cachedHidden = nullptr;
1733 bool dotHiddenChecked = false;
1734 const KIO::UDSEntryList &buf = runningListJobs.value(job);
1735 KFileItemList newItems;
1736 for (const auto &entry : buf) {
1737 // Form the complete url
1738 KFileItem item(entry, jobUrl, delayedMimeTypes, true);
1739
1740 const QString name = item.name();
1741 Q_ASSERT(!name.isEmpty()); // A KIO worker setting an empty UDS_NAME is utterly broken, fix the KIO worker!
1742
1743 // we duplicate the check for dotdot here, to avoid iterating over
1744 // all items again and checking in matchesFilter() that way.
1745 if (name.isEmpty() || name == QLatin1String("..")) {
1746 continue;
1747 }
1748
1749 if (name == QLatin1Char('.')) {
1750 // if the update was started before finishing the original listing
1751 // there is no root item yet
1752 if (dir->rootItem.isNull()) {
1753 dir->rootItem = item;
1754
1755 for (KCoreDirLister *lister : listers) {
1756 if (lister->d->rootFileItem.isNull() && lister->d->url == jobUrl) {
1757 lister->d->rootFileItem = dir->rootItem;
1758 }
1759 }
1760 }
1761 continue;
1762 } else {
1763 // get the names of the files listed in ".hidden", if it exists and is a local file
1764 if (!dotHiddenChecked) {
1765 const QString localPath = item.localPath();
1766 if (!localPath.isEmpty()) {
1767 const QString rootItemPath = QFileInfo(localPath).absolutePath();
1768 cachedHidden = cachedDotHiddenForDir(rootItemPath);
1769 }
1770 dotHiddenChecked = true;
1771 }
1772 }
1773
1774 // hide file if listed in ".hidden"
1775 if (cachedHidden && cachedHidden->listedFiles.find(name) != cachedHidden->listedFiles.cend()) {
1776 item.setHidden();
1777 }
1778
1779 // Find this item
1780 FileItemHash::iterator fiit = fileItems.find(item.name());
1781 if (fiit != fileItems.end()) {
1782 const KFileItem tmp = fiit.value();
1783
1784 bool inPendingUpdates = false;
1785 bool inPendingRemoteUpdates = false;
1786
1787 std::set<QString>::iterator pu_it;
1788 std::set<KFileItem>::iterator pru_it;
1789 if (tmp.url().isLocalFile()) {
1790 pu_it = pendingUpdates.find(tmp.url().toLocalFile());
1791 inPendingUpdates = pu_it != pendingUpdates.end();
1792 } else {
1793 pru_it = pendingRemoteUpdates.find(tmp);
1794 inPendingRemoteUpdates = pru_it != pendingRemoteUpdates.end();
1795 }
1796
1797 // check if something changed for this file, using KFileItem::cmp()
1798 if (inPendingRemoteUpdates || inPendingUpdates || !tmp.cmp(item)) {
1799 if (inPendingRemoteUpdates) {
1800 pendingRemoteUpdates.erase(pru_it);
1801 }
1802 if (inPendingUpdates) {
1803 pendingUpdates.erase(pu_it);
1804 }
1805
1806 qCDebug(KIO_CORE_DIRLISTER) << "file changed:" << tmp.name();
1807
1808 reinsert(item, tmp.url());
1809 for (KCoreDirLister *lister : listers) {
1810 lister->d->addRefreshItem(jobUrl, tmp, item);
1811 }
1812 }
1813 // Seen, remove
1814 fileItems.erase(fiit);
1815 } else { // this is a new file
1816 qCDebug(KIO_CORE_DIRLISTER) << "new file:" << name;
1817 newItems.append(item);
1818 }
1819 }
1820
1821 // sort by url using KFileItem::operator<
1822 std::sort(newItems.begin(), newItems.end());
1823
1824 // Add the items sorted by url, needed by findByUrl
1825 dir->insertSortedItems(newItems);
1826
1827 for (KCoreDirLister *lister : listers) {
1828 lister->d->addNewItems(jobUrl, newItems);
1829 }
1830
1831 runningListJobs.remove(job);
1832
1833 if (!fileItems.isEmpty()) {
1834 deleteUnmarkedItems(listers, dir->lstItems, fileItems);
1835 }
1836
1837 for (KCoreDirLister *lister : listers) {
1838 lister->d->emitItems();
1839
1840 lister->d->jobDone(job);
1841 Q_EMIT lister->listingDirCompleted(jobUrl);
1842 if (lister->d->numJobs() == 0) {
1843 lister->d->complete = true;
1844 Q_EMIT lister->completed();
1845 }
1846 }
1847
1848 // TODO: hmm, if there was an error and job is a parent of one or more
1849 // of the pending urls we should cancel it/them as well
1850 processPendingUpdates();
1851
1852 if (job->property("need_another_update").toBool()) {
1853 updateDirectory(jobUrl);
1854 }
1855}
1856
1857// private
1858
1859KIO::ListJob *KCoreDirListerCache::jobForUrl(const QUrl &url, KIO::ListJob *not_job)
1860{
1861 for (auto it = runningListJobs.cbegin(); it != runningListJobs.cend(); ++it) {
1862 KIO::ListJob *job = it.key();
1863 const QUrl jobUrl = joburl(job).adjusted(QUrl::StripTrailingSlash);
1864
1865 if (jobUrl == url && job != not_job) {
1866 return job;
1867 }
1868 }
1869 return nullptr;
1870}
1871
1872const QUrl &KCoreDirListerCache::joburl(KIO::ListJob *job)
1873{
1874 if (job->redirectionUrl().isValid()) {
1875 return job->redirectionUrl();
1876 } else {
1877 return job->url();
1878 }
1879}
1880
1881void KCoreDirListerCache::killJob(KIO::ListJob *job)
1882{
1883 runningListJobs.remove(job);
1884 job->disconnect(this);
1885 job->kill();
1886}
1887
1888void KCoreDirListerCache::deleteUnmarkedItems(const QList<KCoreDirLister *> &listers,
1889 QList<KFileItem> &lstItems,
1890 const QHash<QString, KFileItem> &itemsToDelete)
1891{
1892 // Make list of deleted items (for emitting)
1893 KFileItemList deletedItems;
1894 deletedItems.reserve(itemsToDelete.size());
1895 for (auto kit = itemsToDelete.cbegin(), endIt = itemsToDelete.cend(); kit != endIt; ++kit) {
1896 const KFileItem item = kit.value();
1897 deletedItems.append(item);
1898 qCDebug(KIO_CORE_DIRLISTER) << "deleted:" << item.name() << item;
1899 }
1900
1901 // Delete all remaining items
1902 auto it = std::remove_if(lstItems.begin(), lstItems.end(), [&itemsToDelete](const KFileItem &item) {
1903 return itemsToDelete.contains(item.name());
1904 });
1905 lstItems.erase(it, lstItems.end());
1906
1907 itemsDeleted(listers, deletedItems);
1908}
1909
1910void KCoreDirListerCache::itemsDeleted(const QList<KCoreDirLister *> &listers, const KFileItemList &deletedItems)
1911{
1912 for (KCoreDirLister *lister : listers) {
1913 lister->d->emitItemsDeleted(deletedItems);
1914 }
1915
1916 for (const KFileItem &item : deletedItems) {
1917 if (item.isDir()) {
1918 deleteDir(item.url());
1919 }
1920 }
1921}
1922
1923void KCoreDirListerCache::deleteDir(const QUrl &_dirUrl)
1924{
1925 qCDebug(KIO_CORE_DIRLISTER) << _dirUrl;
1926 // unregister and remove the children of the deleted item.
1927 // Idea: tell all the KCoreDirListers that they should forget the dir
1928 // and then remove it from the cache.
1929
1930 QUrl dirUrl(_dirUrl.adjusted(QUrl::StripTrailingSlash));
1931
1932 // Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse)
1933 QList<QUrl> affectedItems;
1934
1935 auto itu = itemsInUse.cbegin();
1936 const auto ituend = itemsInUse.cend();
1937 for (; itu != ituend; ++itu) {
1938 const QUrl &deletedUrl = itu.key();
1939 if (dirUrl == deletedUrl || dirUrl.isParentOf(deletedUrl)) {
1940 affectedItems.append(deletedUrl);
1941 }
1942 }
1943
1944 const auto possibleMountPoints = KMountPoint::possibleMountPoints(KMountPoint::NeedMountOptions);
1945 for (const QUrl &deletedUrl : std::as_const(affectedItems)) {
1946 // stop all jobs for deletedUrlStr
1947 auto dit = directoryData.constFind(deletedUrl);
1948 if (dit != directoryData.cend()) {
1949 // we need a copy because stop modifies the list
1950 const QList<KCoreDirLister *> listers = (*dit).listersByStatus(ListerStatus::Listing);
1951 for (KCoreDirLister *lister : listers) {
1952 stopListingUrl(lister, 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).listersByStatus(ListerStatus::Holding);
1959 for (KCoreDirLister *holder : holders) {
1960 // lister's root is the deleted item
1961 if (holder->d->url == deletedUrl) {
1962 // tell the view first. It might need the subdirs' items (which forgetDirs will delete)
1963 if (!holder->d->rootFileItem.isNull()) {
1964 Q_EMIT holder->itemsDeleted(KFileItemList{holder->d->rootFileItem});
1965 }
1966 forgetDirs(holder);
1967 holder->d->rootFileItem = KFileItem();
1968 } else {
1969 const bool treeview = holder->d->lstDirs.count() > 1;
1970 if (!treeview) {
1971 Q_EMIT holder->clear();
1972 holder->d->lstDirs.clear();
1973 } else {
1974 holder->d->lstDirs.removeAll(deletedUrl);
1975 }
1976
1977 forgetDirs(holder, deletedUrl, treeview, possibleMountPoints);
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(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(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(file);
2001 KFileItem item = findByUrl(nullptr, 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(oldItem.url());
2010 } else {
2011 reinsert(item, oldItem.url());
2012 }
2013 listers.merge(emitRefreshItem(oldItem, item));
2014 }
2015 }
2016 }
2017 pendingUpdates.clear();
2018 for (KCoreDirLister *lister : listers) {
2019 lister->d->emitItems();
2020 }
2021
2022 // clean orphan KFileItem, after events were emitted
2023 for (const auto &removedUrl : removedUrls) {
2024 remove(removedUrl);
2025 }
2026
2027 // Directories in need of updating
2028 for (const QString &dir : pendingDirectoryUpdates) {
2029 updateDirectory(QUrl::fromLocalFile(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(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).listersByStatus(ListerStatus::Listing);
2053 for (KCoreDirLister *listit : listers) {
2054 list += QLatin1String(" 0x") + QString::number(reinterpret_cast<qlonglong>(listit), 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(dit.key())) {
2061 qCDebug(KIO_CORE_DIRLISTER) << " Lister" << listit << "has ListJob" << listJob;
2062 } else {
2063 listersWithoutJob.append(listit);
2064 }
2065 }
2066
2067 list.clear();
2068 const QList<KCoreDirLister *> holders = (*dit).listersByStatus(ListerStatus::Holding);
2069 for (KCoreDirLister *listit : holders) {
2070 list += QLatin1String(" 0x") + QString::number(reinterpret_cast<qlonglong>(listit), 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(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(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(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
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
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(this);
2119 }
2120}
2121
2122// TODO KF6: remove bool ret val, it's always true
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(this, _url, _flags & Keep, _flags & Reload);
2133}
2134
2136{
2137 s_kDirListerCache.localData().stop(this);
2138}
2139
2141{
2142 s_kDirListerCache.localData().stopListingUrl(this, _url);
2143}
2144
2146{
2147 s_kDirListerCache.localData().forgetDirs(this, _url, true, KMountPoint::possibleMountPoints(KMountPoint::NeedMountOptions));
2148}
2149
2150bool KCoreDirLister::autoUpdate() const
2151{
2152 return d->autoUpdate;
2153}
2154
2156{
2157 if (d->autoUpdate == enable) {
2158 return;
2159 }
2160
2161 d->autoUpdate = enable;
2162 s_kDirListerCache.localData().setAutoUpdate(this, enable);
2163}
2164
2165bool KCoreDirLister::showHiddenFiles() const
2166{
2167 return d->settings.isShowingDotFiles;
2168}
2169
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
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
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(this);
2212 }
2213}
2214
2216{
2217 return d->url;
2218}
2219
2221{
2222 return d->lstDirs;
2223}
2224
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(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(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(item.name()) != oldVisibleItems.cend();
2275 const bool mimeFiltered = matchesMimeFilter(item);
2276 const bool nowVisible = isItemVisible(item) && mimeFiltered;
2277 if (nowVisible && !wasVisible) {
2278 addNewItem(dir, item); // takes care of emitting newItem or itemsFilteredByMime
2279 } else if (!nowVisible && wasVisible) {
2280 if (!mimeFiltered) {
2281 lstMimeFilteredItems.append(item);
2282 }
2283 deletedItems.append(item);
2284 }
2285 }
2286 if (!deletedItems.isEmpty()) {
2287 Q_EMIT q->itemsDeleted(deletedItems);
2288 }
2289 emitItems();
2290 }
2291 oldSettings = settings;
2292}
2293
2295{
2296 s_kDirListerCache.localData().updateDirectory(dirUrl);
2297}
2298
2300{
2301 return d->complete;
2302}
2303
2305{
2306 return d->rootFileItem;
2307}
2308
2310{
2311 return s_kDirListerCache.localData().findByUrl(this, url);
2312}
2313
2315{
2316 return s_kDirListerCache.localData().findByName(this, name);
2317}
2318
2319// ================ public filter methods ================ //
2320
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(QLatin1Char(' '), Qt::SkipEmptyParts);
2333 for (const QStringView filter : list) {
2335 }
2336}
2337
2338QString KCoreDirLister::nameFilter() const
2339{
2340 return d->nameFilter;
2341}
2342
2344{
2345 if (d->settings.mimeFilter == mimeFilter) {
2346 return;
2347 }
2348
2349 d->prepareForSettingsChange();
2350 if (mimeFilter.contains(QLatin1String("application/octet-stream")) || mimeFilter.contains(QLatin1String("all/allfiles"))) { // all files
2351 d->settings.mimeFilter.clear();
2352 } else {
2353 d->settings.mimeFilter = mimeFilter;
2354 }
2355}
2356
2358{
2359 if (d->settings.mimeExcludeFilter == mimeExcludeFilter) {
2360 return;
2361 }
2362
2363 d->prepareForSettingsChange();
2364 d->settings.mimeExcludeFilter = mimeExcludeFilter;
2365}
2366
2368{
2369 d->prepareForSettingsChange();
2370 d->settings.mimeFilter.clear();
2371 d->settings.mimeExcludeFilter.clear();
2372}
2373
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(settings.lstFilters.cbegin(), settings.lstFilters.cend(), [&item](const QRegularExpression &filter) {
2398 return filter.match(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(item.mimetype(), settings.mimeFilter) && doMimeExcludeFilter(item.mimetype(), 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(mime);
2420 if (!mimeptr.isValid()) {
2421 return false;
2422 }
2423
2424 qCDebug(KIO_CORE_DIRLISTER) << "doMimeFilter: investigating:" << mimeptr.name();
2425 return std::any_of(filters.cbegin(), filters.cend(), [&mimeptr](const QString &filter) {
2426 return mimeptr.inherits(filter);
2427 });
2428}
2429
2430bool KCoreDirListerPrivate::doMimeExcludeFilter(const QString &mime, const QStringList &filters) const
2431{
2432 return !std::any_of(filters.cbegin(), filters.cend(), [&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(item); // items not filtered
2450 } else {
2451 Q_ASSERT(!item.isNull());
2452 lstMimeFilteredItems.append(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({oldItem, item});
2471 return;
2472 }
2473
2474 const bool refreshItemWasFiltered = !isItemVisible(oldItem) || !matchesMimeFilter(oldItem);
2475 if (item.exists() && isItemVisible(item) && matchesMimeFilter(item)) {
2476 if (refreshItemWasFiltered) {
2477 Q_ASSERT(!item.isNull());
2478 lstNewItems[directoryUrl].append(item);
2479 } else {
2480 Q_ASSERT(!item.isNull());
2481 lstRefreshItems.append(qMakePair(oldItem, 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(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(it.key(), val);
2498 Q_EMIT q->newItems(val); // compat
2499 }
2500 lstNewItems.clear();
2501 }
2502
2503 if (!lstMimeFilteredItems.empty()) {
2504 Q_EMIT q->itemsFilteredByMime(lstMimeFilteredItems);
2505 lstMimeFilteredItems.clear();
2506 }
2507
2508 if (!lstRefreshItems.empty()) {
2509 Q_EMIT q->refreshItems(lstRefreshItems);
2510 lstRefreshItems.clear();
2511 }
2512
2513 if (!lstRemoveItems.empty()) {
2514 Q_EMIT q->itemsDeleted(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(itemsList.cbegin(), itemsList.cend(), std::back_inserter(items), [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(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(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(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(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(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(job);
2625}
2626
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(job, data);
2636 d->complete = false;
2637}
2638
2639void KCoreDirListerPrivate::connectJob(KIO::ListJob *job)
2640{
2641 q->connect(job, &KJob::infoMessage, q, [this](KJob *job, const QString &plain) {
2642 slotInfoMessage(job, plain);
2643 });
2644
2645 q->connect(job, &KJob::percentChanged, q, [this](KJob *job, ulong _percent) {
2646 slotPercent(job, _percent);
2647 });
2648
2649 q->connect(job, &KJob::totalSize, q, [this](KJob *job, qulonglong _size) {
2650 slotTotalSize(job, _size);
2651 });
2652 q->connect(job, &KJob::processedSize, q, [this](KJob *job, qulonglong _psize) {
2653 slotProcessedSize(job, _psize);
2654 });
2655 q->connect(job, &KJob::speed, q, [this](KJob *job, qulonglong _speed) {
2656 slotSpeed(job, _speed);
2657 });
2658}
2659
2661{
2662 return itemsForDir(url(), which);
2663}
2664
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(allItems->cbegin(), allItems->cend(), std::back_inserter(result), [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(oldUrl, 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(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(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 auto listers = listersByStatus(ListerStatus::Listing);
2732 QMutableListIterator<KCoreDirLister *> lister_it(listers);
2733 while (lister_it.hasNext()) {
2734 KCoreDirLister *lister = lister_it.next();
2735 if (!lister->d->cachedItemsJobForUrl(url)) {
2736 // OK, move this lister from "currently listing" to "currently holding".
2737
2738 insertOrModifyLister(lister, ListerStatus::Holding);
2739 lister_it.remove();
2740 } else {
2741 qCDebug(KIO_CORE_DIRLISTER) << "Not moving" << lister << "to listersCurrentlyHolding because it still has job" << lister->d->m_cachedItemsJobs;
2742 }
2743 }
2744}
2745
2747{
2748 if (s_kDirListerCache.hasLocalData()) {
2749 return s_kDirListerCache.localData().itemForUrl(url);
2750 } else {
2751 return {};
2752 }
2753}
2754
2755bool KCoreDirLister::autoErrorHandlingEnabled() const
2756{
2757 return d->m_autoErrorHandling;
2758}
2759
2761{
2762 d->m_autoErrorHandling = enable;
2763}
2764
2765KCoreDirListerCache::CacheHiddenFile *KCoreDirListerCache::cachedDotHiddenForDir(const QString &dir)
2766{
2767 const QString path = dir + QLatin1String("/.hidden");
2768 QFile dotHiddenFile(path);
2769
2770 if (dotHiddenFile.exists()) {
2771 const QDateTime mtime = QFileInfo(dotHiddenFile).lastModified();
2772 CacheHiddenFile *cachedDotHiddenFile = m_cacheHiddenFiles.object(path);
2773
2774 if (cachedDotHiddenFile && mtime <= cachedDotHiddenFile->mtime) {
2775 // ".hidden" is in cache and still valid (the file was not modified since then),
2776 // so return it
2777 return cachedDotHiddenFile;
2778 } else {
2779 // read the ".hidden" file, then cache it and return it
2780 if (dotHiddenFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
2781 std::set<QString> filesToHide;
2782 QTextStream stream(&dotHiddenFile);
2783 while (!stream.atEnd()) {
2784 QString name = stream.readLine();
2785 if (!name.isEmpty()) {
2786 filesToHide.insert(name);
2787 }
2788 }
2789
2790 m_cacheHiddenFiles.insert(path, new CacheHiddenFile(mtime, std::move(filesToHide)));
2791
2792 return m_cacheHiddenFiles.object(path);
2793 }
2794 }
2795 }
2796
2797 return {};
2798}
2799
2800QList<KCoreDirLister *> KCoreDirListerCacheDirectoryData::listersByStatus(ListerStatus status) const
2801{
2803 for (const auto &[lister, listerStatus] : m_listerContainer) {
2804 if (listerStatus == status) {
2805 listers.append(lister);
2806 }
2807 }
2808 return listers;
2809}
2810
2811void KCoreDirListerCacheDirectoryData::insertOrModifyListers(const QList<KCoreDirLister *> listers, ListerStatus status)
2812{
2813 for (const auto &lister : listers) {
2814 insertOrModifyLister(lister, status);
2815 }
2816}
2817
2818void KCoreDirListerCacheDirectoryData::insertOrModifyLister(KCoreDirLister *lister, ListerStatus status)
2819{
2820 Q_ASSERT(lister);
2821 if (!lister) {
2822 return;
2823 }
2824
2825 m_listerContainer.insert_or_assign(lister, status);
2826}
2827
2828void KCoreDirListerCacheDirectoryData::removeLister(KCoreDirLister *lister)
2829{
2830 Q_ASSERT(lister);
2831 if (!lister) {
2832 return;
2833 }
2834
2835 const auto it = m_listerContainer.find(lister);
2836 if (it != m_listerContainer.end()) {
2837 m_listerContainer.erase(it);
2838 }
2839}
2840
2841QList<KCoreDirLister *> KCoreDirListerCacheDirectoryData::allListers() const
2842{
2844 for (const auto &[lister, listerStatus] : m_listerContainer) {
2845 listers.append(lister);
2846 }
2847 return listers;
2848}
2849
2850int KCoreDirListerCacheDirectoryData::totaListerCount()
2851{
2852 return m_listerContainer.size();
2853}
2854
2855int KCoreDirListerCacheDirectoryData::listerCountByStatus(ListerStatus status)
2856{
2857 int count = 0;
2858 for (const auto &[lister, listerStatus] : m_listerContainer) {
2859 if (listerStatus == status) {
2860 count++;
2861 }
2862 }
2863 return count;
2864}
2865
2866bool KCoreDirListerCacheDirectoryData::contains(KCoreDirLister *lister, ListerStatus status)
2867{
2868 const auto it = m_listerContainer.find(lister);
2869 if (it != m_listerContainer.end()) {
2870 const auto listerStatus = it->second;
2871 return listerStatus == status;
2872 }
2873 return false;
2874}
2875
2876#include "moc_kcoredirlister.cpp"
2877#include "moc_kcoredirlister_p.cpp"
Helper class for the kiojob used to list and update a directory.
void emitChanges()
Actually emit the changes made with setShowHiddenFiles, setDirOnlyMode, setNameFilter and setMimeFilt...
virtual void jobStarted(KIO::ListJob *)
Reimplemented by KDirLister to associate windows with jobs.
void stop()
Stop listing all directories currently being listed.
void forgetDirs(const QUrl &dirUrl)
Stop listening for further changes in the given directory.
QFlags< OpenUrlFlag > OpenUrlFlags
Stores a combination of OpenUrlFlag values.
static KFileItem cachedItemForUrl(const QUrl &url)
Return the KFileItem for the given URL, if it was listed recently and it's still in the cache,...
void setShowHiddenFiles(bool showHiddenFiles)
Toggles whether hidden files (e.g.
void listingDirCompleted(const QUrl &dirUrl)
Tell the view that the listing of the directory dirUrl is finished.
WhichItems
Used by items() and itemsForDir() to specify whether you want all items for a directory or just the f...
void updateDirectory(const QUrl &dirUrl)
Update the directory dirUrl.
void clear()
Signals to the view to remove all items (when e.g. going from dirA to dirB).
void setNameFilter(const QString &filter)
Set a name filter to only list items matching this name, e.g. "*.cpp".
QList< QUrl > directories() const
Returns all URLs that are listed by this KCoreDirLister.
bool openUrl(const QUrl &dirUrl, OpenUrlFlags flags=NoFlags)
Run the directory lister on the given url.
void setMimeExcludeFilter(const QStringList &mimeList)
Filtering should be done with KFileFilter.
QUrl url() const
Returns the top level URL that is listed by this KCoreDirLister.
void setRequestMimeTypeWhileListing(bool request)
Toggles whether to request MIME types from the worker or in-process.
void jobError(KIO::Job *job)
Emitted if listing a directory fails with an error.
void started(const QUrl &dirUrl)
Tell the view that this KCoreDirLister has started to list dirUrl.
KFileItemList items(WhichItems which=FilteredItems) const
Returns the items listed for the current url().
KFileItem findByUrl(const QUrl &url) const
Find an item by its URL.
void setDelayedMimeTypes(bool delayedMimeTypes)
Delayed MIME types feature: If enabled, MIME types will be fetched on demand, which leads to a faster...
QStringList mimeFilters() const
Returns the list of MIME type based filters, as set via setMimeFilter().
void clearDir(const QUrl &dirUrl)
Signals to the view to clear all items from directory dirUrl.
void setAutoUpdate(bool enable)
Toggle automatic directory updating, when a directory changes (using KDirWatch).
~KCoreDirLister() override
Destroy the directory lister.
void setMimeFilter(const QStringList &mimeList)
Set MIME type based filter to only list items matching the given MIME types.
KFileItem findByName(const QString &name) const
Find an item by its name.
void setDirOnlyMode(bool dirsOnly)
Call this to list only directories (by default all items (directories and files) are listed).
KCoreDirLister(QObject *parent=nullptr)
Create a directory lister.
@ Reload
Indicates whether to use the cache or to reread the directory from the disk.
@ Keep
Previous directories aren't forgotten.
KFileItem rootItem() const
Returns the file item of the URL.
void listingDirCanceled(const QUrl &dirUrl)
Tell the view that the listing of the directory dirUrl was canceled.
bool isFinished() const
Returns true if no I/O operation is currently in progress.
void canceled()
Tell the view that the user canceled the listing.
void completed()
Tell the view that listing is finished.
KFileItemList itemsForDir(const QUrl &dirUrl, WhichItems which=FilteredItems) const
Returns the items listed for the given dirUrl.
void clearMimeFilter()
Clears the MIME type based filter.
void setAutoErrorHandlingEnabled(bool enable)
Enable or disable auto error handling.
static KDirWatch * self()
void deleted(const QString &path)
static bool exists()
void dirty(const QString &path)
void created(const QString &path)
List of KFileItems, which adds a few helper methods to QList<KFileItem>.
Definition kfileitem.h:632
A KFileItem is a generic class to handle a file, local or remote.
Definition kfileitem.h:36
void setUrl(const QUrl &url)
Sets the item's URL.
bool cmp(const KFileItem &item) const
Somewhat like a comparison operator, but more explicit, and it can detect that two fileitems differ i...
void refreshMimeType()
Re-reads MIME type information.
bool exists() const
returns whether the KFileItem exists on-disk Call only after initialization (i.e KIO::stat or refresh...
bool isNull() const
Return true if default-constructed.
KIO::UDSEntry entry() const
Returns the UDS entry.
void setLocalPath(const QString &path)
Sets the item's local path (UDS_LOCAL_PATH).
void refresh()
Throw away and re-read (for local files) all information about the file.
void setName(const QString &name)
Sets the item's name (i.e. the filename).
The base class for all jobs.
Definition job_base.h:45
void addMetaData(const QString &key, const QString &value)
Add key/value pair to the meta data that is sent to the worker.
Definition job.cpp:221
A ListJob is allows you to get the get the content of a directory.
Definition listjob.h:28
const QUrl & redirectionUrl() const
Returns the ListJob's redirection URL.
Definition listjob.cpp:283
void redirection(KIO::Job *job, const QUrl &url)
Signals a redirection.
void entries(KIO::Job *job, const KIO::UDSEntryList &list)
This signal emits the entry found by the job while listing.
const QUrl & url() const
Returns the SimpleJob's URL.
Definition simplejob.cpp:70
QString stringValue(uint field) const
Definition udsentry.cpp:365
@ UDS_URL
An alternative URL (If different from the caption).
Definition udsentry.h:251
@ UDS_NAME
Filename - as displayed in directory listings etc.
Definition udsentry.h:224
virtual void showErrorMessage()
int error() const
void processedSize(KJob *job, qulonglong size)
void result(KJob *job)
KJobUiDelegate * uiDelegate() const
void infoMessage(KJob *job, const QString &message)
void totalSize(KJob *job, qulonglong size)
bool kill(KJob::KillVerbosity verbosity=KJob::Quietly)
void percentChanged(KJob *job, unsigned long percent)
void speed(KJob *job, unsigned long speed)
List of mount points.
Definition kmountpoint.h:39
Ptr findByPath(const QString &path) const
Find the mountpoint on which resides path For instance if /home is a separate partition,...
@ NeedMountOptions
Also fetch the options used when mounting, see KMountPoint::mountOptions().
Definition kmountpoint.h:73
QStringList mountOptions() const
Options used to mount the filesystem.
static List possibleMountPoints(DetailsNeededFlags infoNeeded=BasicInfoNeeded)
This function gives a list of all possible mountpoints.
void stop(Ekos::AlignState mode)
Q_SCRIPTABLE Q_NOREPLY void abort()
Q_SCRIPTABLE CaptureState status()
Q_SCRIPTABLE Q_NOREPLY void start()
KIOCORE_EXPORT ListJob * listDir(const QUrl &url, JobFlags flags=DefaultFlags, ListJob::ListFlags listFlags=ListJob::ListFlag::IncludeHidden)
List the contents of url, which is assumed to be a directory.
Definition listjob.cpp:239
QList< UDSEntry > UDSEntryList
A directory listing is a list of UDSEntry instances.
Definition udsentry.h:379
@ HideProgressInfo
Hide progress information dialog, i.e. don't show a GUI.
Definition job_base.h:251
qulonglong filesize_t
64-bit file size
Definition global.h:35
@ StatDefaultDetails
Default StatDetail flag when creating a StatJob.
Definition global.h:275
@ StatMimeType
MIME type.
Definition global.h:271
QString path(const QString &relativePath)
KIOCORE_EXPORT QString dir(const QString &fileClass)
Returns the most recently used directory associated with this file-class.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
QString name(StandardAction id)
KGuiItem remove()
const QList< QKeySequence > & reload()
QDBusConnection sessionBus()
QString cleanPath(const QString &path)
QString absolutePath() const const
QString canonicalFilePath() const const
QString fileName() const const
QDateTime lastModified() const const
const_iterator cbegin() const const
const_iterator cend() const const
qsizetype size() const const
void append(QList< T > &&value)
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
void clear()
const_iterator constBegin() const const
bool contains(const AT &value) const const
qsizetype count() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
qsizetype indexOf(const AT &value, qsizetype from) const const
bool isEmpty() const const
void prepend(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
T value(qsizetype i) const const
typedef Iterator
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
iterator erase(const_iterator first, const_iterator last)
Key key(const T &value, const Key &defaultKey) const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool isValid() const const
QObject(QObject *parent)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
QString wildcardToRegularExpression(QStringView pattern, WildcardConversionOptions options)
const_iterator cend() const const
void clear()
iterator erase(const_iterator first, const_iterator last)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
bool isNull() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
SkipEmptyParts
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
StripTrailingSlash
QUrl adjusted(FormattingOptions options) const const
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QList< QUrl > fromStringList(const QStringList &urls, ParsingMode mode)
bool isLocalFile() const const
bool isParentOf(const QUrl &childUrl) const const
bool isValid() const const
bool matches(const QUrl &url, FormattingOptions options) const const
QString path(ComponentFormattingOptions options) const const
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool toBool() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 21 2025 11:54:39 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.