1 /**
2 * \file fileinfogatherer.cpp
3 * \cond
4 * Taken from Qt Git, revision e73bd4a
5 * qtbase/src/widgets/dialogs/qfileinfogatherer.cpp
6 * Adapted for Kid3 with the following changes:
7 * - Remove Q prefix from class names
8 * - Remove QT_..._CONFIG, QT_..._NAMESPACE, Q_..._EXPORT...
9 * - Allow compilation with Qt versions < 5.7
10 * - Remove moc includes
11 * - Remove dependencies to Qt5::Widgets
12 */
13 /****************************************************************************
14 **
15 ** Copyright (C) 2016 The Qt Company Ltd.
16 ** Contact: https://www.qt.io/licensing/
17 **
18 ** This file is part of the QtWidgets module of the Qt Toolkit.
19 **
20 ** $QT_BEGIN_LICENSE:LGPL$
21 ** Commercial License Usage
22 ** Licensees holding valid commercial Qt licenses may use this file in
23 ** accordance with the commercial license agreement provided with the
24 ** Software or, alternatively, in accordance with the terms contained in
25 ** a written agreement between you and The Qt Company. For licensing terms
26 ** and conditions see https://www.qt.io/terms-conditions. For further
27 ** information use the contact form at https://www.qt.io/contact-us.
28 **
29 ** GNU Lesser General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU Lesser
31 ** General Public License version 3 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
33 ** packaging of this file. Please review the following information to
34 ** ensure the GNU Lesser General Public License version 3 requirements
35 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
36 **
37 ** GNU General Public License Usage
38 ** Alternatively, this file may be used under the terms of the GNU
39 ** General Public License version 2.0 or (at your option) the GNU General
40 ** Public license version 3 or any later version approved by the KDE Free
41 ** Qt Foundation. The licenses are as published by the Free Software
42 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
43 ** included in the packaging of this file. Please review the following
44 ** information to ensure the GNU General Public License requirements will
45 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
46 ** https://www.gnu.org/licenses/gpl-3.0.html.
47 **
48 ** $QT_END_LICENSE$
49 **
50 ****************************************************************************/
51
52 #include "fileinfogatherer_p.h"
53 #include <qdebug.h>
54 #include <qdiriterator.h>
55 #ifndef Q_OS_WIN
56 # include <unistd.h>
57 # include <sys/types.h>
58 #endif
59 #if defined(Q_OS_VXWORKS)
60 # include "qplatformdefs.h"
61 #endif
62 #include "abstractfiledecorationprovider.h"
63
64 #ifdef Q_OS_WIN
65 #include <windows.h>
66
67 /**
68 * Check if path is a drive which could cause an insert disk dialog to pop up.
69 *
70 * This method should be used before calling QFileInfo::permissions(),
71 * or QFileInfo::isReadable() on Windows.
72 * The bug has been reported for Windows 7 32-bit and could be reproduced with
73 * Windows XP. To trigger the bug, a CD has to be inserted and then removed once
74 * before fetching the root directory with a file system model. See
75 * https://forum.qt.io/topic/34799/checking-is-a-drive-is-readable-in-qt-pops-up-a-no-disk-error-in-windows-7
76 *
77 * @param path drive path, e.g. "D:/"
78 * @return true if path is for a drive and getting volume information fails.
79 */
isInvalidDrive(const QString & path)80 bool ExtendedInformation::isInvalidDrive(const QString &path)
81 {
82 // Windows drive nodes are queried with paths like "D:/", check if path is
83 // a drive letter followed by a colon.
84 const int pathLen = path.length();
85 if (pathLen < 2 || pathLen > 3 || path.at(1) != QLatin1Char(':') ||
86 !path.at(0).isLetter())
87 return false;
88
89 const DWORD VOLUME_NAME_SIZE = 255;
90 const DWORD FILE_SYSTEM_NAME_SIZE = 255;
91 LPCWSTR rootPathName = (LPCWSTR)path.utf16();
92 UCHAR fileSystemNameBuffer[255], volumeNameBuffer[255];
93 DWORD volumeSerialNumber, maximumComponentLength, fileSystemFlags;
94
95 BOOL bSuccess = ::GetVolumeInformationW(
96 rootPathName,
97 (LPWSTR)volumeNameBuffer,
98 VOLUME_NAME_SIZE,
99 &volumeSerialNumber,
100 &maximumComponentLength,
101 &fileSystemFlags,
102 (LPWSTR)fileSystemNameBuffer,
103 FILE_SYSTEM_NAME_SIZE
104 );
105
106 return !bSuccess;
107 }
108 #endif // Q_OS_WIN
109
110 #ifdef QT_BUILD_INTERNAL
111 static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false);
qt_test_resetFetchedRoot()112 void qt_test_resetFetchedRoot()
113 {
114 fetchedRoot.store(false);
115 }
116
qt_test_isFetchedRoot()117 bool qt_test_isFetchedRoot()
118 {
119 return fetchedRoot.load();
120 }
121 #endif
122
translateDriveName(const QFileInfo & drive)123 static QString translateDriveName(const QFileInfo &drive)
124 {
125 QString driveName = drive.absoluteFilePath();
126 #ifdef Q_OS_WIN
127 if (driveName.startsWith(QLatin1Char('/'))) // UNC host
128 return drive.fileName();
129 if (driveName.endsWith(QLatin1Char('/')))
130 driveName.chop(1);
131 #endif // Q_OS_WIN
132 return driveName;
133 }
134
135 /*!
136 Creates thread
137 */
FileInfoGatherer(QObject * parent)138 FileInfoGatherer::FileInfoGatherer(QObject *parent)
139 : QThread(parent), abort(false),
140 #ifndef QT_NO_FILESYSTEMWATCHER
141 watcher(0),
142 #endif
143 #ifdef Q_OS_WIN
144 m_resolveSymlinks(true),
145 #endif
146 m_decorationProvider(Q_NULLPTR)
147 {
148 #ifndef QT_NO_FILESYSTEMWATCHER
149 watcher = new QFileSystemWatcher(this);
150 connect(watcher, SIGNAL(directoryChanged(QString)), this, SLOT(list(QString)));
151 connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT(updateFile(QString)));
152
153 # if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
154 const QVariant listener = watcher->property("_q_driveListener");
155 if (listener.canConvert<QObject *>()) {
156 if (QObject *driveListener = listener.value<QObject *>()) {
157 connect(driveListener, SIGNAL(driveAdded()), this, SLOT(driveAdded()));
158 connect(driveListener, SIGNAL(driveRemoved()), this, SLOT(driveRemoved()));
159 }
160 }
161 # endif // Q_OS_WIN && !Q_OS_WINRT
162 #endif
163 start(LowPriority);
164 }
165
166 /*!
167 Destroys thread
168 */
~FileInfoGatherer()169 FileInfoGatherer::~FileInfoGatherer()
170 {
171 #if QT_VERSION >= 0x050e00
172 abort.storeRelaxed(true);
173 #else
174 abort.store(true);
175 #endif
176 QMutexLocker locker(&mutex);
177 condition.wakeAll();
178 locker.unlock();
179 wait();
180 }
181
setResolveSymlinks(bool enable)182 void FileInfoGatherer::setResolveSymlinks(bool enable)
183 {
184 Q_UNUSED(enable)
185 #ifdef Q_OS_WIN
186 m_resolveSymlinks = enable;
187 #endif
188 }
189
driveAdded()190 void FileInfoGatherer::driveAdded()
191 {
192 fetchExtendedInformation(QString(), QStringList());
193 }
194
driveRemoved()195 void FileInfoGatherer::driveRemoved()
196 {
197 QStringList drives;
198 const QFileInfoList driveInfoList = QDir::drives();
199 for (const QFileInfo &fi : driveInfoList)
200 drives.append(translateDriveName(fi));
201 newListOfFiles(QString(), drives);
202 }
203
resolveSymlinks() const204 bool FileInfoGatherer::resolveSymlinks() const
205 {
206 #ifdef Q_OS_WIN
207 return m_resolveSymlinks;
208 #else
209 return false;
210 #endif
211 }
212
setDecorationProvider(AbstractFileDecorationProvider * provider)213 void FileInfoGatherer::setDecorationProvider(AbstractFileDecorationProvider *provider)
214 {
215 m_decorationProvider = provider;
216 }
217
decorationProvider() const218 AbstractFileDecorationProvider *FileInfoGatherer::decorationProvider() const
219 {
220 return m_decorationProvider;
221 }
222
223 /*!
224 Fetch extended information for all \a files in \a path
225
226 \sa updateFile(), update(), resolvedName()
227 */
fetchExtendedInformation(const QString & path,const QStringList & files)228 void FileInfoGatherer::fetchExtendedInformation(const QString &path, const QStringList &files)
229 {
230 QMutexLocker locker(&mutex);
231 // See if we already have this dir/file in our queue
232 int loc = this->path.lastIndexOf(path);
233 while (loc > 0) {
234 if (this->files.at(loc) == files) {
235 return;
236 }
237 loc = this->path.lastIndexOf(path, loc - 1);
238 }
239 this->path.push(path);
240 this->files.push(files);
241 condition.wakeAll();
242
243 #ifndef QT_NO_FILESYSTEMWATCHER
244 if (files.isEmpty()
245 && !path.isEmpty()
246 && !path.startsWith(QLatin1String("//")) /*don't watch UNC path*/) {
247 if (!watcher->directories().contains(path))
248 watcher->addPath(path);
249 }
250 #endif
251 }
252
253 /*!
254 Fetch extended information for all \a filePath
255
256 \sa fetchExtendedInformation()
257 */
updateFile(const QString & filePath)258 void FileInfoGatherer::updateFile(const QString &filePath)
259 {
260 QString dir = filePath.mid(0, filePath.lastIndexOf(QLatin1Char('/')));
261 QString fileName = filePath.mid(dir.length() + 1);
262 fetchExtendedInformation(dir, QStringList(fileName));
263 }
264
265 /*
266 List all files in \a directoryPath
267
268 \sa listed()
269 */
clear()270 void FileInfoGatherer::clear()
271 {
272 #ifndef QT_NO_FILESYSTEMWATCHER
273 QMutexLocker locker(&mutex);
274 watcher->removePaths(watcher->files());
275 watcher->removePaths(watcher->directories());
276 #endif
277
278 path.clear();
279 files.clear();
280 }
281
282 /*
283 Add a \a path to the watcher
284 */
addPath(const QString & path)285 void FileInfoGatherer::addPath(const QString &path)
286 {
287 #ifndef QT_NO_FILESYSTEMWATCHER
288 QMutexLocker locker(&mutex);
289 watcher->addPath(path);
290 #else
291 Q_UNUSED(path);
292 #endif
293 }
294
295 /*
296 Remove a \a path from the watcher
297
298 \sa listed()
299 */
removePath(const QString & path)300 void FileInfoGatherer::removePath(const QString &path)
301 {
302 #ifndef QT_NO_FILESYSTEMWATCHER
303 QMutexLocker locker(&mutex);
304 watcher->removePath(path);
305 #else
306 Q_UNUSED(path);
307 #endif
308 }
309
310 /*
311 List all files in \a directoryPath
312
313 \sa listed()
314 */
list(const QString & directoryPath)315 void FileInfoGatherer::list(const QString &directoryPath)
316 {
317 fetchExtendedInformation(directoryPath, QStringList());
318 }
319
320 /*
321 Until aborted wait to fetch a directory or files
322 */
run()323 void FileInfoGatherer::run()
324 {
325 forever {
326 QMutexLocker locker(&mutex);
327 #if QT_VERSION >= 0x050e00
328 while (!abort.loadRelaxed() && path.isEmpty())
329 condition.wait(&mutex);
330 if (abort.loadRelaxed())
331 return;
332 #else
333 while (!abort.load() && path.isEmpty())
334 condition.wait(&mutex);
335 if (abort.load())
336 return;
337 #endif
338 #if QT_VERSION >= 0x050700
339 const QString thisPath = qAsConst(path).front();
340 #else
341 const auto constPath = path;
342 const QString thisPath = constPath.front();
343 #endif
344 path.pop_front();
345 #if QT_VERSION >= 0x050700
346 const QStringList thisList = qAsConst(files).front();
347 #else
348 const auto constFiles = files;
349 const QStringList thisList = constFiles.front();
350 #endif
351 files.pop_front();
352 locker.unlock();
353
354 getFileInfos(thisPath, thisList);
355 }
356 }
357
getInfo(const QFileInfo & fileInfo) const358 ExtendedInformation FileInfoGatherer::getInfo(const QFileInfo &fileInfo) const
359 {
360 ExtendedInformation info(fileInfo);
361 if (m_decorationProvider) {
362 info.icon = m_decorationProvider->decoration(fileInfo);
363 info.displayType = m_decorationProvider->type(fileInfo);
364 } else {
365 info.icon = QVariant();
366 info.displayType = AbstractFileDecorationProvider::fileTypeDescription(fileInfo);
367 }
368 #ifndef QT_NO_FILESYSTEMWATCHER
369 // ### Not ready to listen all modifications by default
370 static const bool watchFiles = qEnvironmentVariableIsSet("QT_FILESYSTEMMODEL_WATCH_FILES");
371 if (watchFiles) {
372 if (!fileInfo.exists() && !fileInfo.isSymLink()) {
373 watcher->removePath(fileInfo.absoluteFilePath());
374 } else {
375 const QString path = fileInfo.absoluteFilePath();
376 if (!path.isEmpty() && fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()
377 && !watcher->files().contains(path)) {
378 watcher->addPath(path);
379 }
380 }
381 }
382 #endif
383
384 #ifdef Q_OS_WIN
385 if (m_resolveSymlinks && info.isSymLink(/* ignoreNtfsSymLinks = */ true)) {
386 QFileInfo resolvedInfo(fileInfo.symLinkTarget());
387 resolvedInfo = resolvedInfo.canonicalFilePath();
388 if (resolvedInfo.exists()) {
389 emit nameResolved(fileInfo.filePath(), resolvedInfo.fileName());
390 }
391 }
392 #endif
393 return info;
394 }
395
396 /*
397 Get specific file info's, batch the files so update when we have 100
398 items and every 200ms after that
399 */
getFileInfos(const QString & path,const QStringList & files)400 void FileInfoGatherer::getFileInfos(const QString &path, const QStringList &files)
401 {
402 // List drives
403 if (path.isEmpty()) {
404 #ifdef QT_BUILD_INTERNAL
405 fetchedRoot.store(true);
406 #endif
407 QFileInfoList infoList;
408 if (files.isEmpty()) {
409 infoList = QDir::drives();
410 } else {
411 infoList.reserve(files.count());
412 for (const auto &file : files)
413 infoList << QFileInfo(file);
414 }
415 for (int i = infoList.count() - 1; i >= 0; --i) {
416 QString driveName = translateDriveName(infoList.at(i));
417 QVector<QPair<QString,QFileInfo> > updatedFiles;
418 updatedFiles.append(QPair<QString,QFileInfo>(driveName, infoList.at(i)));
419 emit updates(path, updatedFiles);
420 }
421 return;
422 }
423
424 QElapsedTimer base;
425 base.start();
426 QFileInfo fileInfo;
427 bool firstTime = true;
428 QVector<QPair<QString, QFileInfo> > updatedFiles;
429 QStringList filesToCheck = files;
430
431 QStringList allFiles;
432 if (files.isEmpty()) {
433 QDirIterator dirIt(path, QDir::AllEntries | QDir::System | QDir::Hidden);
434 #if QT_VERSION >= 0x050e00
435 while (!abort.loadRelaxed() && dirIt.hasNext())
436 #else
437 while (!abort.load() && dirIt.hasNext())
438 #endif
439 {
440 dirIt.next();
441 fileInfo = dirIt.fileInfo();
442 allFiles.append(fileInfo.fileName());
443 fetch(fileInfo, base, firstTime, updatedFiles, path);
444 }
445 }
446 if (!allFiles.isEmpty())
447 emit newListOfFiles(path, allFiles);
448
449 QStringList::const_iterator filesIt = filesToCheck.constBegin();
450 #if QT_VERSION >= 0x050e00
451 while (!abort.loadRelaxed() && filesIt != filesToCheck.constEnd())
452 #else
453 while (!abort.load() && filesIt != filesToCheck.constEnd())
454 #endif
455 {
456 fileInfo.setFile(path + QDir::separator() + *filesIt);
457 ++filesIt;
458 fetch(fileInfo, base, firstTime, updatedFiles, path);
459 }
460 if (!updatedFiles.isEmpty())
461 emit updates(path, updatedFiles);
462 emit directoryLoaded(path);
463 }
464
fetch(const QFileInfo & fileInfo,QElapsedTimer & base,bool & firstTime,QVector<QPair<QString,QFileInfo>> & updatedFiles,const QString & path)465 void FileInfoGatherer::fetch(const QFileInfo &fileInfo, QElapsedTimer &base, bool &firstTime, QVector<QPair<QString, QFileInfo> > &updatedFiles, const QString &path) {
466 updatedFiles.append(QPair<QString, QFileInfo>(fileInfo.fileName(), fileInfo));
467 QElapsedTimer current;
468 current.start();
469 if ((firstTime && updatedFiles.count() > 100) || base.msecsTo(current) > 1000) {
470 emit updates(path, updatedFiles);
471 updatedFiles.clear();
472 base = current;
473 firstTime = false;
474 }
475 }
476 /** \endcond */
477