File indexing completed on 2024-05-19 04:56:02

0001 /**
0002  * \file fileinfogatherer.cpp
0003  * \cond
0004  * Taken from Qt Git, revision e73bd4a
0005  * qtbase/src/widgets/dialogs/qfileinfogatherer.cpp
0006  * Adapted for Kid3 with the following changes:
0007  * - Remove Q prefix from class names
0008  * - Remove QT_..._CONFIG, QT_..._NAMESPACE, Q_..._EXPORT...
0009  * - Allow compilation with Qt versions < 5.7
0010  * - Remove moc includes
0011  * - Remove dependencies to Qt5::Widgets
0012  */
0013 /****************************************************************************
0014 **
0015 ** Copyright (C) 2016 The Qt Company Ltd.
0016 ** Contact: https://www.qt.io/licensing/
0017 **
0018 ** This file is part of the QtWidgets module of the Qt Toolkit.
0019 **
0020 ** $QT_BEGIN_LICENSE:LGPL$
0021 ** Commercial License Usage
0022 ** Licensees holding valid commercial Qt licenses may use this file in
0023 ** accordance with the commercial license agreement provided with the
0024 ** Software or, alternatively, in accordance with the terms contained in
0025 ** a written agreement between you and The Qt Company. For licensing terms
0026 ** and conditions see https://www.qt.io/terms-conditions. For further
0027 ** information use the contact form at https://www.qt.io/contact-us.
0028 **
0029 ** GNU Lesser General Public License Usage
0030 ** Alternatively, this file may be used under the terms of the GNU Lesser
0031 ** General Public License version 3 as published by the Free Software
0032 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
0033 ** packaging of this file. Please review the following information to
0034 ** ensure the GNU Lesser General Public License version 3 requirements
0035 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
0036 **
0037 ** GNU General Public License Usage
0038 ** Alternatively, this file may be used under the terms of the GNU
0039 ** General Public License version 2.0 or (at your option) the GNU General
0040 ** Public license version 3 or any later version approved by the KDE Free
0041 ** Qt Foundation. The licenses are as published by the Free Software
0042 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
0043 ** included in the packaging of this file. Please review the following
0044 ** information to ensure the GNU General Public License requirements will
0045 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
0046 ** https://www.gnu.org/licenses/gpl-3.0.html.
0047 **
0048 ** $QT_END_LICENSE$
0049 **
0050 ****************************************************************************/
0051 
0052 #include "fileinfogatherer_p.h"
0053 #include <qdebug.h>
0054 #include <qdiriterator.h>
0055 #ifndef Q_OS_WIN
0056 #  include <unistd.h>
0057 #  include <sys/types.h>
0058 #endif
0059 #if defined(Q_OS_VXWORKS)
0060 #  include "qplatformdefs.h"
0061 #endif
0062 #include "abstractfiledecorationprovider.h"
0063 
0064 #ifdef Q_OS_WIN
0065 #include <windows.h>
0066 
0067 /**
0068  * Check if path is a drive which could cause an insert disk dialog to pop up.
0069  *
0070  * This method should be used before calling QFileInfo::permissions(),
0071  * or QFileInfo::isReadable() on Windows.
0072  * The bug has been reported for Windows 7 32-bit and could be reproduced with
0073  * Windows XP. To trigger the bug, a CD has to be inserted and then removed once
0074  * before fetching the root directory with a file system model. See
0075  * https://forum.qt.io/topic/34799/checking-is-a-drive-is-readable-in-qt-pops-up-a-no-disk-error-in-windows-7
0076  *
0077  * @param path drive path, e.g. "D:/"
0078  * @return true if path is for a drive and getting volume information fails.
0079  */
0080 bool ExtendedInformation::isInvalidDrive(const QString &path)
0081 {
0082     // Windows drive nodes are queried with paths like "D:/", check if path is
0083     // a drive letter followed by a colon.
0084     const int pathLen = path.length();
0085     if (pathLen < 2 || pathLen > 3 || path.at(1) != QLatin1Char(':') ||
0086         !path.at(0).isLetter())
0087         return false;
0088 
0089     const DWORD VOLUME_NAME_SIZE = 255;
0090     const DWORD FILE_SYSTEM_NAME_SIZE = 255;
0091     LPCWSTR rootPathName = (LPCWSTR)path.utf16();
0092     UCHAR fileSystemNameBuffer[255], volumeNameBuffer[255];
0093     DWORD volumeSerialNumber, maximumComponentLength, fileSystemFlags;
0094 
0095     BOOL bSuccess = ::GetVolumeInformationW(
0096         rootPathName,
0097         (LPWSTR)volumeNameBuffer,
0098         VOLUME_NAME_SIZE,
0099         &volumeSerialNumber,
0100         &maximumComponentLength,
0101         &fileSystemFlags,
0102         (LPWSTR)fileSystemNameBuffer,
0103         FILE_SYSTEM_NAME_SIZE
0104     );
0105 
0106     return !bSuccess;
0107 }
0108 #endif // Q_OS_WIN
0109 
0110 #ifdef QT_BUILD_INTERNAL
0111 static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false);
0112 void qt_test_resetFetchedRoot()
0113 {
0114     fetchedRoot.store(false);
0115 }
0116 
0117 bool qt_test_isFetchedRoot()
0118 {
0119     return fetchedRoot.load();
0120 }
0121 #endif
0122 
0123 static QString translateDriveName(const QFileInfo &drive)
0124 {
0125     QString driveName = drive.absoluteFilePath();
0126 #ifdef Q_OS_WIN
0127     if (driveName.startsWith(QLatin1Char('/'))) // UNC host
0128         return drive.fileName();
0129     if (driveName.endsWith(QLatin1Char('/')))
0130         driveName.chop(1);
0131 #endif // Q_OS_WIN
0132     return driveName;
0133 }
0134 
0135 /*!
0136     Creates thread
0137 */
0138 FileInfoGatherer::FileInfoGatherer(QObject *parent)
0139     : QThread(parent), abort(false),
0140 #ifndef QT_NO_FILESYSTEMWATCHER
0141       watcher(nullptr),
0142 #endif
0143 #ifdef Q_OS_WIN
0144       m_resolveSymlinks(true),
0145 #endif
0146       m_decorationProvider(Q_NULLPTR)
0147 {
0148 #ifndef QT_NO_FILESYSTEMWATCHER
0149     watcher = new QFileSystemWatcher(this);
0150     connect(watcher, SIGNAL(directoryChanged(QString)), this, SLOT(list(QString)));
0151     connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT(updateFile(QString)));
0152 
0153 #  if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
0154     const QVariant listener = watcher->property("_q_driveListener");
0155     if (listener.canConvert<QObject *>()) {
0156         if (QObject *driveListener = listener.value<QObject *>()) {
0157             connect(driveListener, SIGNAL(driveAdded()), this, SLOT(driveAdded()));
0158             connect(driveListener, SIGNAL(driveRemoved()), this, SLOT(driveRemoved()));
0159         }
0160     }
0161 #  endif // Q_OS_WIN && !Q_OS_WINRT
0162 #endif
0163     start(LowPriority);
0164 }
0165 
0166 /*!
0167     Destroys thread
0168 */
0169 FileInfoGatherer::~FileInfoGatherer()
0170 {
0171 #if QT_VERSION >= 0x050e00
0172   abort.storeRelaxed(true);
0173 #else
0174   abort.store(true);
0175 #endif
0176     QMutexLocker locker(&mutex);
0177     condition.wakeAll();
0178     locker.unlock();
0179     wait();
0180 }
0181 
0182 void FileInfoGatherer::setResolveSymlinks(bool enable)
0183 {
0184     Q_UNUSED(enable)
0185 #ifdef Q_OS_WIN
0186     m_resolveSymlinks = enable;
0187 #endif
0188 }
0189 
0190 void FileInfoGatherer::driveAdded()
0191 {
0192     fetchExtendedInformation(QString(), QStringList());
0193 }
0194 
0195 void FileInfoGatherer::driveRemoved()
0196 {
0197     QStringList drives;
0198     const QFileInfoList driveInfoList = QDir::drives();
0199     for (const QFileInfo &fi : driveInfoList)
0200         drives.append(translateDriveName(fi));
0201     newListOfFiles(QString(), drives);
0202 }
0203 
0204 bool FileInfoGatherer::resolveSymlinks() const
0205 {
0206 #ifdef Q_OS_WIN
0207     return m_resolveSymlinks;
0208 #else
0209     return false;
0210 #endif
0211 }
0212 
0213 void FileInfoGatherer::setDecorationProvider(AbstractFileDecorationProvider *provider)
0214 {
0215     m_decorationProvider = provider;
0216 }
0217 
0218 AbstractFileDecorationProvider *FileInfoGatherer::decorationProvider() const
0219 {
0220     return m_decorationProvider;
0221 }
0222 
0223 /*!
0224     Fetch extended information for all \a files in \a path
0225 
0226     \sa updateFile(), update(), resolvedName()
0227 */
0228 void FileInfoGatherer::fetchExtendedInformation(const QString &path, const QStringList &files)
0229 {
0230     QMutexLocker locker(&mutex);
0231     // See if we already have this dir/file in our queue
0232     int loc = this->path.lastIndexOf(path);
0233     while (loc > 0)  {
0234         if (this->files.at(loc) == files) {
0235             return;
0236         }
0237         loc = this->path.lastIndexOf(path, loc - 1);
0238     }
0239     this->path.push(path);
0240     this->files.push(files);
0241     condition.wakeAll();
0242 
0243 #ifndef QT_NO_FILESYSTEMWATCHER
0244     if (files.isEmpty()
0245         && !path.isEmpty()
0246         && !path.startsWith(QLatin1String("//")) /*don't watch UNC path*/) {
0247         if (!watcher->directories().contains(path))
0248             watcher->addPath(path);
0249     }
0250 #endif
0251 }
0252 
0253 /*!
0254     Fetch extended information for all \a filePath
0255 
0256     \sa fetchExtendedInformation()
0257 */
0258 void FileInfoGatherer::updateFile(const QString &filePath)
0259 {
0260     QString dir = filePath.mid(0, filePath.lastIndexOf(QLatin1Char('/')));
0261     QString fileName = filePath.mid(dir.length() + 1);
0262     fetchExtendedInformation(dir, QStringList(fileName));
0263 }
0264 
0265 /*
0266     List all files in \a directoryPath
0267 
0268     \sa listed()
0269 */
0270 void FileInfoGatherer::clear()
0271 {
0272 #ifndef QT_NO_FILESYSTEMWATCHER
0273     QMutexLocker locker(&mutex);
0274     watcher->removePaths(watcher->files());
0275     watcher->removePaths(watcher->directories());
0276 #endif
0277 
0278     path.clear();
0279     files.clear();
0280 }
0281 
0282 /*
0283     Add a \a path to the watcher
0284 */
0285 void FileInfoGatherer::addPath(const QString &path)
0286 {
0287 #ifndef QT_NO_FILESYSTEMWATCHER
0288     QMutexLocker locker(&mutex);
0289     watcher->addPath(path);
0290 #else
0291     Q_UNUSED(path);
0292 #endif
0293 }
0294 
0295 /*
0296     Remove a \a path from the watcher
0297 
0298     \sa listed()
0299 */
0300 void FileInfoGatherer::removePath(const QString &path)
0301 {
0302 #ifndef QT_NO_FILESYSTEMWATCHER
0303     QMutexLocker locker(&mutex);
0304     watcher->removePath(path);
0305 #else
0306     Q_UNUSED(path);
0307 #endif
0308 }
0309 
0310 /*
0311     List all files in \a directoryPath
0312 
0313     \sa listed()
0314 */
0315 void FileInfoGatherer::list(const QString &directoryPath)
0316 {
0317     fetchExtendedInformation(directoryPath, QStringList());
0318 }
0319 
0320 /*
0321     Until aborted wait to fetch a directory or files
0322 */
0323 void FileInfoGatherer::run()
0324 {
0325     forever {
0326         QMutexLocker locker(&mutex);
0327 #if QT_VERSION >= 0x050e00
0328         while (!abort.loadRelaxed() && path.isEmpty())
0329             condition.wait(&mutex);
0330         if (abort.loadRelaxed())
0331             return;
0332 #else
0333         while (!abort.load() && path.isEmpty())
0334             condition.wait(&mutex);
0335         if (abort.load())
0336             return;
0337 #endif
0338 #if QT_VERSION >= 0x050700
0339         const QString thisPath = qAsConst(path).front();
0340 #else
0341         const auto constPath = path;
0342         const QString thisPath = constPath.front();
0343 #endif
0344         path.pop_front();
0345 #if QT_VERSION >= 0x050700
0346         const QStringList thisList = qAsConst(files).front();
0347 #else
0348         const auto constFiles = files;
0349         const QStringList thisList = constFiles.front();
0350 #endif
0351         files.pop_front();
0352         locker.unlock();
0353 
0354         getFileInfos(thisPath, thisList);
0355     }
0356 }
0357 
0358 ExtendedInformation FileInfoGatherer::getInfo(const QFileInfo &fileInfo) const
0359 {
0360     ExtendedInformation info(fileInfo);
0361     if (m_decorationProvider) {
0362         info.icon = m_decorationProvider->decoration(fileInfo);
0363         info.displayType = m_decorationProvider->type(fileInfo);
0364     } else {
0365         info.icon = QVariant();
0366         info.displayType = AbstractFileDecorationProvider::fileTypeDescription(fileInfo);
0367     }
0368 #ifndef QT_NO_FILESYSTEMWATCHER
0369     // ### Not ready to listen all modifications by default
0370     static const bool watchFiles = qEnvironmentVariableIsSet("QT_FILESYSTEMMODEL_WATCH_FILES");
0371     if (watchFiles) {
0372         if (!fileInfo.exists() && !fileInfo.isSymLink()) {
0373             watcher->removePath(fileInfo.absoluteFilePath());
0374         } else {
0375             if (const QString filePath = fileInfo.absoluteFilePath();
0376                 !filePath.isEmpty() && fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()
0377                 && !watcher->files().contains(filePath)) {
0378                 watcher->addPath(filePath);
0379             }
0380         }
0381     }
0382 #endif
0383 
0384 #ifdef Q_OS_WIN
0385     if (m_resolveSymlinks && info.isSymLink(/* ignoreNtfsSymLinks = */ true)) {
0386         QFileInfo resolvedInfo(fileInfo.symLinkTarget());
0387         resolvedInfo = QFileInfo(resolvedInfo.canonicalFilePath());
0388         if (resolvedInfo.exists()) {
0389             emit nameResolved(fileInfo.filePath(), resolvedInfo.fileName());
0390         }
0391     }
0392 #endif
0393     return info;
0394 }
0395 
0396 /*
0397     Get specific file info's, batch the files so update when we have 100
0398     items and every 200ms after that
0399  */
0400 void FileInfoGatherer::getFileInfos(const QString &path, const QStringList &files)
0401 {
0402     // List drives
0403     if (path.isEmpty()) {
0404 #ifdef QT_BUILD_INTERNAL
0405         fetchedRoot.store(true);
0406 #endif
0407         QFileInfoList infoList;
0408         if (files.isEmpty()) {
0409             infoList = QDir::drives();
0410         } else {
0411             infoList.reserve(files.count());
0412             for (const auto &file : files)
0413                 infoList << QFileInfo(file);
0414         }
0415         for (int i = infoList.count() - 1; i >= 0; --i) {
0416             QString driveName = translateDriveName(infoList.at(i));
0417             QVector<QPair<QString,QFileInfo> > updatedFiles;
0418             updatedFiles.append(QPair<QString,QFileInfo>(driveName, infoList.at(i)));
0419             emit updates(path, updatedFiles);
0420         }
0421         return;
0422     }
0423 
0424     QElapsedTimer base;
0425     base.start();
0426     QFileInfo fileInfo;
0427     bool firstTime = true;
0428     QVector<QPair<QString, QFileInfo> > updatedFiles;
0429     QStringList filesToCheck = files;
0430 
0431     QStringList allFiles;
0432     if (files.isEmpty()) {
0433         QDirIterator dirIt(path, QDir::AllEntries | QDir::System | QDir::Hidden);
0434 #if QT_VERSION >= 0x050e00
0435         while (!abort.loadRelaxed() && dirIt.hasNext())
0436 #else
0437         while (!abort.load() && dirIt.hasNext())
0438 #endif
0439         {
0440             dirIt.next();
0441             fileInfo = dirIt.fileInfo();
0442             allFiles.append(fileInfo.fileName());
0443             fetch(fileInfo, base, firstTime, updatedFiles, path);
0444         }
0445     }
0446     if (!allFiles.isEmpty())
0447         emit newListOfFiles(path, allFiles);
0448 
0449     QStringList::const_iterator filesIt = filesToCheck.constBegin();
0450 #if QT_VERSION >= 0x050e00
0451     while (!abort.loadRelaxed() && filesIt != filesToCheck.constEnd())
0452 #else
0453     while (!abort.load() && filesIt != filesToCheck.constEnd())
0454 #endif
0455     {
0456         fileInfo.setFile(path + QDir::separator() + *filesIt);
0457         ++filesIt;
0458         fetch(fileInfo, base, firstTime, updatedFiles, path);
0459     }
0460     if (!updatedFiles.isEmpty())
0461         emit updates(path, updatedFiles);
0462     emit directoryLoaded(path);
0463 }
0464 
0465 void FileInfoGatherer::fetch(const QFileInfo &fileInfo, QElapsedTimer &base, bool &firstTime, QVector<QPair<QString, QFileInfo> > &updatedFiles, const QString &path) {
0466     updatedFiles.append(QPair<QString, QFileInfo>(fileInfo.fileName(), fileInfo));
0467     QElapsedTimer current;
0468     current.start();
0469     if ((firstTime && updatedFiles.count() > 100) || base.msecsTo(current) > 1000) {
0470         emit updates(path, updatedFiles);
0471         updatedFiles.clear();
0472         base = current;
0473         firstTime = false;
0474     }
0475 }
0476 /** \endcond */