1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qfileinfogatherer_p.h"
5#include <qcoreapplication.h>
6#include <qdebug.h>
7#include <qdirlisting.h>
8#include <private/qabstractfileiconprovider_p.h>
9#include <private/qfileinfo_p.h>
10#ifndef Q_OS_WIN
11# include <unistd.h>
12# include <sys/types.h>
13#endif
14#if defined(Q_OS_VXWORKS)
15# include "qplatformdefs.h"
16#endif
17
18QT_BEGIN_NAMESPACE
19
20using namespace Qt::StringLiterals;
21
22#ifdef QT_BUILD_INTERNAL
23Q_CONSTINIT static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false);
24Q_AUTOTEST_EXPORT void qt_test_resetFetchedRoot()
25{
26 fetchedRoot.storeRelaxed(newValue: false);
27}
28
29Q_AUTOTEST_EXPORT bool qt_test_isFetchedRoot()
30{
31 return fetchedRoot.loadRelaxed();
32}
33#endif
34
35static QString translateDriveName(const QFileInfo &drive)
36{
37 QString driveName = drive.absoluteFilePath();
38#ifdef Q_OS_WIN
39 if (driveName.startsWith(u'/')) // UNC host
40 return drive.fileName();
41 if (driveName.endsWith(u'/'))
42 driveName.chop(1);
43#endif // Q_OS_WIN
44 return driveName;
45}
46
47/*!
48 Creates thread
49*/
50QFileInfoGatherer::QFileInfoGatherer(QObject *parent)
51 : QThread(parent)
52 , m_iconProvider(&defaultProvider)
53{
54 start(LowPriority);
55}
56
57/*!
58 Destroys thread
59*/
60QFileInfoGatherer::~QFileInfoGatherer()
61{
62 requestAbort();
63 wait();
64}
65
66bool QFileInfoGatherer::event(QEvent *event)
67{
68 if (event->type() == QEvent::DeferredDelete && isRunning()) {
69 // We have been asked to shut down later but were blocked,
70 // so the owning QFileSystemModel proceeded with its shut-down
71 // and deferred the destruction of the gatherer.
72 // If we are still blocked now, then we have three bad options:
73 // terminate, wait forever (preventing the process from shutting down),
74 // or accept a memory leak.
75 requestAbort();
76 if (!wait(time: 5000)) {
77 // If the application is shutting down, then we terminate.
78 // Otherwise assume that sooner or later the thread will finish,
79 // and we delete it then.
80 if (QCoreApplication::closingDown())
81 terminate();
82 else
83 connect(sender: this, signal: &QThread::finished, context: this, slot: [this]{ delete this; });
84 return true;
85 }
86 }
87
88 return QThread::event(event);
89}
90
91void QFileInfoGatherer::requestAbort()
92{
93 requestInterruption();
94 QMutexLocker locker(&mutex);
95 condition.wakeAll();
96}
97
98void QFileInfoGatherer::setResolveSymlinks(bool enable)
99{
100 Q_UNUSED(enable);
101#ifdef Q_OS_WIN
102 m_resolveSymlinks = enable;
103#endif
104}
105
106void QFileInfoGatherer::driveAdded()
107{
108 fetchExtendedInformation(path: QString(), files: QStringList());
109}
110
111void QFileInfoGatherer::driveRemoved()
112{
113 QStringList drives;
114 const QFileInfoList driveInfoList = QDir::drives();
115 for (const QFileInfo &fi : driveInfoList)
116 drives.append(t: translateDriveName(drive: fi));
117 emit newListOfFiles(directory: QString(), listOfFiles: drives);
118}
119
120bool QFileInfoGatherer::resolveSymlinks() const
121{
122#ifdef Q_OS_WIN
123 return m_resolveSymlinks;
124#else
125 return false;
126#endif
127}
128
129void QFileInfoGatherer::setIconProvider(QAbstractFileIconProvider *provider)
130{
131 m_iconProvider = provider;
132}
133
134QAbstractFileIconProvider *QFileInfoGatherer::iconProvider() const
135{
136 return m_iconProvider;
137}
138
139/*!
140 Fetch extended information for all \a files in \a path
141
142 \sa updateFile(), update(), resolvedName()
143*/
144void QFileInfoGatherer::fetchExtendedInformation(const QString &path, const QStringList &files)
145{
146 QMutexLocker locker(&mutex);
147 // See if we already have this dir/file in our queue
148 qsizetype loc = 0;
149 while ((loc = this->path.lastIndexOf(str: path, from: loc - 1)) != -1) {
150 if (this->files.at(i: loc) == files)
151 return;
152 }
153
154#if QT_CONFIG(thread)
155 this->path.push(t: path);
156 this->files.push(t: files);
157 condition.wakeAll();
158#else // !QT_CONFIG(thread)
159 getFileInfos(path, files);
160#endif // QT_CONFIG(thread)
161
162#if QT_CONFIG(filesystemwatcher)
163 if (files.isEmpty()
164 && !path.isEmpty()
165 && !path.startsWith(s: "//"_L1) /*don't watch UNC path*/) {
166 if (!watchedDirectories().contains(str: path))
167 watchPaths(paths: QStringList(path));
168 }
169#endif
170}
171
172/*!
173 Fetch extended information for all \a filePath
174
175 \sa fetchExtendedInformation()
176*/
177void QFileInfoGatherer::updateFile(const QString &filePath)
178{
179 QString dir = filePath.mid(position: 0, n: filePath.lastIndexOf(c: u'/'));
180 QString fileName = filePath.mid(position: dir.size() + 1);
181 fetchExtendedInformation(path: dir, files: QStringList(fileName));
182}
183
184QStringList QFileInfoGatherer::watchedFiles() const
185{
186#if QT_CONFIG(filesystemwatcher)
187 if (m_watcher)
188 return m_watcher->files();
189#endif
190 return {};
191}
192
193QStringList QFileInfoGatherer::watchedDirectories() const
194{
195#if QT_CONFIG(filesystemwatcher)
196 if (m_watcher)
197 return m_watcher->directories();
198#endif
199 return {};
200}
201
202void QFileInfoGatherer::createWatcher()
203{
204#if QT_CONFIG(filesystemwatcher)
205 m_watcher = new QFileSystemWatcher(this);
206 connect(sender: m_watcher, signal: &QFileSystemWatcher::directoryChanged, context: this, slot: &QFileInfoGatherer::list);
207 connect(sender: m_watcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &QFileInfoGatherer::updateFile);
208# if defined(Q_OS_WIN)
209 const QVariant listener = m_watcher->property("_q_driveListener");
210 if (listener.canConvert<QObject *>()) {
211 if (QObject *driveListener = listener.value<QObject *>()) {
212 connect(driveListener, SIGNAL(driveAdded()), this, SLOT(driveAdded()));
213 connect(driveListener, SIGNAL(driveRemoved()), this, SLOT(driveRemoved()));
214 }
215 }
216# endif // Q_OS_WIN
217#endif
218}
219
220void QFileInfoGatherer::watchPaths(const QStringList &paths)
221{
222#if QT_CONFIG(filesystemwatcher)
223 if (m_watching) {
224 if (m_watcher == nullptr)
225 createWatcher();
226 m_watcher->addPaths(files: paths);
227 }
228#else
229 Q_UNUSED(paths);
230#endif
231}
232
233void QFileInfoGatherer::unwatchPaths(const QStringList &paths)
234{
235#if QT_CONFIG(filesystemwatcher)
236 if (m_watcher && !paths.isEmpty())
237 m_watcher->removePaths(files: paths);
238#else
239 Q_UNUSED(paths);
240#endif
241}
242
243bool QFileInfoGatherer::isWatching() const
244{
245 bool result = false;
246#if QT_CONFIG(filesystemwatcher)
247 QMutexLocker locker(&mutex);
248 result = m_watching;
249#endif
250 return result;
251}
252
253/*! \internal
254
255 If \a v is \c false, the QFileSystemWatcher used internally will be deleted
256 and subsequent calls to watchPaths() will do nothing.
257
258 If \a v is \c true, subsequent calls to watchPaths() will add those paths to
259 the filesystem watcher; watchPaths() will initialize a QFileSystemWatcher if
260 one hasn't already been initialized.
261*/
262void QFileInfoGatherer::setWatching(bool v)
263{
264#if QT_CONFIG(filesystemwatcher)
265 QMutexLocker locker(&mutex);
266 if (v != m_watching) {
267 m_watching = v;
268 if (!m_watching)
269 delete std::exchange(obj&: m_watcher, new_val: nullptr);
270 }
271#else
272 Q_UNUSED(v);
273#endif
274}
275
276/*
277 List all files in \a directoryPath
278
279 \sa listed()
280*/
281void QFileInfoGatherer::clear()
282{
283#if QT_CONFIG(filesystemwatcher)
284 QMutexLocker locker(&mutex);
285 unwatchPaths(paths: watchedFiles());
286 unwatchPaths(paths: watchedDirectories());
287#endif
288}
289
290/*
291 Remove a \a path from the watcher
292
293 \sa listed()
294*/
295void QFileInfoGatherer::removePath(const QString &path)
296{
297#if QT_CONFIG(filesystemwatcher)
298 QMutexLocker locker(&mutex);
299 unwatchPaths(paths: QStringList(path));
300#else
301 Q_UNUSED(path);
302#endif
303}
304
305/*
306 List all files in \a directoryPath
307
308 \sa listed()
309*/
310void QFileInfoGatherer::list(const QString &directoryPath)
311{
312 fetchExtendedInformation(path: directoryPath, files: QStringList());
313}
314
315/*
316 Until aborted wait to fetch a directory or files
317*/
318void QFileInfoGatherer::run()
319{
320 forever {
321 // Disallow termination while we are holding a mutex or can be
322 // woken up cleanly.
323 setTerminationEnabled(false);
324 QMutexLocker locker(&mutex);
325 while (!isInterruptionRequested() && path.isEmpty())
326 condition.wait(lockedMutex: &mutex);
327 if (isInterruptionRequested())
328 return;
329 const QString thisPath = std::as_const(t&: path).front();
330 path.pop_front();
331 const QStringList thisList = std::as_const(t&: files).front();
332 files.pop_front();
333 locker.unlock();
334
335 // Some of the system APIs we call when gathering file infomration
336 // might hang (e.g. waiting for network), so we explicitly allow
337 // termination now.
338 setTerminationEnabled(true);
339 getFileInfos(path: thisPath, files: thisList);
340 }
341}
342
343QExtendedInformation QFileInfoGatherer::getInfo(const QFileInfo &fileInfo) const
344{
345 QExtendedInformation info(fileInfo);
346 if (m_iconProvider) {
347 info.icon = m_iconProvider->icon(fileInfo);
348 info.displayType = m_iconProvider->type(fileInfo);
349 } else {
350 info.displayType = QAbstractFileIconProviderPrivate::getFileType(info: fileInfo);
351 }
352#if QT_CONFIG(filesystemwatcher)
353 // ### Not ready to listen all modifications by default
354 static const bool watchFiles = qEnvironmentVariableIsSet(varName: "QT_FILESYSTEMMODEL_WATCH_FILES");
355 if (watchFiles) {
356 if (!fileInfo.exists() && !fileInfo.isSymLink()) {
357 const_cast<QFileInfoGatherer *>(this)->
358 unwatchPaths(paths: QStringList(fileInfo.absoluteFilePath()));
359 } else {
360 const QString path = fileInfo.absoluteFilePath();
361 if (!path.isEmpty() && fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()
362 && !watchedFiles().contains(str: path)) {
363 const_cast<QFileInfoGatherer *>(this)->watchPaths(paths: QStringList(path));
364 }
365 }
366 }
367#endif // filesystemwatcher
368
369#ifdef Q_OS_WIN
370 if (m_resolveSymlinks && info.isSymLink(/* ignoreNtfsSymLinks = */ true)) {
371 QFileInfo resolvedInfo(QFileInfo(fileInfo.symLinkTarget()).canonicalFilePath());
372 if (resolvedInfo.exists()) {
373 emit nameResolved(fileInfo.filePath(), resolvedInfo.fileName());
374 }
375 }
376#endif
377 return info;
378}
379
380/*
381 Get specific file info's, batch the files so update when we have 100
382 items and every 200ms after that
383 */
384void QFileInfoGatherer::getFileInfos(const QString &path, const QStringList &files)
385{
386 // List drives
387 if (path.isEmpty()) {
388#ifdef QT_BUILD_INTERNAL
389 fetchedRoot.storeRelaxed(newValue: true);
390#endif
391 QList<std::pair<QString, QFileInfo>> updatedFiles;
392 auto addToUpdatedFiles = [&updatedFiles](QFileInfo &&fileInfo) {
393 fileInfo.stat();
394 updatedFiles.emplace_back(args: std::pair{translateDriveName(drive: fileInfo), fileInfo});
395 };
396
397 if (files.isEmpty()) {
398 // QDir::drives() calls QFSFileEngine::drives() which creates the QFileInfoList on
399 // the stack and return it, so this list is not shared, so no detaching.
400 QFileInfoList infoList = QDir::drives();
401 updatedFiles.reserve(asize: infoList.size());
402 for (auto rit = infoList.rbegin(), rend = infoList.rend(); rit != rend; ++rit)
403 addToUpdatedFiles(std::move(*rit));
404 } else {
405 updatedFiles.reserve(asize: files.size());
406 for (auto rit = files.crbegin(), rend = files.crend(); rit != rend; ++rit)
407 addToUpdatedFiles(QFileInfo(*rit));
408 }
409 emit updates(directory: path, updates: updatedFiles);
410 return;
411 }
412
413 QElapsedTimer base;
414 base.start();
415 QFileInfo fileInfo;
416 bool firstTime = true;
417 QList<std::pair<QString, QFileInfo>> updatedFiles;
418 QStringList filesToCheck = files;
419
420 QStringList allFiles;
421 if (files.isEmpty()) {
422 // Use QDirListing::IteratorFlags when QFileSystemModel is
423 // changed to use them too
424 constexpr auto dirFilters = QDir::AllEntries | QDir::System | QDir::Hidden;
425 for (const auto &dirEntry : QDirListing(path, {}, dirFilters.toInt())) {
426 if (isInterruptionRequested())
427 break;
428 fileInfo = dirEntry.fileInfo();
429 fileInfo.stat();
430 allFiles.append(t: fileInfo.fileName());
431 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
432 }
433 }
434 if (!allFiles.isEmpty())
435 emit newListOfFiles(directory: path, listOfFiles: allFiles);
436
437 QStringList::const_iterator filesIt = filesToCheck.constBegin();
438 while (!isInterruptionRequested() && filesIt != filesToCheck.constEnd()) {
439 fileInfo.setFile(path + QDir::separator() + *filesIt);
440 ++filesIt;
441 fileInfo.stat();
442 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
443 }
444 if (!updatedFiles.isEmpty())
445 emit updates(directory: path, updates: updatedFiles);
446 emit directoryLoaded(path);
447}
448
449void QFileInfoGatherer::fetch(const QFileInfo &fileInfo, QElapsedTimer &base, bool &firstTime,
450 QList<std::pair<QString, QFileInfo>> &updatedFiles, const QString &path)
451{
452 updatedFiles.emplace_back(args: std::pair(fileInfo.fileName(), fileInfo));
453 QElapsedTimer current;
454 current.start();
455 if ((firstTime && updatedFiles.size() > 100) || base.msecsTo(other: current) > 1000) {
456 emit updates(directory: path, updates: updatedFiles);
457 updatedFiles.clear();
458 base = current;
459 firstTime = false;
460 }
461}
462
463QT_END_NAMESPACE
464
465#include "moc_qfileinfogatherer_p.cpp"
466

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtbase/src/gui/itemmodels/qfileinfogatherer.cpp