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 ¤t, 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"