/**************************************************************************** ** ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://www.qt.io/licensing. For further information ** use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ****************************************************************************/ #include "fileutils.h" #include "savefile.h" #include "hostosinfo.h" #include "qtcassert.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #endif QT_BEGIN_NAMESPACE QDebug operator<<(QDebug dbg, const Utils::FileName &c) { return dbg << c.toString(); } QT_END_NAMESPACE namespace Utils { /*! \class Utils::FileUtils \brief The FileUtils class contains file and directory related convenience functions. */ /*! Removes the directory \a filePath and its subdirectories recursively. \note The \a error parameter is optional. Returns whether the operation succeeded. */ bool FileUtils::removeRecursively(const FileName &filePath, QString *error) { QFileInfo fileInfo = filePath.toFileInfo(); if (!fileInfo.exists() && !fileInfo.isSymLink()) return true; QFile::setPermissions(filePath.toString(), fileInfo.permissions() | QFile::WriteUser); if (fileInfo.isDir()) { QDir dir(filePath.toString()); dir = dir.canonicalPath(); if (dir.isRoot()) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Refusing to remove root directory."); } return false; } if (dir.path() == QDir::home().canonicalPath()) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Refusing to remove your home directory."); } return false; } QStringList fileNames = dir.entryList(QDir::Files | QDir::Hidden | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot); foreach (const QString &fileName, fileNames) { if (!removeRecursively(FileName(filePath).appendPath(fileName), error)) return false; } if (!QDir::root().rmdir(dir.path())) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove directory \"%1\".") .arg(filePath.toUserOutput()); } return false; } } else { if (!QFile::remove(filePath.toString())) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove file \"%1\".") .arg(filePath.toUserOutput()); } return false; } } return true; } /*! Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath. \a tgtFilePath will contain the target directory, which will be created. Example usage: \code QString error; book ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error); if (!ok) qDebug() << error; \endcode This will copy the contents of /foo/bar into to the baz directory under /foo, which will be created in the process. \note The \a error parameter is optional. Returns whether the operation succeeded. */ bool FileUtils::copyRecursively(const FileName &srcFilePath, const FileName &tgtFilePath, QString *error) { QFileInfo srcFileInfo = srcFilePath.toFileInfo(); if (srcFileInfo.isDir()) { QDir targetDir(tgtFilePath.toString()); targetDir.cdUp(); if (!targetDir.mkdir(tgtFilePath.toFileInfo().fileName())) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Failed to create directory \"%1\".") .arg(tgtFilePath.toUserOutput()); } return false; } QDir sourceDir(srcFilePath.toString()); QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); foreach (const QString &fileName, fileNames) { FileName newSrcFilePath = srcFilePath; newSrcFilePath.appendPath(fileName); FileName newTgtFilePath = tgtFilePath; newTgtFilePath.appendPath(fileName); if (!copyRecursively(newSrcFilePath, newTgtFilePath, error)) return false; } } else { if (!QFile::copy(srcFilePath.toString(), tgtFilePath.toString())) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Could not copy file \"%1\" to \"%2\".") .arg(srcFilePath.toUserOutput(), tgtFilePath.toUserOutput()); } return false; } } return true; } /*! If \a filePath is a directory, the function will recursively check all files and return true if one of them is newer than \a timeStamp. If \a filePath is a single file, true will be returned if the file is newer than \a timeStamp. Returns whether at least one file in \a filePath has a newer date than \a timeStamp. */ bool FileUtils::isFileNewerThan(const FileName &filePath, const QDateTime &timeStamp) { QFileInfo fileInfo = filePath.toFileInfo(); if (!fileInfo.exists() || fileInfo.lastModified() >= timeStamp) return true; if (fileInfo.isDir()) { const QStringList dirContents = QDir(filePath.toString()) .entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); foreach (const QString &curFileName, dirContents) { if (isFileNewerThan(FileName(filePath).appendPath(curFileName), timeStamp)) return true; } } return false; } /*! Recursively resolves possibly present symlinks in \a filePath. Unlike QFileInfo::canonicalFilePath(), this function will still return the expected target file even if the symlink is dangling. \note Maximum recursion depth == 16. Returns the symlink target file path. */ FileName FileUtils::resolveSymlinks(const FileName &path) { QFileInfo f = path.toFileInfo(); int links = 16; while (links-- && f.isSymLink()) f.setFile(f.symLinkTarget()); if (links <= 0) return FileName(); return FileName::fromString(f.filePath()); } /*! Like QDir::toNativeSeparators(), but use prefix '~' instead of $HOME on unix systems when an absolute path is given. Returns the possibly shortened path with native separators. */ QString FileUtils::shortNativePath(const FileName &path) { if (HostOsInfo::isAnyUnixHost()) { const FileName home = FileName::fromString(QDir::cleanPath(QDir::homePath())); if (path.isChildOf(home)) { return QLatin1Char('~') + QDir::separator() + QDir::toNativeSeparators(path.relativeChildPath(home).toString()); } } return path.toUserOutput(); } QString FileUtils::fileSystemFriendlyName(const QString &name) { QString result = name; result.replace(QRegExp(QLatin1String("\\W")), QLatin1String("_")); result.replace(QRegExp(QLatin1String("_+")), QLatin1String("_")); // compact _ result.remove(QRegExp(QLatin1String("^_*"))); // remove leading _ result.remove(QRegExp(QLatin1String("_+$"))); // remove trailing _ if (result.isEmpty()) result = QLatin1String("unknown"); return result; } int FileUtils::indexOfQmakeUnfriendly(const QString &name, int startpos) { static QRegExp checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]")); return checkRegExp.indexIn(name, startpos); } QString FileUtils::qmakeFriendlyName(const QString &name) { QString result = name; // Remove characters that might trip up a build system (especially qmake): int pos = indexOfQmakeUnfriendly(result); while (pos >= 0) { result[pos] = QLatin1Char('_'); pos = indexOfQmakeUnfriendly(result, pos); } return fileSystemFriendlyName(result); } bool FileUtils::makeWritable(const FileName &path) { const QString fileName = path.toString(); return QFile::setPermissions(fileName, QFile::permissions(fileName) | QFile::WriteUser); } // makes sure that capitalization of directories is canonical on Windows. // This mimics the logic in QDeclarative_isFileCaseCorrect QString FileUtils::normalizePathName(const QString &name) { #ifdef Q_OS_WIN const QString nativeSeparatorName(QDir::toNativeSeparators(name)); const LPCTSTR nameC = reinterpret_cast(nativeSeparatorName.utf16()); // MinGW PIDLIST_ABSOLUTE file; HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL); if (FAILED(hr)) return name; TCHAR buffer[MAX_PATH]; if (!SHGetPathFromIDList(file, buffer)) return name; return QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast(buffer))); #else // Filesystem is case-insensitive only on Windows return name; #endif } bool FileUtils::isRelativePath(const QString &path) { if (path.startsWith(QLatin1Char('/'))) return false; if (HostOsInfo::isWindowsHost()) { if (path.startsWith(QLatin1Char('\\'))) return false; // Unlike QFileInfo, this won't accept a relative path with a drive letter. // Such paths result in a royal mess anyway ... if (path.length() >= 3 && path.at(1) == QLatin1Char(':') && path.at(0).isLetter() && (path.at(2) == QLatin1Char('/') || path.at(2) == QLatin1Char('\\'))) return false; } return true; } QString FileUtils::resolvePath(const QString &baseDir, const QString &fileName) { if (fileName.isEmpty()) return QString(); if (isAbsolutePath(fileName)) return QDir::cleanPath(fileName); return QDir::cleanPath(baseDir + QLatin1Char('/') + fileName); } QByteArray FileReader::fetchQrc(const QString &fileName) { QTC_ASSERT(fileName.startsWith(QLatin1Char(':')), return QByteArray()); QFile file(fileName); bool ok = file.open(QIODevice::ReadOnly); QTC_ASSERT(ok, qWarning() << fileName << "not there!"; return QByteArray()); return file.readAll(); } bool FileReader::fetch(const QString &fileName, QIODevice::OpenMode mode) { QTC_ASSERT(!(mode & ~(QIODevice::ReadOnly | QIODevice::Text)), return false); QFile file(fileName); if (!file.open(QIODevice::ReadOnly | mode)) { m_errorString = tr("Cannot open %1 for reading: %2").arg( QDir::toNativeSeparators(fileName), file.errorString()); return false; } m_data = file.readAll(); if (file.error() != QFile::NoError) { m_errorString = tr("Cannot read %1: %2").arg( QDir::toNativeSeparators(fileName), file.errorString()); return false; } return true; } bool FileReader::fetch(const QString &fileName, QIODevice::OpenMode mode, QString *errorString) { if (fetch(fileName, mode)) return true; if (errorString) *errorString = m_errorString; return false; } bool FileReader::fetch(const QString &fileName, QIODevice::OpenMode mode, QWidget *parent) { if (fetch(fileName, mode)) return true; if (parent) QMessageBox::critical(parent, tr("File Error"), m_errorString); return false; } FileSaverBase::FileSaverBase() : m_hasError(false) { } FileSaverBase::~FileSaverBase() { delete m_file; } bool FileSaverBase::finalize() { m_file->close(); setResult(m_file->error() == QFile::NoError); // We delete the object, so it is really closed even if it is a QTemporaryFile. delete m_file; m_file = 0; return !m_hasError; } bool FileSaverBase::finalize(QString *errStr) { if (finalize()) return true; if (errStr) *errStr = errorString(); return false; } bool FileSaverBase::finalize(QWidget *parent) { if (finalize()) return true; QMessageBox::critical(parent, tr("File Error"), errorString()); return false; } bool FileSaverBase::write(const char *data, int len) { if (m_hasError) return false; return setResult(m_file->write(data, len) == len); } bool FileSaverBase::write(const QByteArray &bytes) { if (m_hasError) return false; return setResult(m_file->write(bytes) == bytes.count()); } bool FileSaverBase::setResult(bool ok) { if (!ok && !m_hasError) { m_errorString = tr("Cannot write file %1. Disk full?").arg( QDir::toNativeSeparators(m_fileName)); m_hasError = true; } return ok; } bool FileSaverBase::setResult(QTextStream *stream) { stream->flush(); return setResult(stream->status() == QTextStream::Ok); } bool FileSaverBase::setResult(QDataStream *stream) { return setResult(stream->status() == QDataStream::Ok); } bool FileSaverBase::setResult(QXmlStreamWriter *stream) { return setResult(!stream->hasError()); } FileSaver::FileSaver(const QString &filename, QIODevice::OpenMode mode) { m_fileName = filename; if (mode & (QIODevice::ReadOnly | QIODevice::Append)) { m_file = new QFile(filename); m_isSafe = false; } else { m_file = new SaveFile(filename); m_isSafe = true; } if (!m_file->open(QIODevice::WriteOnly | mode)) { QString err = QFile::exists(filename) ? tr("Cannot overwrite file %1: %2") : tr("Cannot create file %1: %2"); m_errorString = err.arg(QDir::toNativeSeparators(filename), m_file->errorString()); m_hasError = true; } } bool FileSaver::finalize() { if (!m_isSafe) return FileSaverBase::finalize(); SaveFile *sf = static_cast(m_file); if (m_hasError) { if (sf->isOpen()) sf->rollback(); } else { setResult(sf->commit()); } delete sf; m_file = 0; return !m_hasError; } TempFileSaver::TempFileSaver(const QString &templ) : m_autoRemove(true) { QTemporaryFile *tempFile = new QTemporaryFile(); if (!templ.isEmpty()) tempFile->setFileTemplate(templ); tempFile->setAutoRemove(false); if (!tempFile->open()) { m_errorString = tr("Cannot create temporary file in %1: %2").arg( QDir::toNativeSeparators(QFileInfo(tempFile->fileTemplate()).absolutePath()), tempFile->errorString()); m_hasError = true; } m_file = tempFile; m_fileName = tempFile->fileName(); } TempFileSaver::~TempFileSaver() { delete m_file; m_file = 0; if (m_autoRemove) QFile::remove(m_fileName); } /*! \class Utils::FileName \brief The FileName class is a light-weight convenience class for filenames. On windows filenames are compared case insensitively. */ FileName::FileName() : QString() { } /// Constructs a FileName from \a info FileName::FileName(const QFileInfo &info) : QString(info.absoluteFilePath()) { } /// \returns a QFileInfo QFileInfo FileName::toFileInfo() const { return QFileInfo(*this); } /// \returns a QString for passing on to QString based APIs QString FileName::toString() const { return QString(*this); } /// \returns a QString to display to the user /// Converts the separators to the native format QString FileName::toUserOutput() const { return QDir::toNativeSeparators(toString()); } /// Find the parent directory of a given directory. /// Returns an empty FileName if the current directory is already /// a root level directory. /// \returns \a FileName with the last segment removed. FileName FileName::parentDir() const { const QString basePath = toString(); if (basePath.isEmpty()) return FileName(); const QDir base(basePath); if (base.isRoot()) return FileName(); const QString path = basePath + QLatin1String("/.."); const QString parent = QDir::cleanPath(path); return FileName::fromString(parent); } /// Constructs a FileName from \a fileName /// \a fileName is not checked for validity. FileName FileName::fromString(const QString &filename) { return FileName(filename); } /// Constructs a FileName from \a fileName /// \a fileName is not checked for validity. FileName FileName::fromLatin1(const QByteArray &filename) { return FileName(QString::fromLatin1(filename)); } /// Constructs a FileName from \a fileName /// \a fileName is only passed through QDir::cleanPath FileName FileName::fromUserInput(const QString &filename) { QString clean = QDir::cleanPath(filename); if (clean.startsWith(QLatin1String("~/"))) clean = QDir::homePath() + clean.mid(1); return FileName(clean); } FileName::FileName(const QString &string) : QString(string) { } bool FileName::operator==(const FileName &other) const { return QString::compare(*this, other, HostOsInfo::fileNameCaseSensitivity()) == 0; } bool FileName::operator!=(const FileName &other) const { return !(*this == other); } bool FileName::operator<(const FileName &other) const { return QString::compare(*this, other, HostOsInfo::fileNameCaseSensitivity()) < 0; } bool FileName::operator<=(const FileName &other) const { return QString::compare(*this, other, HostOsInfo::fileNameCaseSensitivity()) <= 0; } bool FileName::operator>(const FileName &other) const { return other < *this; } bool FileName::operator>=(const FileName &other) const { return other <= *this; } /// \returns whether FileName is a child of \a s bool FileName::isChildOf(const FileName &s) const { if (s.isEmpty()) return false; if (!QString::startsWith(s, HostOsInfo::fileNameCaseSensitivity())) return false; if (size() <= s.size()) return false; // s is root, '/' was already tested in startsWith if (s.QString::endsWith(QLatin1Char('/'))) return true; // s is a directory, next character should be '/' (/tmpdir is NOT a child of /tmp) return at(s.size()) == QLatin1Char('/'); } /// \overload bool FileName::isChildOf(const QDir &dir) const { return isChildOf(FileName::fromString(dir.absolutePath())); } /// \returns whether FileName endsWith \a s bool FileName::endsWith(const QString &s) const { return QString::endsWith(s, HostOsInfo::fileNameCaseSensitivity()); } /// \returns the relativeChildPath of FileName to parent if FileName is a child of parent /// \note returns a empty FileName if FileName is not a child of parent /// That is, this never returns a path starting with "../" FileName FileName::relativeChildPath(const FileName &parent) const { if (!isChildOf(parent)) return FileName(); return FileName(QString::mid(parent.size() + 1, -1)); } /// Appends \a s, ensuring a / between the parts FileName &FileName::appendPath(const QString &s) { if (!isEmpty() && !QString::endsWith(QLatin1Char('/'))) appendString(QLatin1Char('/')); appendString(s); return *this; } FileName &FileName::appendString(const QString &str) { QString::append(str); return *this; } FileName &FileName::appendString(QChar str) { QString::append(str); return *this; } static bool isDesktopFileManagerDrop(const QMimeData *d, QStringList *files = 0) { if (files) files->clear(); // Extract dropped files from Mime data. if (!d->hasUrls()) return false; const QList urls = d->urls(); if (urls.empty()) return false; // Try to find local files bool hasFiles = false; const QList::const_iterator cend = urls.constEnd(); for (QList::const_iterator it = urls.constBegin(); it != cend; ++it) { const QString fileName = it->toLocalFile(); if (!fileName.isEmpty()) { hasFiles = true; if (files) files->push_back(fileName); else break; // No result list, sufficient for checking } } return hasFiles; } FileDropSupport::FileDropSupport(QWidget *parentWidget) : QObject(parentWidget) { QTC_ASSERT(parentWidget, return); parentWidget->setAcceptDrops(true); parentWidget->installEventFilter(this); } bool FileDropSupport::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj) if (event->type() == QEvent::DragEnter) { auto dee = static_cast(event); if (isDesktopFileManagerDrop(dee->mimeData())) event->accept(); else event->ignore(); } else if (event->type() == QEvent::Drop) { auto de = static_cast(event); QStringList tempFiles; if (isDesktopFileManagerDrop(de->mimeData(), &tempFiles)) { event->accept(); bool needToScheduleEmit = m_files.isEmpty(); m_files.append(tempFiles); if (needToScheduleEmit) // otherwise we already have a timer pending QTimer::singleShot(0, this, SLOT(emitFilesDropped())); } else { event->ignore(); } } return false; } void FileDropSupport::emitFilesDropped() { QTC_ASSERT(!m_files.isEmpty(), return); emit filesDropped(m_files); m_files.clear(); } } // namespace Utils QT_BEGIN_NAMESPACE uint qHash(const Utils::FileName &a) { if (Utils::HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive) return qHash(a.toString().toUpper()); return qHash(a.toString()); } QT_END_NAMESPACE