From 601ce9e08aa92b273f1a6daf0bdbc67dbf9b4e5f Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Thu, 31 Oct 2019 17:20:13 +0100 Subject: Implement moving of a single file system entry to the trash This implements the operation for Windows, macOS, and Unix, for now only as a private API (since QFileSystemEngine is private). This adds the capability as a testable function; public API to be agreed on and added in a separate commit. The Unix implementation follows the freedesktop.org specification [1] version 1.0. [1] https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html On macOS and Windows, native APIs are used, with each having some limitations: * on macOS, the file in the trash won't have a "put back" option, as we don't use Finder automation, for the reasons provided in the comments * on Windows, we might not be able to use the modern IFileOperation API, e.g. if Qt is built with mingw which doesn't seem to provide the interface definition; the fallback doesn't provide access to the file name in the trash The test case creates files and directories, and moves them to the trash. As part of the cleanup routine, it deletes all file system entries created. If run on Windows without IFileOperations support, this will add a file in the trash for each test run, filling up hard drive space. Task-number: QTBUG-47703 Change-Id: I5f5f4e578be2f45d7da84f70a03acbe1a12a1231 Reviewed-by: Vitaly Fanaskov --- src/corelib/io/qfilesystemengine_unix.cpp | 193 ++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) (limited to 'src/corelib/io/qfilesystemengine_unix.cpp') diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp index 3bbebc7fe9..16901be187 100644 --- a/src/corelib/io/qfilesystemengine_unix.cpp +++ b/src/corelib/io/qfilesystemengine_unix.cpp @@ -42,6 +42,8 @@ #include "qplatformdefs.h" #include "qfilesystemengine_p.h" #include "qfile.h" +#include "qstorageinfo.h" +#include "qtextstream.h" #include #include @@ -1197,6 +1199,197 @@ bool QFileSystemEngine::createLink(const QFileSystemEntry &source, const QFileSy return false; } +#ifndef Q_OS_DARWIN +/* + Implementing as per https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html +*/ +static QString freeDesktopTrashLocation(const QString &sourcePath) +{ + auto makeTrashDir = [](const QDir &topDir, const QString &trashDir) -> QString { + auto ownerPerms = QFileDevice::ReadOwner + | QFileDevice::WriteOwner + | QFileDevice::ExeOwner; + QString targetDir = topDir.filePath(trashDir); + if (topDir.mkdir(trashDir)) + QFile::setPermissions(targetDir, ownerPerms); + if (QFileInfo(targetDir).isDir()) + return targetDir; + return QString(); + }; + auto isSticky = [](const QFileInfo &fileInfo) -> bool { + struct stat st; + if (stat(QFile::encodeName(fileInfo.absoluteFilePath()).constData(), &st) == 0) + return st.st_mode & S_ISVTX; + + return false; + }; + + QString trash; + const QLatin1String dotTrash(".Trash"); + const QStorageInfo sourceStorage(sourcePath); + const QStorageInfo homeStorage(QDir::home()); + // We support trashing of files outside the users home partition + if (sourceStorage != homeStorage) { + QDir topDir(sourceStorage.rootPath()); + /* + Method 1: + "An administrator can create an $topdir/.Trash directory. The permissions on this + directories should permit all users who can trash files at all to write in it; + and the “sticky bit” in the permissions must be set, if the file system supports + it. + When trashing a file from a non-home partition/device, an implementation + (if it supports trashing in top directories) MUST check for the presence + of $topdir/.Trash." + */ + const QString userID = QString::number(::getuid()); + if (topDir.cd(dotTrash)) { + const QFileInfo trashInfo(topDir.path()); + + // we MUST check that the sticky bit is set, and that it is not a symlink + if (trashInfo.isSymLink()) { + // we SHOULD report the failed check to the administrator + qCritical("Warning: '%s' is a symlink to '%s'", + trashInfo.absoluteFilePath().toLocal8Bit().constData(), + trashInfo.symLinkTarget().toLatin1().constData()); + } else if (!isSticky(trashInfo)) { + // we SHOULD report the failed check to the administrator + qCritical("Warning: '%s' doesn't have sticky bit set!", + trashInfo.absoluteFilePath().toLocal8Bit().constData()); + } else if (trashInfo.isDir()) { + /* + "If the directory exists and passes the checks, a subdirectory of the + $topdir/.Trash directory is to be used as the user's trash directory + for this partition/device. The name of this subdirectory is the numeric + identifier of the current user ($topdir/.Trash/$uid). + When trashing a file, if this directory does not exist for the current user, + the implementation MUST immediately create it, without any warnings or + delays for the user." + */ + trash = makeTrashDir(topDir, userID); + } + } + /* + Method 2: + "If an $topdir/.Trash directory is absent, an $topdir/.Trash-$uid directory is to be + used as the user's trash directory for this device/partition. [...] When trashing a + file, if an $topdir/.Trash-$uid directory does not exist, the implementation MUST + immediately create it, without any warnings or delays for the user." + */ + if (trash.isEmpty()) { + topDir = QDir(sourceStorage.rootPath()); + const QString userTrashDir = dotTrash + QLatin1Char('-') + userID; + trash = makeTrashDir(topDir, userTrashDir); + } + } + /* + "If both (1) and (2) fail [...], the implementation MUST either trash the + file into the user's “home trash” or refuse to trash it." + + We trash the file into the user's home trash. + */ + if (trash.isEmpty()) { + QDir topDir = QDir::home(); + trash = makeTrashDir(topDir, dotTrash); + if (!QFileInfo(trash).isDir()) { + qWarning("Unable to establish trash directory %s in %s", + dotTrash.latin1(), topDir.path().toLocal8Bit().constData()); + } + } + + return trash; +} + +//static +bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, + QFileSystemEntry &newLocation, QSystemError &error) +{ + const QFileInfo sourceInfo(source.filePath()); + if (!sourceInfo.exists()) { + error = QSystemError(ENOENT, QSystemError::StandardLibraryError); + return false; + } + const QString sourcePath = sourceInfo.absoluteFilePath(); + + QDir trashDir(freeDesktopTrashLocation(sourcePath)); + if (!trashDir.exists()) + return false; + /* + "A trash directory contains two subdirectories, named info and files." + */ + const QLatin1String filesDir("files"); + const QLatin1String infoDir("info"); + trashDir.mkdir(filesDir); + int savedErrno = errno; + trashDir.mkdir(infoDir); + if (!savedErrno) + savedErrno = errno; + if (!trashDir.exists(filesDir) || !trashDir.exists(infoDir)) { + error = QSystemError(savedErrno, QSystemError::StandardLibraryError); + return false; + } + /* + "The $trash/files directory contains the files and directories that were trashed. + The names of files in this directory are to be determined by the implementation; + the only limitation is that they must be unique within the directory. Even if a + file with the same name and location gets trashed many times, each subsequent + trashing must not overwrite a previous copy." + */ + const QString trashedName = sourceInfo.isDir() + ? QDir(sourcePath).dirName() + : sourceInfo.fileName(); + QString uniqueTrashedName = QLatin1Char('/') + trashedName; + QString infoFileName; + int counter = 0; + QFile infoFile; + do { + while (QFile::exists(trashDir.filePath(filesDir) + uniqueTrashedName)) { + ++counter; + uniqueTrashedName = QString(QLatin1String("/%1-%2")) + .arg(trashedName) + .arg(counter, 4, 10, QLatin1Char('0')); + } + /* + "The $trash/info directory contains an "information file" for every file and directory + in $trash/files. This file MUST have exactly the same name as the file or directory in + $trash/files, plus the extension ".trashinfo" + [...] + When trashing a file or directory, the implementation MUST create the corresponding + file in $trash/info first. Moreover, it MUST try to do this in an atomic fashion, + so that if two processes try to trash files with the same filename this will result + in two different trash files. On Unix-like systems this is done by generating a + filename, and then opening with O_EXCL. If that succeeds the creation was atomic + (at least on the same machine), if it fails you need to pick another filename." + */ + infoFileName = trashDir.filePath(infoDir) + + uniqueTrashedName + QLatin1String(".trashinfo"); + infoFile.setFileName(infoFileName); + } while (!infoFile.open(QIODevice::NewOnly | QIODevice::WriteOnly | QIODevice::Text)); + + const QString targetPath = trashDir.filePath(filesDir) + uniqueTrashedName; + const QFileSystemEntry target(targetPath); + + if (!renameFile(source, target, error)) { + infoFile.close(); + infoFile.remove(); + error = QSystemError(errno, QSystemError::StandardLibraryError); + return false; + } + + QTextStream out(&infoFile); +#if QT_CONFIG(textcodec) + out.setCodec("UTF-8"); +#endif + out << "[Trash Info]" << Qt::endl; + out << "Path=" << sourcePath << Qt::endl; + out << "DeletionDate=" + << QDateTime::currentDateTime().toString(QLatin1String("yyyy-MM-ddThh:mm:ss")) << Qt::endl; + infoFile.close(); + + newLocation = QFileSystemEntry(targetPath); + return true; +} +#endif + //static bool QFileSystemEngine::copyFile(const QFileSystemEntry &source, const QFileSystemEntry &target, QSystemError &error) { -- cgit v1.2.3 From 336b3bb0dd494409b89e1de480eb7204ff7b554e Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Fri, 31 Jan 2020 12:54:17 +0100 Subject: Address failing test case for internal implementation of moveToTrash This ammends 601ce9e08aa92b273f1a6daf0bdbc67dbf9b4e5f, which added a new test case for the internal Qt APIs. The test was not getting executed by coin as it wasn't included in the io.pro file, and trying to fix that generates link errors on Windows, since these internal APIs depend on other internal APIs. Short of bootstrapping much of QtCore into this test case, the only sensible option is to remove this test case again, and cover the testing when the public API is added in a follow up commit. At the same time, address those failures that were discovered on platforms that could build the test, and fix compilation on iOS platforms in Coin. Change-Id: Id31b43c9df9f205476c48bccb6b87c7a53ed15c5 Reviewed-by: Vitaly Fanaskov --- src/corelib/io/qfilesystemengine_unix.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) (limited to 'src/corelib/io/qfilesystemengine_unix.cpp') diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp index 16901be187..eed34086b4 100644 --- a/src/corelib/io/qfilesystemengine_unix.cpp +++ b/src/corelib/io/qfilesystemengine_unix.cpp @@ -1341,13 +1341,15 @@ bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, QString infoFileName; int counter = 0; QFile infoFile; - do { - while (QFile::exists(trashDir.filePath(filesDir) + uniqueTrashedName)) { - ++counter; - uniqueTrashedName = QString(QLatin1String("/%1-%2")) + auto makeUniqueTrashedName = [trashedName, &counter]() -> QString { + ++counter; + return QString(QLatin1String("/%1-%2")) .arg(trashedName) .arg(counter, 4, 10, QLatin1Char('0')); - } + }; + do { + while (QFile::exists(trashDir.filePath(filesDir) + uniqueTrashedName)) + uniqueTrashedName = makeUniqueTrashedName(); /* "The $trash/info directory contains an "information file" for every file and directory in $trash/files. This file MUST have exactly the same name as the file or directory in @@ -1363,15 +1365,21 @@ bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, infoFileName = trashDir.filePath(infoDir) + uniqueTrashedName + QLatin1String(".trashinfo"); infoFile.setFileName(infoFileName); - } while (!infoFile.open(QIODevice::NewOnly | QIODevice::WriteOnly | QIODevice::Text)); + if (!infoFile.open(QIODevice::NewOnly | QIODevice::WriteOnly | QIODevice::Text)) + uniqueTrashedName = makeUniqueTrashedName(); + } while (!infoFile.isOpen()); const QString targetPath = trashDir.filePath(filesDir) + uniqueTrashedName; const QFileSystemEntry target(targetPath); + /* + We might fail to rename if source and target are on different file systems. + In that case, we don't try further, i.e. copying and removing the original + is usually not what the user would expect to happen. + */ if (!renameFile(source, target, error)) { infoFile.close(); infoFile.remove(); - error = QSystemError(errno, QSystemError::StandardLibraryError); return false; } -- cgit v1.2.3 From f8e39a66569b27bb45cc7a49508dba29038122a4 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Tue, 4 Feb 2020 14:47:28 +0100 Subject: Fix build of bootstrapped tools with clang 10 Building of qmake fails with clang 10 due to QStorageInfo symbols not being resolved. Since bootstrapped tools don't need this functionality, we can remove the respective code and make the function fail as "not implemented". Change-Id: I48bbbd822e4f70630d903e5caead1a08fe4f13a8 Reviewed-by: Robert Loehning --- src/corelib/io/qfilesystemengine_unix.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/corelib/io/qfilesystemengine_unix.cpp') diff --git a/src/corelib/io/qfilesystemengine_unix.cpp b/src/corelib/io/qfilesystemengine_unix.cpp index eed34086b4..eaf4e2d9af 100644 --- a/src/corelib/io/qfilesystemengine_unix.cpp +++ b/src/corelib/io/qfilesystemengine_unix.cpp @@ -1203,6 +1203,9 @@ bool QFileSystemEngine::createLink(const QFileSystemEntry &source, const QFileSy /* Implementing as per https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html */ + +// bootstrapped tools don't need this, and we don't want QStorageInfo +#ifndef QT_BOOTSTRAPPED static QString freeDesktopTrashLocation(const QString &sourcePath) { auto makeTrashDir = [](const QDir &topDir, const QString &trashDir) -> QString { @@ -1298,11 +1301,18 @@ static QString freeDesktopTrashLocation(const QString &sourcePath) return trash; } +#endif // QT_BOOTSTRAPPED //static bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, QFileSystemEntry &newLocation, QSystemError &error) { +#ifdef QT_BOOTSTRAPPED + Q_UNUSED(source); + Q_UNUSED(newLocation); + error = QSystemError(ENOSYS, QSystemError::StandardLibraryError); + return false; +#else const QFileInfo sourceInfo(source.filePath()); if (!sourceInfo.exists()) { error = QSystemError(ENOENT, QSystemError::StandardLibraryError); @@ -1395,8 +1405,9 @@ bool QFileSystemEngine::moveFileToTrash(const QFileSystemEntry &source, newLocation = QFileSystemEntry(targetPath); return true; +#endif // QT_BOOTSTRAPPED } -#endif +#endif // Q_OS_DARWIN //static bool QFileSystemEngine::copyFile(const QFileSystemEntry &source, const QFileSystemEntry &target, QSystemError &error) -- cgit v1.2.3