File indexing completed on 2024-05-12 05:47:29

0001 /*
0002  * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
0003  * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kdirectorycontentscounter.h"
0009 #include "dolphin_contentdisplaysettings.h"
0010 #include "kitemviews/kfileitemmodel.h"
0011 
0012 #include <KDirWatch>
0013 
0014 #include <QDir>
0015 #include <QFileInfo>
0016 #include <QThread>
0017 
0018 namespace
0019 {
0020 
0021 class LocalCache
0022 {
0023 public:
0024     struct cacheData {
0025         int count = 0;
0026         long long size = 0;
0027         ushort refCount = 0;
0028         qint64 timestamp = 0;
0029 
0030         inline operator bool() const
0031         {
0032             return timestamp != 0 && count != -1;
0033         }
0034     };
0035 
0036     LocalCache()
0037         : m_cache()
0038     {
0039     }
0040 
0041     cacheData insert(const QString &key, cacheData data, bool inserted)
0042     {
0043         data.timestamp = QDateTime::currentMSecsSinceEpoch();
0044         if (inserted) {
0045             data.refCount += 1;
0046         }
0047         m_cache.insert(key, data);
0048         return data;
0049     }
0050 
0051     cacheData value(const QString &key) const
0052     {
0053         return m_cache.value(key);
0054     }
0055 
0056     void unRefAll(const QSet<QString> &keys)
0057     {
0058         for (const auto &key : keys) {
0059             auto entry = m_cache[key];
0060             entry.refCount -= 1;
0061             if (entry.refCount == 0) {
0062                 m_cache.remove(key);
0063             }
0064         }
0065     }
0066 
0067     void removeAll(const QSet<QString> &keys)
0068     {
0069         for (const auto &key : keys) {
0070             m_cache.remove(key);
0071         }
0072     }
0073 
0074 private:
0075     QHash<QString, cacheData> m_cache;
0076 };
0077 
0078 /// cache of directory counting result
0079 static LocalCache *s_cache;
0080 static QThread *s_workerThread;
0081 static KDirectoryContentsCounterWorker *s_worker;
0082 }
0083 
0084 KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel *model, QObject *parent)
0085     : QObject(parent)
0086     , m_model(model)
0087     , m_priorityQueue()
0088     , m_queue()
0089     , m_workerIsBusy(false)
0090     , m_dirWatcher(nullptr)
0091     , m_watchedDirs()
0092 {
0093     if (s_cache == nullptr) {
0094         s_cache = new LocalCache();
0095     }
0096 
0097     if (!s_workerThread) {
0098         s_workerThread = new QThread();
0099         s_workerThread->setObjectName(QStringLiteral("KDirectoryContentsCounterThread"));
0100         s_workerThread->start();
0101     }
0102 
0103     if (!s_worker) {
0104         s_worker = new KDirectoryContentsCounterWorker();
0105         s_worker->moveToThread(s_workerThread);
0106     }
0107 
0108     connect(m_model, &KFileItemModel::itemsRemoved, this, &KDirectoryContentsCounter::slotItemsRemoved);
0109     connect(m_model, &KFileItemModel::directoryRefreshing, this, &KDirectoryContentsCounter::slotDirectoryRefreshing);
0110 
0111     connect(this, &KDirectoryContentsCounter::requestDirectoryContentsCount, s_worker, &KDirectoryContentsCounterWorker::countDirectoryContents);
0112 
0113     connect(s_worker, &KDirectoryContentsCounterWorker::result, this, &KDirectoryContentsCounter::slotResult);
0114     connect(s_worker, &KDirectoryContentsCounterWorker::intermediateResult, this, &KDirectoryContentsCounter::result);
0115     connect(s_worker, &KDirectoryContentsCounterWorker::finished, this, &KDirectoryContentsCounter::scheduleNext);
0116 
0117     m_dirWatcher = new KDirWatch(this);
0118     connect(m_dirWatcher, &KDirWatch::dirty, this, &KDirectoryContentsCounter::slotDirWatchDirty);
0119 }
0120 
0121 KDirectoryContentsCounter::~KDirectoryContentsCounter()
0122 {
0123     s_cache->unRefAll(m_watchedDirs);
0124 }
0125 
0126 void KDirectoryContentsCounter::slotResult(const QString &path, int count, long long size)
0127 {
0128     const auto fileInfo = QFileInfo(path);
0129     const QString resolvedPath = fileInfo.canonicalFilePath();
0130     if (fileInfo.isReadable() && !m_watchedDirs.contains(resolvedPath)) {
0131         m_dirWatcher->addDir(resolvedPath);
0132     }
0133     bool inserted = m_watchedDirs.insert(resolvedPath) == m_watchedDirs.end();
0134 
0135     // update cache or overwrite value
0136     s_cache->insert(resolvedPath, {count, size, true}, inserted);
0137 
0138     // sends the results
0139     Q_EMIT result(path, count, size);
0140 }
0141 
0142 void KDirectoryContentsCounter::slotDirWatchDirty(const QString &path)
0143 {
0144     const int index = m_model->index(QUrl::fromLocalFile(path));
0145     if (index >= 0) {
0146         if (!m_model->fileItem(index).isDir()) {
0147             // If INotify is used, KDirWatch issues the dirty() signal
0148             // also for changed files inside the directory, even if we
0149             // don't enable this behavior explicitly (see bug 309740).
0150             return;
0151         }
0152 
0153         scanDirectory(path, PathCountPriority::High);
0154     }
0155 }
0156 
0157 void KDirectoryContentsCounter::slotItemsRemoved()
0158 {
0159     const bool allItemsRemoved = (m_model->count() == 0);
0160 
0161     if (allItemsRemoved) {
0162         s_cache->removeAll(m_watchedDirs);
0163         stopWorker();
0164     }
0165 
0166     if (!m_watchedDirs.isEmpty()) {
0167         // Don't let KDirWatch watch for removed items
0168         if (allItemsRemoved) {
0169             for (const QString &path : std::as_const(m_watchedDirs)) {
0170                 m_dirWatcher->removeDir(path);
0171             }
0172             m_watchedDirs.clear();
0173         } else {
0174             QMutableSetIterator<QString> it(m_watchedDirs);
0175             while (it.hasNext()) {
0176                 const QString &path = it.next();
0177                 if (m_model->index(QUrl::fromLocalFile(path)) < 0) {
0178                     m_dirWatcher->removeDir(path);
0179                     it.remove();
0180                 }
0181             }
0182         }
0183     }
0184 }
0185 
0186 void KDirectoryContentsCounter::slotDirectoryRefreshing()
0187 {
0188     s_cache->removeAll(m_watchedDirs);
0189 }
0190 
0191 void KDirectoryContentsCounter::scheduleNext()
0192 {
0193     if (!m_priorityQueue.empty()) {
0194         m_currentPath = m_priorityQueue.front();
0195         m_priorityQueue.pop_front();
0196     } else if (!m_queue.empty()) {
0197         m_currentPath = m_queue.front();
0198         m_queue.pop_front();
0199     } else {
0200         m_currentPath.clear();
0201         m_workerIsBusy = false;
0202         return;
0203     }
0204 
0205     const auto fileInfo = QFileInfo(m_currentPath);
0206     const QString resolvedPath = fileInfo.canonicalFilePath();
0207     const auto pair = s_cache->value(resolvedPath);
0208     if (pair) {
0209         // fast path when in cache
0210         // will be updated later if result has changed
0211         Q_EMIT result(m_currentPath, pair.count, pair.size);
0212     }
0213 
0214     // if scanned fully recently, skip rescan
0215     if (pair && pair.timestamp >= fileInfo.fileTime(QFile::FileModificationTime).toMSecsSinceEpoch()) {
0216         scheduleNext();
0217         return;
0218     }
0219 
0220     KDirectoryContentsCounterWorker::Options options;
0221 
0222     if (m_model->showHiddenFiles()) {
0223         options |= KDirectoryContentsCounterWorker::CountHiddenFiles;
0224     }
0225 
0226     m_workerIsBusy = true;
0227     Q_EMIT requestDirectoryContentsCount(m_currentPath, options, ContentDisplaySettings::recursiveDirectorySizeLimit());
0228 }
0229 
0230 void KDirectoryContentsCounter::enqueuePathScanning(const QString &path, bool alreadyInCache, PathCountPriority priority)
0231 {
0232     // ensure to update the entry in the queue
0233     auto it = std::find(m_queue.begin(), m_queue.end(), path);
0234     if (it != m_queue.end()) {
0235         m_queue.erase(it);
0236     } else {
0237         it = std::find(m_priorityQueue.begin(), m_priorityQueue.end(), path);
0238         if (it != m_priorityQueue.end()) {
0239             m_priorityQueue.erase(it);
0240         }
0241     }
0242 
0243     if (priority == PathCountPriority::Normal) {
0244         if (alreadyInCache) {
0245             // we already knew the dir size
0246             // otherwise it gets lower priority
0247             m_queue.push_back(path);
0248         } else {
0249             m_queue.push_front(path);
0250         }
0251     } else {
0252         // append to priority queue
0253         m_priorityQueue.push_front(path);
0254     }
0255 }
0256 
0257 void KDirectoryContentsCounter::scanDirectory(const QString &path, PathCountPriority priority)
0258 {
0259     if (m_workerIsBusy && m_currentPath == path) {
0260         // already listing
0261         return;
0262     }
0263 
0264     const auto fileInfo = QFileInfo(path);
0265     const QString resolvedPath = fileInfo.canonicalFilePath();
0266     const auto pair = s_cache->value(resolvedPath);
0267     if (pair) {
0268         // fast path when in cache
0269         // will be updated later if result has changed
0270         Q_EMIT result(path, pair.count, pair.size);
0271     }
0272 
0273     // if scanned fully recently, skip rescan
0274     if (pair && pair.timestamp >= fileInfo.fileTime(QFile::FileModificationTime).toMSecsSinceEpoch()) {
0275         return;
0276     }
0277 
0278     enqueuePathScanning(path, pair, priority);
0279 
0280     if (!m_workerIsBusy && !s_worker->stopping()) {
0281         scheduleNext();
0282     }
0283 }
0284 
0285 void KDirectoryContentsCounter::stopWorker()
0286 {
0287     m_queue.clear();
0288     m_priorityQueue.clear();
0289 
0290     if (m_workerIsBusy && m_currentPath == s_worker->scannedPath()) {
0291         s_worker->stop();
0292     }
0293     m_currentPath.clear();
0294 }
0295 
0296 #include "moc_kdirectorycontentscounter.cpp"