/* * SPDX-FileCopyrightText: 2011 Peter Penz * SPDX-FileCopyrightText: 2013 Frank Reininghaus * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "kdirectorycontentscounter.h" #include "dolphin_contentdisplaysettings.h" #include "kitemviews/kfileitemmodel.h" #include #include #include #include namespace { class LocalCache { public: struct cacheData { int count = 0; long long size = 0; ushort refCount = 0; qint64 timestamp = 0; inline operator bool() const { return timestamp != 0 && count != -1; } }; LocalCache() : m_cache() { } cacheData insert(const QString &key, cacheData data, bool inserted) { data.timestamp = QDateTime::currentMSecsSinceEpoch(); if (inserted) { data.refCount += 1; } m_cache.insert(key, data); return data; } cacheData value(const QString &key) const { return m_cache.value(key); } void unRefAll(const QSet &keys) { for (const auto &key : keys) { auto entry = m_cache[key]; entry.refCount -= 1; if (entry.refCount == 0) { m_cache.remove(key); } } } void removeAll(const QSet &keys) { for (const auto &key : keys) { m_cache.remove(key); } } private: QHash m_cache; }; /// cache of directory counting result static LocalCache *s_cache; static QThread *s_workerThread; static KDirectoryContentsCounterWorker *s_worker; } KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel *model, QObject *parent) : QObject(parent) , m_model(model) , m_priorityQueue() , m_queue() , m_workerIsBusy(false) , m_dirWatcher(nullptr) , m_watchedDirs() { if (s_cache == nullptr) { s_cache = new LocalCache(); } if (!s_workerThread) { s_workerThread = new QThread(); s_workerThread->setObjectName(QStringLiteral("KDirectoryContentsCounterThread")); s_workerThread->start(); } if (!s_worker) { s_worker = new KDirectoryContentsCounterWorker(); s_worker->moveToThread(s_workerThread); } connect(m_model, &KFileItemModel::itemsRemoved, this, &KDirectoryContentsCounter::slotItemsRemoved); connect(m_model, &KFileItemModel::directoryRefreshing, this, &KDirectoryContentsCounter::slotDirectoryRefreshing); connect(this, &KDirectoryContentsCounter::requestDirectoryContentsCount, s_worker, &KDirectoryContentsCounterWorker::countDirectoryContents); connect(s_worker, &KDirectoryContentsCounterWorker::result, this, &KDirectoryContentsCounter::slotResult); connect(s_worker, &KDirectoryContentsCounterWorker::intermediateResult, this, &KDirectoryContentsCounter::result); connect(s_worker, &KDirectoryContentsCounterWorker::finished, this, &KDirectoryContentsCounter::scheduleNext); m_dirWatcher = new KDirWatch(this); connect(m_dirWatcher, &KDirWatch::dirty, this, &KDirectoryContentsCounter::slotDirWatchDirty); } KDirectoryContentsCounter::~KDirectoryContentsCounter() { s_cache->unRefAll(m_watchedDirs); } void KDirectoryContentsCounter::slotResult(const QString &path, int count, long long size) { const auto fileInfo = QFileInfo(path); const QString resolvedPath = fileInfo.canonicalFilePath(); if (fileInfo.isReadable() && !m_watchedDirs.contains(resolvedPath)) { m_dirWatcher->addDir(resolvedPath); } bool inserted = m_watchedDirs.insert(resolvedPath) == m_watchedDirs.end(); // update cache or overwrite value s_cache->insert(resolvedPath, {count, size, true}, inserted); // sends the results Q_EMIT result(path, count, size); } void KDirectoryContentsCounter::slotDirWatchDirty(const QString &path) { const int index = m_model->index(QUrl::fromLocalFile(path)); if (index >= 0) { if (!m_model->fileItem(index).isDir()) { // If INotify is used, KDirWatch issues the dirty() signal // also for changed files inside the directory, even if we // don't enable this behavior explicitly (see bug 309740). return; } scanDirectory(path, PathCountPriority::High); } } void KDirectoryContentsCounter::slotItemsRemoved() { const bool allItemsRemoved = (m_model->count() == 0); if (allItemsRemoved) { s_cache->removeAll(m_watchedDirs); stopWorker(); } if (!m_watchedDirs.isEmpty()) { // Don't let KDirWatch watch for removed items if (allItemsRemoved) { for (const QString &path : std::as_const(m_watchedDirs)) { m_dirWatcher->removeDir(path); } m_watchedDirs.clear(); } else { QMutableSetIterator it(m_watchedDirs); while (it.hasNext()) { const QString &path = it.next(); if (m_model->index(QUrl::fromLocalFile(path)) < 0) { m_dirWatcher->removeDir(path); it.remove(); } } } } } void KDirectoryContentsCounter::slotDirectoryRefreshing() { s_cache->removeAll(m_watchedDirs); } void KDirectoryContentsCounter::scheduleNext() { if (!m_priorityQueue.empty()) { m_currentPath = m_priorityQueue.front(); m_priorityQueue.pop_front(); } else if (!m_queue.empty()) { m_currentPath = m_queue.front(); m_queue.pop_front(); } else { m_currentPath.clear(); m_workerIsBusy = false; return; } const auto fileInfo = QFileInfo(m_currentPath); const QString resolvedPath = fileInfo.canonicalFilePath(); const auto pair = s_cache->value(resolvedPath); if (pair) { // fast path when in cache // will be updated later if result has changed Q_EMIT result(m_currentPath, pair.count, pair.size); } // if scanned fully recently, skip rescan if (pair && pair.timestamp >= fileInfo.fileTime(QFile::FileModificationTime).toMSecsSinceEpoch()) { scheduleNext(); return; } KDirectoryContentsCounterWorker::Options options; if (m_model->showHiddenFiles()) { options |= KDirectoryContentsCounterWorker::CountHiddenFiles; } m_workerIsBusy = true; Q_EMIT requestDirectoryContentsCount(m_currentPath, options, ContentDisplaySettings::recursiveDirectorySizeLimit()); } void KDirectoryContentsCounter::enqueuePathScanning(const QString &path, bool alreadyInCache, PathCountPriority priority) { // ensure to update the entry in the queue auto it = std::find(m_queue.begin(), m_queue.end(), path); if (it != m_queue.end()) { m_queue.erase(it); } else { it = std::find(m_priorityQueue.begin(), m_priorityQueue.end(), path); if (it != m_priorityQueue.end()) { m_priorityQueue.erase(it); } } if (priority == PathCountPriority::Normal) { if (alreadyInCache) { // we already knew the dir size // otherwise it gets lower priority m_queue.push_back(path); } else { m_queue.push_front(path); } } else { // append to priority queue m_priorityQueue.push_front(path); } } void KDirectoryContentsCounter::scanDirectory(const QString &path, PathCountPriority priority) { if (m_workerIsBusy && m_currentPath == path) { // already listing return; } const auto fileInfo = QFileInfo(path); const QString resolvedPath = fileInfo.canonicalFilePath(); const auto pair = s_cache->value(resolvedPath); if (pair) { // fast path when in cache // will be updated later if result has changed Q_EMIT result(path, pair.count, pair.size); } // if scanned fully recently, skip rescan if (pair && pair.timestamp >= fileInfo.fileTime(QFile::FileModificationTime).toMSecsSinceEpoch()) { return; } enqueuePathScanning(path, pair, priority); if (!m_workerIsBusy && !s_worker->stopping()) { scheduleNext(); } } void KDirectoryContentsCounter::stopWorker() { m_queue.clear(); m_priorityQueue.clear(); if (m_workerIsBusy && m_currentPath == s_worker->scannedPath()) { s_worker->stop(); } m_currentPath.clear(); } #include "moc_kdirectorycontentscounter.cpp"