summaryrefslogtreecommitdiffstats
path: root/src/corelib/io/qfilesystemengine_unix.cpp
diff options
context:
space:
mode:
authorVolker Hilsheimer <volker.hilsheimer@qt.io>2019-10-31 17:20:13 +0100
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2020-01-30 06:14:56 +0100
commit601ce9e08aa92b273f1a6daf0bdbc67dbf9b4e5f (patch)
tree732d1d1c57a5ae93da1af115807838ff7e188a29 /src/corelib/io/qfilesystemengine_unix.cpp
parent7321a2c624d1a3bdc0825cf3d09a51643fe83d77 (diff)
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 <vitaly.fanaskov@qt.io>
Diffstat (limited to 'src/corelib/io/qfilesystemengine_unix.cpp')
-rw-r--r--src/corelib/io/qfilesystemengine_unix.cpp193
1 files changed, 193 insertions, 0 deletions
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 <QtCore/qoperatingsystemversion.h>
#include <QtCore/private/qcore_unix_p.h>
@@ -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)
{