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 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | using namespace Qt::StringLiterals; |
19 | |
20 | #ifdef QT_BUILD_INTERNAL |
21 | Q_CONSTINIT static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false); |
22 | Q_AUTOTEST_EXPORT void qt_test_resetFetchedRoot() |
23 | { |
24 | fetchedRoot.storeRelaxed(newValue: false); |
25 | } |
26 | |
27 | Q_AUTOTEST_EXPORT bool qt_test_isFetchedRoot() |
28 | { |
29 | return fetchedRoot.loadRelaxed(); |
30 | } |
31 | #endif |
32 | |
33 | static 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 | */ |
48 | QFileInfoGatherer::QFileInfoGatherer(QObject *parent) |
49 | : QThread(parent) |
50 | , m_iconProvider(&defaultProvider) |
51 | { |
52 | start(LowPriority); |
53 | } |
54 | |
55 | /*! |
56 | Destroys thread |
57 | */ |
58 | QFileInfoGatherer::~QFileInfoGatherer() |
59 | { |
60 | abort.storeRelaxed(newValue: true); |
61 | QMutexLocker locker(&mutex); |
62 | condition.wakeAll(); |
63 | locker.unlock(); |
64 | wait(); |
65 | } |
66 | |
67 | void QFileInfoGatherer::setResolveSymlinks(bool enable) |
68 | { |
69 | Q_UNUSED(enable); |
70 | #ifdef Q_OS_WIN |
71 | m_resolveSymlinks = enable; |
72 | #endif |
73 | } |
74 | |
75 | void QFileInfoGatherer::driveAdded() |
76 | { |
77 | fetchExtendedInformation(path: QString(), files: QStringList()); |
78 | } |
79 | |
80 | void 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 | |
89 | bool QFileInfoGatherer::resolveSymlinks() const |
90 | { |
91 | #ifdef Q_OS_WIN |
92 | return m_resolveSymlinks; |
93 | #else |
94 | return false; |
95 | #endif |
96 | } |
97 | |
98 | void QFileInfoGatherer::setIconProvider(QAbstractFileIconProvider *provider) |
99 | { |
100 | m_iconProvider = provider; |
101 | } |
102 | |
103 | QAbstractFileIconProvider *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 | */ |
113 | void 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 | */ |
147 | void 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 | |
154 | QStringList QFileInfoGatherer::watchedFiles() const |
155 | { |
156 | #if QT_CONFIG(filesystemwatcher) |
157 | if (m_watcher) |
158 | return m_watcher->files(); |
159 | #endif |
160 | return {}; |
161 | } |
162 | |
163 | QStringList QFileInfoGatherer::watchedDirectories() const |
164 | { |
165 | #if QT_CONFIG(filesystemwatcher) |
166 | if (m_watcher) |
167 | return m_watcher->directories(); |
168 | #endif |
169 | return {}; |
170 | } |
171 | |
172 | void 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 | |
190 | void 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 | |
203 | void 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 | |
213 | bool 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 | |
223 | void 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 | */ |
244 | void 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 | */ |
258 | void 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 | */ |
273 | void 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 | */ |
281 | void 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 | |
299 | QExtendedInformation 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 | */ |
336 | void 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 | |
395 | void 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 | |
409 | QT_END_NAMESPACE |
410 | |
411 | #include "moc_qfileinfogatherer_p.cpp" |
412 | |