File indexing completed on 2024-04-21 05:48:32

0001 /***********************************************************************
0002  * SPDX-FileCopyrightText: 2003-2004 Max Howell <max.howell@methylblue.com>
0003  * SPDX-FileCopyrightText: 2008-2009 Martin Sandsmark <martin.sandsmark@kde.org>
0004  * SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007  ***********************************************************************/
0008 
0009 #include "scan.h"
0010 
0011 #include "fileTree.h"
0012 #include "filelight_debug.h"
0013 #include "remoteLister.h"
0014 
0015 #include <QCursor>
0016 #include <QDir>
0017 #include <QGuiApplication>
0018 #include <QStringBuilder>
0019 
0020 namespace Filelight
0021 {
0022 
0023 ScanManager::ScanManager(QObject *parent)
0024     : QObject(parent)
0025     , m_abort(false)
0026     , m_files(0)
0027     , m_thread(nullptr)
0028 {
0029     connect(this, &ScanManager::branchCacheHit, this, &ScanManager::foundCached, Qt::QueuedConnection);
0030     // Bit aggressive this. Completed is emitted per folder I think.
0031     connect(this, &ScanManager::completed, this, &ScanManager::runningChanged, Qt::QueuedConnection);
0032 }
0033 
0034 ScanManager::~ScanManager()
0035 {
0036     if (m_thread) {
0037         qCDebug(FILELIGHT_LOG) << "Attempting to abort scan operation...";
0038         m_abort = true;
0039         m_thread->wait();
0040     }
0041 
0042     // RemoteListers are QObjects and get automatically deleted
0043 }
0044 
0045 bool ScanManager::running() const
0046 {
0047     return (m_thread && m_thread->isRunning()) || (m_remoteLister && !m_remoteLister->isFinished());
0048 }
0049 
0050 bool ScanManager::start(const QUrl &url)
0051 {
0052     QMutexLocker locker(&m_mutex); // The m_mutex gets released once locker is destroyed (goes out of scope).
0053 
0054     // Refresh mounts on each scan in case some have been mounted or unmounted, etc.
0055     Filelight::LocalLister::readMounts();
0056 
0057     // url is guaranteed clean and safe
0058 
0059     qCDebug(FILELIGHT_LOG) << "Scan requested for: " << url;
0060 
0061     if (running()) {
0062         qCWarning(FILELIGHT_LOG) << "Tried to launch two concurrent scans, aborting old one...";
0063         abort();
0064     }
0065 
0066     m_files = 0;
0067     m_totalSize = 0;
0068     m_abort = false;
0069 
0070     if (!url.isLocalFile()) {
0071         QGuiApplication::changeOverrideCursor(Qt::BusyCursor);
0072         // will start listing straight away
0073         m_remoteLister = std::make_unique<Filelight::RemoteLister>(url, this);
0074         connect(m_remoteLister.get(), &Filelight::RemoteLister::branchCompleted, this, &ScanManager::cacheTree, Qt::QueuedConnection);
0075         auto updateRunning = [this] {
0076             if (m_remoteLister && m_remoteLister->isFinished()) {
0077                 m_remoteLister = nullptr;
0078                 Q_EMIT runningChanged();
0079             }
0080         };
0081         connect(m_remoteLister.get(), &Filelight::RemoteLister::completed, this, updateRunning);
0082         connect(m_remoteLister.get(), &Filelight::RemoteLister::canceled, this, updateRunning);
0083         m_remoteLister->openUrl(url);
0084         Q_EMIT runningChanged();
0085         return true;
0086     }
0087 
0088     QString path = url.toLocalFile();
0089 
0090     // Cross-platform consideration: we get the path from a URL and in there the
0091     // separator is always the portable slash, as such toLocalFile will also get us a slash
0092     // separator, not the native one. i.e. on windows this is C:forwardslash not C:backslash
0093     // do not use QDir::separator!
0094     // https://bugs.kde.org/show_bug.cgi?id=450863
0095     if (!path.endsWith(QLatin1Char('/'))) {
0096         path += QLatin1Char('/');
0097     }
0098 
0099     auto *trees = new QList<std::shared_ptr<Folder>>;
0100 
0101     /* CHECK CACHE
0102      *   user wants: /usr/local/
0103      *   cached:     /usr/
0104      *
0105      *   user wants: /usr/
0106      *   cached:     /usr/local/, /usr/include/
0107      */
0108 
0109     QMutableListIterator<std::shared_ptr<Folder>> it(m_cache);
0110     while (it.hasNext()) {
0111         auto folder = it.next();
0112         const QString cachePath = folder->decodedName();
0113 
0114         if (path.startsWith(cachePath)) { // then whole tree already scanned
0115             // find a pointer to the requested branch
0116 
0117             qCDebug(FILELIGHT_LOG) << "Cache-(a)hit: " << cachePath;
0118             QList<QStringView> split = QStringView(path).mid(cachePath.length()).split(QLatin1Char('/'));
0119             std::shared_ptr<Folder> d = folder;
0120 
0121             while (!split.isEmpty() && d != nullptr) { // if NULL we have got lost so abort!!
0122                 if (split.first().isEmpty()) { // found the dir
0123                     break;
0124                 }
0125                 QString s = split.first() % QLatin1Char('/'); // % is the string concatenation operator for QStringBuilder
0126 
0127                 QListIterator<std::shared_ptr<File>> it(d->files);
0128                 d = nullptr;
0129                 while (it.hasNext()) {
0130                     auto subfolder = it.next();
0131                     if (s == subfolder->decodedName()) {
0132                         d = std::dynamic_pointer_cast<Folder>(subfolder);
0133                         break;
0134                     }
0135                 }
0136 
0137                 split.pop_front();
0138             }
0139 
0140             if (d) {
0141                 delete trees;
0142 
0143                 // we found a completed tree, thus no need to scan
0144                 qCDebug(FILELIGHT_LOG) << "Found cache-handle, generating map..";
0145 
0146                 Q_EMIT branchCacheHit(d);
0147 
0148                 return true;
0149             } // something went wrong, we couldn't find the folder we were expecting
0150             qCWarning(FILELIGHT_LOG) << "Didn't find " << path << " in the cache!\n";
0151             it.remove();
0152             Q_EMIT aboutToEmptyCache();
0153             break; // do a full scan
0154         }
0155         if (cachePath.startsWith(path)) { // then part of the requested tree is already scanned
0156             qCDebug(FILELIGHT_LOG) << "Cache-(b)hit: " << cachePath;
0157             it.remove();
0158             trees->append(folder);
0159         }
0160     }
0161 
0162     QGuiApplication::changeOverrideCursor(QCursor(Qt::BusyCursor));
0163     // starts listing by itself
0164     m_thread = std::make_unique<Filelight::LocalLister>(path, trees, this);
0165     connect(m_thread.get(), &LocalLister::branchCompleted, this, &ScanManager::cacheTree, Qt::QueuedConnection);
0166     m_thread->start();
0167 
0168     Q_EMIT runningChanged();
0169 
0170     return true;
0171 }
0172 
0173 bool ScanManager::abort()
0174 {
0175     m_abort = true;
0176 
0177     if (m_remoteLister) {
0178         m_remoteLister->stop();
0179     }
0180     m_remoteLister = nullptr;
0181     const bool ret = m_thread && m_thread->wait();
0182     Q_EMIT runningChanged();
0183 
0184     Q_EMIT aborted();
0185     return ret;
0186 }
0187 
0188 void ScanManager::invalidateCacheFor(const QUrl &url)
0189 {
0190     m_abort = true;
0191 
0192     if (m_thread && m_thread->isRunning()) {
0193         m_thread->wait();
0194     }
0195 
0196     if (!url.isLocalFile()) {
0197         qWarning() << "Remote cache clearing not implemented";
0198         return;
0199     }
0200 
0201     QString path = url.toLocalFile();
0202     // Do not use QDir::separator! The path is using / even on windows.
0203     if (!path.endsWith(QLatin1Char('/'))) {
0204         path += QLatin1Char('/');
0205     }
0206 
0207     Q_EMIT aboutToEmptyCache();
0208 
0209     QMutableListIterator<std::shared_ptr<Folder>> cacheIterator(m_cache);
0210     QList<std::shared_ptr<Folder>> subCaches;
0211     QList<std::shared_ptr<Folder>> oldCache = m_cache;
0212     while (cacheIterator.hasNext()) {
0213         auto folder = cacheIterator.next();
0214         cacheIterator.remove();
0215 
0216         QString cachePath = folder->decodedName();
0217 
0218         if (!path.startsWith(cachePath)) {
0219             continue;
0220         }
0221         QList<QStringView> splitPath = QStringView(path).mid(cachePath.length()).split(QLatin1Char('/'));
0222         auto d = folder;
0223 
0224         while (!splitPath.isEmpty() && d != nullptr) { // if NULL we have got lost so abort!!
0225             if (splitPath.first().isEmpty()) { // found the dir
0226                 break;
0227             }
0228             QString wantedName = splitPath.takeFirst() % QLatin1Char('/'); // % is the string concatenation operator for QStringBuilder
0229 
0230             QListIterator<std::shared_ptr<File>> it(d->files);
0231             d = nullptr;
0232             while (it.hasNext()) {
0233                 auto subfolder = it.next();
0234                 if (subfolder->decodedName() == wantedName) {
0235                     // This is the one we want to remove
0236                     continue;
0237                 }
0238                 if (!subfolder->isFolder()) {
0239                     continue;
0240                 }
0241 
0242                 auto newFolder = std::dynamic_pointer_cast<Folder>(subfolder)->duplicate();
0243                 newFolder->setName((cachePath.toLocal8Bit() + subfolder->name8Bit()));
0244                 subCaches.append(newFolder);
0245                 d = nullptr;
0246             }
0247         }
0248 
0249         if (!d || !d->parent()) {
0250             continue;
0251         }
0252         d->parent()->remove(d);
0253     }
0254 
0255     for (const auto &folder : subCaches) {
0256         m_cache.append(folder);
0257     }
0258 }
0259 
0260 void ScanManager::emptyCache()
0261 {
0262     m_abort = true;
0263 
0264     if (m_thread && m_thread->isRunning()) {
0265         m_thread->wait();
0266     }
0267 
0268     Q_EMIT aboutToEmptyCache();
0269 
0270     m_cache.clear();
0271 }
0272 
0273 void ScanManager::cacheTree(std::shared_ptr<Folder> tree)
0274 {
0275     QMutexLocker locker(&m_mutex); // This gets released once it is destroyed.
0276 
0277     if (m_thread) {
0278         qCDebug(FILELIGHT_LOG) << "Waiting for thread to terminate ...";
0279         m_thread->wait();
0280         qCDebug(FILELIGHT_LOG) << "Thread terminated!";
0281         m_thread = nullptr;
0282     }
0283 
0284     Q_EMIT completed(tree);
0285 
0286     if (tree) {
0287         // we don't cache foreign stuff
0288         // we don't recache stuff (thus only type 1000 events)
0289         // we always just have one tree cached, so we really don't need a list..
0290         m_cache.append(tree);
0291     } else { // scan failed
0292         m_cache.clear();
0293     }
0294 
0295     QGuiApplication::restoreOverrideCursor();
0296 }
0297 
0298 void ScanManager::foundCached(std::shared_ptr<Folder> tree)
0299 {
0300     Q_EMIT completed(tree);
0301     QGuiApplication::restoreOverrideCursor();
0302 }
0303 
0304 }