File indexing completed on 2024-06-09 05:17:27

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     filesystemwatcher.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-libkleo.h>
0011 
0012 #include "filesystemwatcher.h"
0013 
0014 #include <libkleo/stl_util.h>
0015 
0016 #include <libkleo_debug.h>
0017 
0018 #include <QDir>
0019 #include <QFileSystemWatcher>
0020 #include <QRegularExpression>
0021 #include <QString>
0022 #include <QTimer>
0023 
0024 #include <set>
0025 
0026 using namespace Kleo;
0027 
0028 class FileSystemWatcher::Private
0029 {
0030     FileSystemWatcher *const q;
0031 
0032 public:
0033     explicit Private(FileSystemWatcher *qq, const QStringList &paths = QStringList());
0034     ~Private()
0035     {
0036         delete m_watcher;
0037     }
0038 
0039     void onFileChanged(const QString &path);
0040     void onDirectoryChanged(const QString &path);
0041     void handleTimer();
0042     void onTimeout();
0043 
0044     void connectWatcher();
0045 
0046     QFileSystemWatcher *m_watcher = nullptr;
0047     QTimer m_timer;
0048     std::set<QString> m_seenPaths;
0049     std::set<QString> m_cachedDirectories;
0050     std::set<QString> m_cachedFiles;
0051     QStringList m_paths, m_blacklist, m_whitelist;
0052 };
0053 
0054 FileSystemWatcher::Private::Private(FileSystemWatcher *qq, const QStringList &paths)
0055     : q(qq)
0056     , m_watcher(nullptr)
0057     , m_paths(paths)
0058 {
0059     m_timer.setSingleShot(true);
0060     connect(&m_timer, &QTimer::timeout, q, [this]() {
0061         onTimeout();
0062     });
0063 }
0064 
0065 static bool is_matching(const QString &file, const QStringList &list)
0066 {
0067     for (const QString &entry : list) {
0068         if (QRegularExpression::fromWildcard(entry, Qt::CaseInsensitive).match(file).hasMatch()) {
0069             return true;
0070         }
0071     }
0072     return false;
0073 }
0074 
0075 static bool is_blacklisted(const QString &file, const QStringList &blacklist)
0076 {
0077     return is_matching(file, blacklist);
0078 }
0079 
0080 static bool is_whitelisted(const QString &file, const QStringList &whitelist)
0081 {
0082     if (whitelist.empty()) {
0083         return true; // special case
0084     }
0085     return is_matching(file, whitelist);
0086 }
0087 
0088 void FileSystemWatcher::Private::onFileChanged(const QString &path)
0089 {
0090     const QFileInfo fi(path);
0091     if (is_blacklisted(fi.fileName(), m_blacklist)) {
0092         return;
0093     }
0094     if (!is_whitelisted(fi.fileName(), m_whitelist)) {
0095         return;
0096     }
0097     qCDebug(LIBKLEO_LOG) << path;
0098     if (fi.exists()) {
0099         m_seenPaths.insert(path);
0100     } else {
0101         m_seenPaths.erase(path);
0102     }
0103     m_cachedFiles.insert(path);
0104     handleTimer();
0105 }
0106 
0107 static QStringList list_dir_absolute(const QString &path, const QStringList &blacklist, const QStringList &whitelist)
0108 {
0109     QDir dir(path);
0110     QStringList entries = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot);
0111     QStringList::iterator end = std::remove_if(entries.begin(), entries.end(), [&blacklist](const QString &entry) {
0112         return is_blacklisted(entry, blacklist);
0113     });
0114     if (!whitelist.empty()) {
0115         end = std::remove_if(entries.begin(), end, [&whitelist](const QString &entry) {
0116             return !is_whitelisted(entry, whitelist);
0117         });
0118     }
0119     entries.erase(end, entries.end());
0120     std::sort(entries.begin(), entries.end());
0121 
0122     std::transform(entries.begin(), entries.end(), entries.begin(), [&dir](const QString &entry) {
0123         return dir.absoluteFilePath(entry);
0124     });
0125 
0126     return entries;
0127 }
0128 
0129 static QStringList find_new_files(const QStringList &current, const std::set<QString> &seen)
0130 {
0131     QStringList result;
0132     std::set_difference(current.begin(), current.end(), seen.begin(), seen.end(), std::back_inserter(result));
0133     return result;
0134 }
0135 
0136 void FileSystemWatcher::Private::onDirectoryChanged(const QString &path)
0137 {
0138     const QStringList newFiles = find_new_files(list_dir_absolute(path, m_blacklist, m_whitelist), m_seenPaths);
0139 
0140     if (newFiles.empty()) {
0141         return;
0142     }
0143 
0144     qCDebug(LIBKLEO_LOG) << "newFiles" << newFiles;
0145 
0146     m_cachedFiles.insert(newFiles.begin(), newFiles.end());
0147     q->addPaths(newFiles);
0148 
0149     m_cachedDirectories.insert(path);
0150     handleTimer();
0151 }
0152 
0153 void FileSystemWatcher::Private::onTimeout()
0154 {
0155     std::set<QString> dirs;
0156     std::set<QString> files;
0157 
0158     dirs.swap(m_cachedDirectories);
0159     files.swap(m_cachedFiles);
0160 
0161     if (dirs.empty() && files.empty()) {
0162         return;
0163     }
0164 
0165     Q_EMIT q->triggered();
0166 
0167     for (const QString &i : std::as_const(dirs)) {
0168         Q_EMIT q->directoryChanged(i);
0169     }
0170     for (const QString &i : std::as_const(files)) {
0171         Q_EMIT q->fileChanged(i);
0172     }
0173 }
0174 
0175 void FileSystemWatcher::Private::handleTimer()
0176 {
0177     if (m_timer.interval() == 0) {
0178         onTimeout();
0179         return;
0180     }
0181     m_timer.start();
0182 }
0183 
0184 void FileSystemWatcher::Private::connectWatcher()
0185 {
0186     if (!m_watcher) {
0187         return;
0188     }
0189     connect(m_watcher, &QFileSystemWatcher::directoryChanged, q, [this](const QString &str) {
0190         onDirectoryChanged(str);
0191     });
0192     connect(m_watcher, &QFileSystemWatcher::fileChanged, q, [this](const QString &str) {
0193         onFileChanged(str);
0194     });
0195 }
0196 
0197 FileSystemWatcher::FileSystemWatcher(QObject *p)
0198     : QObject(p)
0199     , d(new Private(this))
0200 {
0201     setEnabled(true);
0202 }
0203 
0204 FileSystemWatcher::FileSystemWatcher(const QStringList &paths, QObject *p)
0205     : QObject(p)
0206     , d(new Private(this, paths))
0207 {
0208     setEnabled(true);
0209 }
0210 
0211 void FileSystemWatcher::setEnabled(bool enable)
0212 {
0213     if (isEnabled() == enable) {
0214         return;
0215     }
0216     if (enable) {
0217         Q_ASSERT(!d->m_watcher);
0218         d->m_watcher = new QFileSystemWatcher;
0219         if (!d->m_paths.empty()) {
0220             d->m_watcher->addPaths(d->m_paths);
0221         }
0222         d->connectWatcher();
0223     } else {
0224         Q_ASSERT(d->m_watcher);
0225         delete d->m_watcher;
0226         d->m_watcher = nullptr;
0227     }
0228 }
0229 
0230 bool FileSystemWatcher::isEnabled() const
0231 {
0232     return d->m_watcher != nullptr;
0233 }
0234 
0235 FileSystemWatcher::~FileSystemWatcher()
0236 {
0237 }
0238 
0239 void FileSystemWatcher::setDelay(int ms)
0240 {
0241     Q_ASSERT(ms >= 0);
0242     d->m_timer.setInterval(ms);
0243 }
0244 
0245 int FileSystemWatcher::delay() const
0246 {
0247     return d->m_timer.interval();
0248 }
0249 
0250 void FileSystemWatcher::blacklistFiles(const QStringList &paths)
0251 {
0252     d->m_blacklist += paths;
0253     QStringList blacklisted;
0254     d->m_paths.erase(kdtools::separate_if(d->m_paths.begin(),
0255                                           d->m_paths.end(),
0256                                           std::back_inserter(blacklisted),
0257                                           d->m_paths.begin(),
0258                                           [this](const QString &path) {
0259                                               return is_blacklisted(path, d->m_blacklist);
0260                                           })
0261                          .second,
0262                      d->m_paths.end());
0263     if (d->m_watcher && !blacklisted.empty()) {
0264         d->m_watcher->removePaths(blacklisted);
0265     }
0266 }
0267 
0268 void FileSystemWatcher::whitelistFiles(const QStringList &patterns)
0269 {
0270     d->m_whitelist += patterns;
0271     // ### would be nice to add newly-matching paths here right away,
0272     // ### but it's not as simple as blacklisting above, esp. since we
0273     // ### don't want to subject addPath()'ed paths to whitelisting.
0274 }
0275 
0276 static QStringList resolve(const QStringList &paths, const QStringList &blacklist, const QStringList &whitelist)
0277 {
0278     if (paths.empty()) {
0279         return QStringList();
0280     }
0281     QStringList result;
0282     for (const QString &path : paths) {
0283         if (QDir(path).exists()) {
0284             result += list_dir_absolute(path, blacklist, whitelist);
0285         }
0286     }
0287     return result + resolve(result, blacklist, whitelist);
0288 }
0289 
0290 void FileSystemWatcher::addPaths(const QStringList &paths)
0291 {
0292     if (paths.empty()) {
0293         return;
0294     }
0295     const QStringList newPaths = paths + resolve(paths, d->m_blacklist, d->m_whitelist);
0296     if (!newPaths.empty()) {
0297         qCDebug(LIBKLEO_LOG) << "adding\n " << newPaths.join(QLatin1StringView("\n ")) << "\n/end";
0298     }
0299     d->m_paths += newPaths;
0300     d->m_seenPaths.insert(newPaths.begin(), newPaths.end());
0301     if (d->m_watcher && !newPaths.empty()) {
0302         d->m_watcher->addPaths(newPaths);
0303     }
0304 }
0305 
0306 void FileSystemWatcher::addPath(const QString &path)
0307 {
0308     addPaths(QStringList(path));
0309 }
0310 
0311 void FileSystemWatcher::removePaths(const QStringList &paths)
0312 {
0313     if (paths.empty()) {
0314         return;
0315     }
0316     for (const QString &i : paths) {
0317         d->m_paths.removeAll(i);
0318     }
0319     if (d->m_watcher) {
0320         d->m_watcher->removePaths(paths);
0321     }
0322 }
0323 
0324 void FileSystemWatcher::removePath(const QString &path)
0325 {
0326     removePaths(QStringList(path));
0327 }
0328 
0329 #include "moc_filesystemwatcher.cpp"