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