Libkleo

filesystemwatcher.cpp
1/* -*- mode: c++; c-basic-offset:4 -*-
2 filesystemwatcher.cpp
3
4 This file is part of Kleopatra, the KDE keymanager
5 SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include <config-libkleo.h>
11
12#include "filesystemwatcher.h"
13
14#include <libkleo/stl_util.h>
15
16#include <libkleo_debug.h>
17
18#include <QDir>
19#include <QFileSystemWatcher>
20#include <QRegularExpression>
21#include <QString>
22#include <QTimer>
23
24#include <set>
25
26using namespace Kleo;
27
28class FileSystemWatcher::Private
29{
30 FileSystemWatcher *const q;
31
32public:
33 explicit Private(FileSystemWatcher *qq, const QStringList &paths = QStringList());
34 ~Private()
35 {
36 delete m_watcher;
37 }
38
39 void onFileChanged(const QString &path);
40 void onDirectoryChanged(const QString &path);
41 void handleTimer();
42 void onTimeout();
43
44 void connectWatcher();
45
46 QFileSystemWatcher *m_watcher = nullptr;
47 QTimer m_timer;
48 std::set<QString> m_seenPaths;
49 std::set<QString> m_cachedDirectories;
50 std::set<QString> m_cachedFiles;
51 QStringList m_paths, m_blacklist, m_whitelist;
52};
53
54FileSystemWatcher::Private::Private(FileSystemWatcher *qq, const QStringList &paths)
55 : q(qq)
56 , m_watcher(nullptr)
57 , m_paths(paths)
58{
59 m_timer.setSingleShot(true);
60 connect(&m_timer, &QTimer::timeout, q, [this]() {
61 onTimeout();
62 });
63}
64
65static bool is_matching(const QString &file, const QStringList &list)
66{
67 for (const QString &entry : list) {
68 if (QRegularExpression::fromWildcard(entry, Qt::CaseInsensitive).match(file).hasMatch()) {
69 return true;
70 }
71 }
72 return false;
73}
74
75static bool is_blacklisted(const QString &file, const QStringList &blacklist)
76{
77 return is_matching(file, blacklist);
78}
79
80static bool is_whitelisted(const QString &file, const QStringList &whitelist)
81{
82 if (whitelist.empty()) {
83 return true; // special case
84 }
85 return is_matching(file, whitelist);
86}
87
88void FileSystemWatcher::Private::onFileChanged(const QString &path)
89{
90 const QFileInfo fi(path);
91 if (is_blacklisted(fi.fileName(), m_blacklist)) {
92 return;
93 }
94 if (!is_whitelisted(fi.fileName(), m_whitelist)) {
95 return;
96 }
97 qCDebug(LIBKLEO_LOG) << path;
98 if (fi.exists()) {
99 m_seenPaths.insert(path);
100 } else {
101 m_seenPaths.erase(path);
102 }
103 m_cachedFiles.insert(path);
104 handleTimer();
105}
106
107static QStringList list_dir_absolute(const QString &path, const QStringList &blacklist, const QStringList &whitelist)
108{
109 QDir dir(path);
111 QStringList::iterator end = std::remove_if(entries.begin(), entries.end(), [&blacklist](const QString &entry) {
112 return is_blacklisted(entry, blacklist);
113 });
114 if (!whitelist.empty()) {
115 end = std::remove_if(entries.begin(), end, [&whitelist](const QString &entry) {
116 return !is_whitelisted(entry, whitelist);
117 });
118 }
119 entries.erase(end, entries.end());
120 std::sort(entries.begin(), entries.end());
121
122 std::transform(entries.begin(), entries.end(), entries.begin(), [&dir](const QString &entry) {
123 return dir.absoluteFilePath(entry);
124 });
125
126 return entries;
127}
128
129static QStringList find_new_files(const QStringList &current, const std::set<QString> &seen)
130{
131 QStringList result;
132 std::set_difference(current.begin(), current.end(), seen.begin(), seen.end(), std::back_inserter(result));
133 return result;
134}
135
136void FileSystemWatcher::Private::onDirectoryChanged(const QString &path)
137{
138 const QStringList newFiles = find_new_files(list_dir_absolute(path, m_blacklist, m_whitelist), m_seenPaths);
139
140 if (newFiles.empty()) {
141 return;
142 }
143
144 qCDebug(LIBKLEO_LOG) << "newFiles" << newFiles;
145
146 m_cachedFiles.insert(newFiles.begin(), newFiles.end());
147 q->addPaths(newFiles);
148
149 m_cachedDirectories.insert(path);
150 handleTimer();
151}
152
153void FileSystemWatcher::Private::onTimeout()
154{
155 std::set<QString> dirs;
156 std::set<QString> files;
157
158 dirs.swap(m_cachedDirectories);
159 files.swap(m_cachedFiles);
160
161 if (dirs.empty() && files.empty()) {
162 return;
163 }
164
165 Q_EMIT q->triggered();
166
167 for (const QString &i : std::as_const(dirs)) {
168 Q_EMIT q->directoryChanged(i);
169 }
170 for (const QString &i : std::as_const(files)) {
171 Q_EMIT q->fileChanged(i);
172 }
173}
174
175void FileSystemWatcher::Private::handleTimer()
176{
177 if (m_timer.interval() == 0) {
178 onTimeout();
179 return;
180 }
181 m_timer.start();
182}
183
184void FileSystemWatcher::Private::connectWatcher()
185{
186 if (!m_watcher) {
187 return;
188 }
189 connect(m_watcher, &QFileSystemWatcher::directoryChanged, q, [this](const QString &str) {
190 onDirectoryChanged(str);
191 });
192 connect(m_watcher, &QFileSystemWatcher::fileChanged, q, [this](const QString &str) {
193 onFileChanged(str);
194 });
195}
196
197FileSystemWatcher::FileSystemWatcher(QObject *p)
198 : QObject(p)
199 , d(new Private(this))
200{
201 setEnabled(true);
202}
203
204FileSystemWatcher::FileSystemWatcher(const QStringList &paths, QObject *p)
205 : QObject(p)
206 , d(new Private(this, paths))
207{
208 setEnabled(true);
209}
210
211void FileSystemWatcher::setEnabled(bool enable)
212{
213 if (isEnabled() == enable) {
214 return;
215 }
216 if (enable) {
217 Q_ASSERT(!d->m_watcher);
218 d->m_watcher = new QFileSystemWatcher;
219 if (!d->m_paths.empty()) {
220 d->m_watcher->addPaths(d->m_paths);
221 }
222 d->connectWatcher();
223 } else {
224 Q_ASSERT(d->m_watcher);
225 delete d->m_watcher;
226 d->m_watcher = nullptr;
227 }
228}
229
230bool FileSystemWatcher::isEnabled() const
231{
232 return d->m_watcher != nullptr;
233}
234
235FileSystemWatcher::~FileSystemWatcher()
236{
237}
238
239void FileSystemWatcher::setDelay(int ms)
240{
241 Q_ASSERT(ms >= 0);
242 d->m_timer.setInterval(ms);
243}
244
245int FileSystemWatcher::delay() const
246{
247 return d->m_timer.interval();
248}
249
250void FileSystemWatcher::blacklistFiles(const QStringList &paths)
251{
252 d->m_blacklist += paths;
253 QStringList blacklisted;
254 d->m_paths.erase(kdtools::separate_if(d->m_paths.begin(),
255 d->m_paths.end(),
256 std::back_inserter(blacklisted),
257 d->m_paths.begin(),
258 [this](const QString &path) {
259 return is_blacklisted(path, d->m_blacklist);
260 })
261 .second,
262 d->m_paths.end());
263 if (d->m_watcher && !blacklisted.empty()) {
264 d->m_watcher->removePaths(blacklisted);
265 }
266}
267
268void FileSystemWatcher::whitelistFiles(const QStringList &patterns)
269{
270 d->m_whitelist += patterns;
271 // ### would be nice to add newly-matching paths here right away,
272 // ### but it's not as simple as blacklisting above, esp. since we
273 // ### don't want to subject addPath()'ed paths to whitelisting.
274}
275
276static QStringList resolve(const QStringList &paths, const QStringList &blacklist, const QStringList &whitelist)
277{
278 if (paths.empty()) {
279 return QStringList();
280 }
281 QStringList result;
282 for (const QString &path : paths) {
283 if (QDir(path).exists()) {
284 result += list_dir_absolute(path, blacklist, whitelist);
285 }
286 }
287 return result + resolve(result, blacklist, whitelist);
288}
289
290void FileSystemWatcher::addPaths(const QStringList &paths)
291{
292 if (paths.empty()) {
293 return;
294 }
295 const QStringList newPaths = paths + resolve(paths, d->m_blacklist, d->m_whitelist);
296 if (!newPaths.empty()) {
297 qCDebug(LIBKLEO_LOG) << "adding\n " << newPaths.join(QLatin1StringView("\n ")) << "\n/end";
298 }
299 d->m_paths += newPaths;
300 d->m_seenPaths.insert(newPaths.begin(), newPaths.end());
301 if (d->m_watcher && !newPaths.empty()) {
302 d->m_watcher->addPaths(newPaths);
303 }
304}
305
306void FileSystemWatcher::addPath(const QString &path)
307{
308 addPaths(QStringList(path));
309}
310
311void FileSystemWatcher::removePaths(const QStringList &paths)
312{
313 if (paths.empty()) {
314 return;
315 }
316 for (const QString &i : paths) {
317 d->m_paths.removeAll(i);
318 }
319 if (d->m_watcher) {
320 d->m_watcher->removePaths(paths);
321 }
322}
323
324void FileSystemWatcher::removePath(const QString &path)
325{
326 removePaths(QStringList(path));
327}
328
329#include "moc_filesystemwatcher.cpp"
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString path(const QString &relativePath)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & end()
void directoryChanged(const QString &path)
void fileChanged(const QString &path)
iterator begin()
bool empty() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
iterator insert(const_iterator before, parameter_type value)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QRegularExpression fromWildcard(QStringView pattern, Qt::CaseSensitivity cs, WildcardConversionOptions options)
QString join(QChar separator) const const
CaseInsensitive
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:12 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.