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 <qdebug.h>
6#include <qdiriterator.h>
7#include <private/qfileinfo_p.h>
8#ifndef Q_OS_WIN
9# include <unistd.h>
10# include <sys/types.h>
11#endif
12#if defined(Q_OS_VXWORKS)
13# include "qplatformdefs.h"
14#endif
15
16QT_BEGIN_NAMESPACE
17
18using namespace Qt::StringLiterals;
19
20#ifdef QT_BUILD_INTERNAL
21Q_CONSTINIT static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false);
22Q_AUTOTEST_EXPORT void qt_test_resetFetchedRoot()
23{
24 fetchedRoot.storeRelaxed(newValue: false);
25}
26
27Q_AUTOTEST_EXPORT bool qt_test_isFetchedRoot()
28{
29 return fetchedRoot.loadRelaxed();
30}
31#endif
32
33static QString translateDriveName(const QFileInfo &drive)
34{
35 QString driveName = drive.absoluteFilePath();
36#ifdef Q_OS_WIN
37 if (driveName.startsWith(u'/')) // UNC host
38 return drive.fileName();
39 if (driveName.endsWith(u'/'))
40 driveName.chop(1);
41#endif // Q_OS_WIN
42 return driveName;
43}
44
45/*!
46 Creates thread
47*/
48QFileInfoGatherer::QFileInfoGatherer(QObject *parent)
49 : QThread(parent)
50 , m_iconProvider(&defaultProvider)
51{
52 start(LowPriority);
53}
54
55/*!
56 Destroys thread
57*/
58QFileInfoGatherer::~QFileInfoGatherer()
59{
60 abort.storeRelaxed(newValue: true);
61 QMutexLocker locker(&mutex);
62 condition.wakeAll();
63 locker.unlock();
64 wait();
65}
66
67void QFileInfoGatherer::setResolveSymlinks(bool enable)
68{
69 Q_UNUSED(enable);
70#ifdef Q_OS_WIN
71 m_resolveSymlinks = enable;
72#endif
73}
74
75void QFileInfoGatherer::driveAdded()
76{
77 fetchExtendedInformation(path: QString(), files: QStringList());
78}
79
80void QFileInfoGatherer::driveRemoved()
81{
82 QStringList drives;
83 const QFileInfoList driveInfoList = QDir::drives();
84 for (const QFileInfo &fi : driveInfoList)
85 drives.append(t: translateDriveName(drive: fi));
86 emit newListOfFiles(directory: QString(), listOfFiles: drives);
87}
88
89bool QFileInfoGatherer::resolveSymlinks() const
90{
91#ifdef Q_OS_WIN
92 return m_resolveSymlinks;
93#else
94 return false;
95#endif
96}
97
98void QFileInfoGatherer::setIconProvider(QAbstractFileIconProvider *provider)
99{
100 m_iconProvider = provider;
101}
102
103QAbstractFileIconProvider *QFileInfoGatherer::iconProvider() const
104{
105 return m_iconProvider;
106}
107
108/*!
109 Fetch extended information for all \a files in \a path
110
111 \sa updateFile(), update(), resolvedName()
112*/
113void QFileInfoGatherer::fetchExtendedInformation(const QString &path, const QStringList &files)
114{
115 QMutexLocker locker(&mutex);
116 // See if we already have this dir/file in our queue
117 int loc = this->path.lastIndexOf(str: path);
118 while (loc > 0) {
119 if (this->files.at(i: loc) == files) {
120 return;
121 }
122 loc = this->path.lastIndexOf(str: path, from: loc - 1);
123 }
124#if QT_CONFIG(thread)
125 this->path.push(t: path);
126 this->files.push(t: files);
127 condition.wakeAll();
128#else // !QT_CONFIG(thread)
129 getFileInfos(path, files);
130#endif // QT_CONFIG(thread)
131
132#if QT_CONFIG(filesystemwatcher)
133 if (files.isEmpty()
134 && !path.isEmpty()
135 && !path.startsWith(s: "//"_L1) /*don't watch UNC path*/) {
136 if (!watchedDirectories().contains(str: path))
137 watchPaths(paths: QStringList(path));
138 }
139#endif
140}
141
142/*!
143 Fetch extended information for all \a filePath
144
145 \sa fetchExtendedInformation()
146*/
147void QFileInfoGatherer::updateFile(const QString &filePath)
148{
149 QString dir = filePath.mid(position: 0, n: filePath.lastIndexOf(c: u'/'));
150 QString fileName = filePath.mid(position: dir.size() + 1);
151 fetchExtendedInformation(path: dir, files: QStringList(fileName));
152}
153
154QStringList QFileInfoGatherer::watchedFiles() const
155{
156#if QT_CONFIG(filesystemwatcher)
157 if (m_watcher)
158 return m_watcher->files();
159#endif
160 return {};
161}
162
163QStringList QFileInfoGatherer::watchedDirectories() const
164{
165#if QT_CONFIG(filesystemwatcher)
166 if (m_watcher)
167 return m_watcher->directories();
168#endif
169 return {};
170}
171
172void QFileInfoGatherer::createWatcher()
173{
174#if QT_CONFIG(filesystemwatcher)
175 m_watcher = new QFileSystemWatcher(this);
176 connect(sender: m_watcher, signal: &QFileSystemWatcher::directoryChanged, context: this, slot: &QFileInfoGatherer::list);
177 connect(sender: m_watcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &QFileInfoGatherer::updateFile);
178# if defined(Q_OS_WIN)
179 const QVariant listener = m_watcher->property("_q_driveListener");
180 if (listener.canConvert<QObject *>()) {
181 if (QObject *driveListener = listener.value<QObject *>()) {
182 connect(driveListener, SIGNAL(driveAdded()), this, SLOT(driveAdded()));
183 connect(driveListener, SIGNAL(driveRemoved()), this, SLOT(driveRemoved()));
184 }
185 }
186# endif // Q_OS_WIN
187#endif
188}
189
190void QFileInfoGatherer::watchPaths(const QStringList &paths)
191{
192#if QT_CONFIG(filesystemwatcher)
193 if (m_watching) {
194 if (m_watcher == nullptr)
195 createWatcher();
196 m_watcher->addPaths(files: paths);
197 }
198#else
199 Q_UNUSED(paths);
200#endif
201}
202
203void QFileInfoGatherer::unwatchPaths(const QStringList &paths)
204{
205#if QT_CONFIG(filesystemwatcher)
206 if (m_watcher && !paths.isEmpty())
207 m_watcher->removePaths(files: paths);
208#else
209 Q_UNUSED(paths);
210#endif
211}
212
213bool QFileInfoGatherer::isWatching() const
214{
215 bool result = false;
216#if QT_CONFIG(filesystemwatcher)
217 QMutexLocker locker(&mutex);
218 result = m_watching;
219#endif
220 return result;
221}
222
223void QFileInfoGatherer::setWatching(bool v)
224{
225#if QT_CONFIG(filesystemwatcher)
226 QMutexLocker locker(&mutex);
227 if (v != m_watching) {
228 if (!v) {
229 delete m_watcher;
230 m_watcher = nullptr;
231 }
232 m_watching = v;
233 }
234#else
235 Q_UNUSED(v);
236#endif
237}
238
239/*
240 List all files in \a directoryPath
241
242 \sa listed()
243*/
244void QFileInfoGatherer::clear()
245{
246#if QT_CONFIG(filesystemwatcher)
247 QMutexLocker locker(&mutex);
248 unwatchPaths(paths: watchedFiles());
249 unwatchPaths(paths: watchedDirectories());
250#endif
251}
252
253/*
254 Remove a \a path from the watcher
255
256 \sa listed()
257*/
258void QFileInfoGatherer::removePath(const QString &path)
259{
260#if QT_CONFIG(filesystemwatcher)
261 QMutexLocker locker(&mutex);
262 unwatchPaths(paths: QStringList(path));
263#else
264 Q_UNUSED(path);
265#endif
266}
267
268/*
269 List all files in \a directoryPath
270
271 \sa listed()
272*/
273void QFileInfoGatherer::list(const QString &directoryPath)
274{
275 fetchExtendedInformation(path: directoryPath, files: QStringList());
276}
277
278/*
279 Until aborted wait to fetch a directory or files
280*/
281void QFileInfoGatherer::run()
282{
283 forever {
284 QMutexLocker locker(&mutex);
285 while (!abort.loadRelaxed() && path.isEmpty())
286 condition.wait(lockedMutex: &mutex);
287 if (abort.loadRelaxed())
288 return;
289 const QString thisPath = std::as_const(t&: path).front();
290 path.pop_front();
291 const QStringList thisList = std::as_const(t&: files).front();
292 files.pop_front();
293 locker.unlock();
294
295 getFileInfos(path: thisPath, files: thisList);
296 }
297}
298
299QExtendedInformation QFileInfoGatherer::getInfo(const QFileInfo &fileInfo) const
300{
301 QExtendedInformation info(fileInfo);
302 info.icon = m_iconProvider->icon(fileInfo);
303 info.displayType = m_iconProvider->type(fileInfo);
304#if QT_CONFIG(filesystemwatcher)
305 // ### Not ready to listen all modifications by default
306 static const bool watchFiles = qEnvironmentVariableIsSet(varName: "QT_FILESYSTEMMODEL_WATCH_FILES");
307 if (watchFiles) {
308 if (!fileInfo.exists() && !fileInfo.isSymLink()) {
309 const_cast<QFileInfoGatherer *>(this)->
310 unwatchPaths(paths: QStringList(fileInfo.absoluteFilePath()));
311 } else {
312 const QString path = fileInfo.absoluteFilePath();
313 if (!path.isEmpty() && fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()
314 && !watchedFiles().contains(str: path)) {
315 const_cast<QFileInfoGatherer *>(this)->watchPaths(paths: QStringList(path));
316 }
317 }
318 }
319#endif // filesystemwatcher
320
321#ifdef Q_OS_WIN
322 if (m_resolveSymlinks && info.isSymLink(/* ignoreNtfsSymLinks = */ true)) {
323 QFileInfo resolvedInfo(QFileInfo(fileInfo.symLinkTarget()).canonicalFilePath());
324 if (resolvedInfo.exists()) {
325 emit nameResolved(fileInfo.filePath(), resolvedInfo.fileName());
326 }
327 }
328#endif
329 return info;
330}
331
332/*
333 Get specific file info's, batch the files so update when we have 100
334 items and every 200ms after that
335 */
336void QFileInfoGatherer::getFileInfos(const QString &path, const QStringList &files)
337{
338 // List drives
339 if (path.isEmpty()) {
340#ifdef QT_BUILD_INTERNAL
341 fetchedRoot.storeRelaxed(newValue: true);
342#endif
343 QFileInfoList infoList;
344 if (files.isEmpty()) {
345 infoList = QDir::drives();
346 } else {
347 infoList.reserve(asize: files.size());
348 for (const auto &file : files)
349 infoList << QFileInfo(file);
350 }
351 QList<QPair<QString, QFileInfo>> updatedFiles;
352 updatedFiles.reserve(asize: infoList.size());
353 for (int i = infoList.size() - 1; i >= 0; --i) {
354 QFileInfo driveInfo = infoList.at(i);
355 driveInfo.stat();
356 QString driveName = translateDriveName(drive: driveInfo);
357 updatedFiles.append(t: QPair<QString,QFileInfo>(driveName, driveInfo));
358 }
359 emit updates(directory: path, updates: updatedFiles);
360 return;
361 }
362
363 QElapsedTimer base;
364 base.start();
365 QFileInfo fileInfo;
366 bool firstTime = true;
367 QList<QPair<QString, QFileInfo>> updatedFiles;
368 QStringList filesToCheck = files;
369
370 QStringList allFiles;
371 if (files.isEmpty()) {
372 QDirIterator dirIt(path, QDir::AllEntries | QDir::System | QDir::Hidden);
373 while (!abort.loadRelaxed() && dirIt.hasNext()) {
374 fileInfo = dirIt.nextFileInfo();
375 fileInfo.stat();
376 allFiles.append(t: fileInfo.fileName());
377 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
378 }
379 }
380 if (!allFiles.isEmpty())
381 emit newListOfFiles(directory: path, listOfFiles: allFiles);
382
383 QStringList::const_iterator filesIt = filesToCheck.constBegin();
384 while (!abort.loadRelaxed() && filesIt != filesToCheck.constEnd()) {
385 fileInfo.setFile(path + QDir::separator() + *filesIt);
386 ++filesIt;
387 fileInfo.stat();
388 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
389 }
390 if (!updatedFiles.isEmpty())
391 emit updates(directory: path, updates: updatedFiles);
392 emit directoryLoaded(path);
393}
394
395void QFileInfoGatherer::fetch(const QFileInfo &fileInfo, QElapsedTimer &base, bool &firstTime,
396 QList<QPair<QString, QFileInfo>> &updatedFiles, const QString &path)
397{
398 updatedFiles.append(t: QPair<QString, QFileInfo>(fileInfo.fileName(), fileInfo));
399 QElapsedTimer current;
400 current.start();
401 if ((firstTime && updatedFiles.size() > 100) || base.msecsTo(other: current) > 1000) {
402 emit updates(directory: path, updates: updatedFiles);
403 updatedFiles.clear();
404 base = current;
405 firstTime = false;
406 }
407}
408
409QT_END_NAMESPACE
410
411#include "moc_qfileinfogatherer_p.cpp"
412

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