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 
26 using namespace Kleo;
27 
28 class FileSystemWatcher::Private
29 {
30  FileSystemWatcher *const q;
31 
32 public:
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 
54 FileSystemWatcher::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 
65 static 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 
75 static bool is_blacklisted(const QString &file, const QStringList &blacklist)
76 {
77  return is_matching(file, blacklist);
78 }
79 
80 static 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 
88 void 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 
107 static QStringList list_dir_absolute(const QString &path, const QStringList &blacklist, const QStringList &whitelist)
108 {
109  QDir dir(path);
110  QStringList entries = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot);
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 
129 static 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 
136 void 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 
153 void 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 
175 void FileSystemWatcher::Private::handleTimer()
176 {
177  if (m_timer.interval() == 0) {
178  onTimeout();
179  return;
180  }
181  m_timer.start();
182 }
183 
184 void 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 
197 FileSystemWatcher::FileSystemWatcher(QObject *p)
198  : QObject(p)
199  , d(new Private(this))
200 {
201  setEnabled(true);
202 }
203 
204 FileSystemWatcher::FileSystemWatcher(const QStringList &paths, QObject *p)
205  : QObject(p)
206  , d(new Private(this, paths))
207 {
208  setEnabled(true);
209 }
210 
211 void 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 
230 bool FileSystemWatcher::isEnabled() const
231 {
232  return d->m_watcher != nullptr;
233 }
234 
235 FileSystemWatcher::~FileSystemWatcher()
236 {
237 }
238 
239 void FileSystemWatcher::setDelay(int ms)
240 {
241  Q_ASSERT(ms >= 0);
242  d->m_timer.setInterval(ms);
243 }
244 
245 int FileSystemWatcher::delay() const
246 {
247  return d->m_timer.interval();
248 }
249 
250 void 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 
268 void 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 
276 static 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 
290 void 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 
306 void FileSystemWatcher::addPath(const QString &path)
307 {
308  addPaths(QStringList(path));
309 }
310 
311 void 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 
324 void FileSystemWatcher::removePath(const QString &path)
325 {
326  removePaths(QStringList(path));
327 }
328 
329 #include "moc_filesystemwatcher.cpp"
CaseInsensitive
void fileChanged(const QString &path)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
bool empty() const const
QStringList addPaths(const QStringList &paths)
void timeout()
QString join(const QString &separator) const const
void insert(int i, const T &value)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QList::iterator erase(QList::iterator pos)
void setEnabled(bool)
QString path(const QString &relativePath)
QString & insert(int position, QChar ch)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QList::iterator begin()
QList::iterator end()
const QList< QKeySequence > & end()
void directoryChanged(const QString &path)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:56:14 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.