-/***************************************************************************
- * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
- * Copyright (C) 2013 by Frank Reininghaus <frank78ac@googlemail.com> *
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program; if not, write to the *
- * Free Software Foundation, Inc., *
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
- ***************************************************************************/
+/*
+ * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
+ * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
#include "kdirectorycontentscounter.h"
-
-#include "kdirectorycontentscounterworker.h"
-#include <kitemviews/kfileitemmodel.h>
+#include "dolphin_contentdisplaysettings.h"
+#include "kitemviews/kfileitemmodel.h"
#include <KDirWatch>
+
+#include <QDir>
+#include <QFileInfo>
#include <QThread>
-KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel* model, QObject* parent) :
- QObject(parent),
- m_model(model),
- m_queue(),
- m_worker(0),
- m_workerIsBusy(false),
- m_dirWatcher(0),
- m_watchedDirs()
+namespace
{
- connect(m_model, &KFileItemModel::itemsRemoved,
- this, &KDirectoryContentsCounter::slotItemsRemoved);
- if (!m_workerThread) {
- m_workerThread = new QThread();
- m_workerThread->start();
- }
+class LocalCache
+{
+public:
+ struct cacheData {
+ int count = 0;
+ long long size = 0;
+ ushort refCount = 0;
+ qint64 timestamp = 0;
- m_worker = new KDirectoryContentsCounterWorker();
- m_worker->moveToThread(m_workerThread);
- ++m_workersCount;
+ inline operator bool() const
+ {
+ return timestamp != 0 && count != -1;
+ }
+ };
- connect(this, &KDirectoryContentsCounter::requestDirectoryContentsCount,
- m_worker, &KDirectoryContentsCounterWorker::countDirectoryContents);
- connect(m_worker, &KDirectoryContentsCounterWorker::result,
- this, &KDirectoryContentsCounter::slotResult);
+ LocalCache()
+ : m_cache()
+ {
+ }
- m_dirWatcher = new KDirWatch(this);
- connect(m_dirWatcher, &KDirWatch::dirty, this, &KDirectoryContentsCounter::slotDirWatchDirty);
-}
+ 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;
+ }
-KDirectoryContentsCounter::~KDirectoryContentsCounter()
-{
- --m_workersCount;
+ cacheData value(const QString &key) const
+ {
+ return m_cache.value(key);
+ }
- if (m_workersCount > 0) {
- // The worker thread will continue running. It could even be running
- // a method of m_worker at the moment, so we delete it using
- // deleteLater() to prevent a crash.
- m_worker->deleteLater();
- } else {
- // There are no remaining workers -> stop the worker thread.
- m_workerThread->quit();
- m_workerThread->wait();
- delete m_workerThread;
- m_workerThread = 0;
+ void unRefAll(const QSet<QString> &keys)
+ {
+ for (const auto &key : keys) {
+ auto entry = m_cache[key];
+ entry.refCount -= 1;
+ if (entry.refCount == 0) {
+ m_cache.remove(key);
+ }
+ }
+ }
- // The worker thread has finished running now, so it's safe to delete
- // m_worker. deleteLater() would not work at all because the event loop
- // which would deliver the event to m_worker is not running any more.
- delete m_worker;
+ void removeAll(const QSet<QString> &keys)
+ {
+ for (const auto &key : keys) {
+ m_cache.remove(key);
+ }
}
-}
-void KDirectoryContentsCounter::addDirectory(const QString& path)
-{
- startWorker(path);
+private:
+ QHash<QString, cacheData> m_cache;
+};
+
+/// cache of directory counting result
+static LocalCache *s_cache;
+static QThread *s_workerThread;
+static KDirectoryContentsCounterWorker *s_worker;
}
-int KDirectoryContentsCounter::countDirectoryContentsSynchronously(const QString& path)
+KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel *model, QObject *parent)
+ : QObject(parent)
+ , m_model(model)
+ , m_priorityQueue()
+ , m_queue()
+ , m_workerIsBusy(false)
+ , m_dirWatcher(nullptr)
+ , m_watchedDirs()
{
- if (!m_dirWatcher->contains(path)) {
- m_dirWatcher->addDir(path);
- m_watchedDirs.insert(path);
+ if (s_cache == nullptr) {
+ s_cache = new LocalCache();
}
- KDirectoryContentsCounterWorker::Options options;
-
- if (m_model->showHiddenFiles()) {
- options |= KDirectoryContentsCounterWorker::CountHiddenFiles;
+ if (!s_workerThread) {
+ s_workerThread = new QThread();
+ s_workerThread->setObjectName(QStringLiteral("KDirectoryContentsCounterThread"));
+ s_workerThread->start();
}
- if (m_model->showDirectoriesOnly()) {
- options |= KDirectoryContentsCounterWorker::CountDirectoriesOnly;
+ if (!s_worker) {
+ s_worker = new KDirectoryContentsCounterWorker();
+ s_worker->moveToThread(s_workerThread);
}
- return KDirectoryContentsCounterWorker::subItemsCount(path, options);
+ 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);
}
-void KDirectoryContentsCounter::slotResult(const QString& path, int count)
+KDirectoryContentsCounter::~KDirectoryContentsCounter()
{
- m_workerIsBusy = false;
+ s_cache->unRefAll(m_watchedDirs);
+}
- if (!m_dirWatcher->contains(path)) {
- m_dirWatcher->addDir(path);
- m_watchedDirs.insert(path);
+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();
- if (!m_queue.isEmpty()) {
- startWorker(m_queue.dequeue());
- }
+ // update cache or overwrite value
+ s_cache->insert(resolvedPath, {count, size, true}, inserted);
- emit result(path, count);
+ // sends the results
+ Q_EMIT result(path, count, size);
}
-void KDirectoryContentsCounter::slotDirWatchDirty(const QString& path)
+void KDirectoryContentsCounter::slotDirWatchDirty(const QString &path)
{
- const int index = m_model->index(KUrl(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
return;
}
- startWorker(path);
+ scanDirectory(path, PathCountPriority::High);
}
}
{
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) {
- foreach (const QString& path, m_watchedDirs) {
+ for (const QString &path : qAsConst(m_watchedDirs)) {
m_dirWatcher->removeDir(path);
}
m_watchedDirs.clear();
- m_queue.clear();
} else {
QMutableSetIterator<QString> it(m_watchedDirs);
while (it.hasNext()) {
- const QString& path = it.next();
- if (m_model->index(KUrl(path)) < 0) {
+ const QString &path = it.next();
+ if (m_model->index(QUrl::fromLocalFile(path)) < 0) {
m_dirWatcher->removeDir(path);
it.remove();
}
}
}
-void KDirectoryContentsCounter::startWorker(const QString& path)
+void KDirectoryContentsCounter::slotDirectoryRefreshing()
{
- if (m_workerIsBusy) {
- m_queue.enqueue(path);
+ 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 {
- KDirectoryContentsCounterWorker::Options options;
+ 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;
+ }
- 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 (m_model->showDirectoriesOnly()) {
- options |= KDirectoryContentsCounterWorker::CountDirectoriesOnly;
+ 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();
- emit requestDirectoryContentsCount(path, options);
- m_workerIsBusy = true;
+ if (m_workerIsBusy && m_currentPath == s_worker->scannedPath()) {
+ s_worker->stop();
}
+ m_currentPath.clear();
}
-QThread* KDirectoryContentsCounter::m_workerThread = 0;
-int KDirectoryContentsCounter::m_workersCount = 0;
\ No newline at end of file
+#include "moc_kdirectorycontentscounter.cpp"