1 /*
2 * Copyright (C) 2018 Roman Chistokhodov <freeslave93@gmail.com>
3 * This file is part of Phototonic Image Viewer.
4 *
5 * Phototonic is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * Phototonic is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with Phototonic. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <QtGlobal>
20 #include "Trashcan.h"
21
22 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) && !defined(Q_OS_DARWIN)
23
24 // Implementation for freedesktop systems adheres to https://specifications.freedesktop.org/trash-spec/trashspec-latest.html
25
26 #include <QDateTime>
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QFile>
30 #include <QStandardPaths>
31 #include <QStorageInfo>
32 #include <QTextStream>
33 #include <QUrl>
34
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 #include <unistd.h>
38 #include <fcntl.h>
39 #include <cerrno>
40
moveToTrashDir(const QString & filePath,const QDir & trashDir,QString & error,const QStorageInfo & nonHomeStorage)41 static Trash::Result moveToTrashDir(const QString& filePath, const QDir& trashDir, QString& error, const QStorageInfo& nonHomeStorage)
42 {
43 const QDir trashInfoDir = QDir(trashDir.filePath("info"));
44 const QDir trashFilesDir = QDir(trashDir.filePath("files"));
45 if (trashInfoDir.mkpath(".") && trashFilesDir.mkpath(".")) {
46 QFileInfo fileInfo(filePath);
47 QString fileName = fileInfo.fileName();
48 QString infoFileName = fileName + ".trashinfo";
49 int fd;
50 const int flag = O_CREAT | O_WRONLY | O_EXCL;
51 const int mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
52 for (unsigned int n = 2; trashFilesDir.exists(fileName) ||
53 ((fd = open(trashInfoDir.filePath(infoFileName).toUtf8().data(), flag, mode)) == -1 && errno == EEXIST); ++n) {
54 fileName = QString("%1.%2.%3").arg(fileInfo.baseName(), QString::number(n), fileInfo.completeSuffix());
55 infoFileName = fileName + ".trashinfo";
56 }
57 if (fd == -1) {
58 error = strerror(errno);
59 return Trash::Error;
60 }
61 const QString moveHere = trashFilesDir.filePath(fileName);
62 const QString deletionDate = QDateTime::currentDateTime().toString(Qt::ISODate);
63 const QString path = nonHomeStorage.isValid() ? QDir(nonHomeStorage.rootPath()).relativeFilePath(filePath) : filePath;
64 const QString escapedPath = QString::fromUtf8(QUrl::toPercentEncoding(path, "/"));
65 QFile infoFile;
66 if (infoFile.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) {
67 QTextStream out(&infoFile);
68 out << "[Trash Info]\nPath=" << escapedPath << "\nDeletionDate=" << deletionDate << '\n';
69 } else {
70 error = infoFile.errorString();
71 return Trash::Error;
72 }
73
74
75 if (QDir().rename(filePath, moveHere)) {
76 return Trash::Success;
77 } else {
78 error = QString("Could not rename %1 to %2").arg(filePath, moveHere);
79 return Trash::Error;
80 }
81 } else {
82 error = "Could not set up trash subdirectories";
83 return Trash::Error;
84 }
85 }
86
moveToTrash(const QString & path,QString & error,Trash::Options trashOptions)87 Trash::Result Trash::moveToTrash(const QString &path, QString &error, Trash::Options trashOptions)
88 {
89 if (path.isEmpty()) {
90 error = "Path is empty";
91 return Trash::Error;
92 }
93 const QString filePath = QFileInfo(path).absoluteFilePath();
94 const QStorageInfo filePathStorage(filePath);
95 if (!filePathStorage.isValid()) {
96 error = "Could not get device of the file being trashed";
97 return Trash::Error;
98 }
99 const QString homeDataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
100 const QDir homeDataDirectory(homeDataLocation);
101 if (homeDataLocation.isEmpty() || !homeDataDirectory.exists()) {
102 error = "Could not get home data folder";
103 return Trash::Error;
104 }
105
106 if (QStorageInfo(homeDataLocation) == filePathStorage || trashOptions == Trash::ForceDeletionToHomeTrash) {
107 const QDir homeTrashDirectory = QDir(homeDataDirectory.filePath("Trash"));
108 if (homeTrashDirectory.mkpath(".")) {
109 return moveToTrashDir(filePath, homeTrashDirectory, error, QStorageInfo());
110 } else {
111 error = "Could not ensure that home trash directory exists";
112 return Trash::Error;
113 }
114 } else {
115 const QDir topdir = QDir(filePathStorage.rootPath());
116 const QDir topdirTrash = QDir(topdir.filePath(".Trash"));
117 struct stat trashStat;
118 if (lstat(topdirTrash.path().toUtf8().data(), &trashStat) == 0) {
119 // should be a directory, not link, and have sticky bit
120 if (S_ISDIR(trashStat.st_mode) && !S_ISLNK(trashStat.st_mode) && (trashStat.st_mode & S_ISVTX)) {
121 const QString subdir = QString::number(getuid());
122 if (topdirTrash.mkpath(subdir)) {
123 return moveToTrashDir(filePath, QDir(topdirTrash.filePath(subdir)), error, filePathStorage);
124 }
125 }
126 }
127 // if we're still here, $topdir/.Trash does not exist or failed some check
128 QDir topdirUserTrash = QDir(topdir.filePath(QString(".Trash-%1").arg(getuid())));
129 if (topdirUserTrash.mkpath(".")) {
130 return moveToTrashDir(filePath, topdirUserTrash, error, filePathStorage);
131 }
132 error = "Could not find trash directory for the disk where the file resides";
133 return Trash::NeedsUserInput;
134 }
135 }
136
137 #elif defined(Q_OS_WIN)
138 #include <windows.h>
139 #include <shellapi.h>
140
moveToTrash(const QString & path,QString & error,Trash::Options trashOptions)141 Trash::Result Trash::moveToTrash(const QString &path, QString &error, Trash::Options trashOptions)
142 {
143 Q_UNUSED(trashOptions);
144 SHFILEOPSTRUCTW fileOp;
145 ZeroMemory(&fileOp, sizeof(fileOp));
146 fileOp.wFunc = FO_DELETE;
147 fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO;
148 std::wstring wFileName = path.toStdWString();
149 wFileName.push_back('\0');
150 wFileName.push_back('\0');
151 fileOp.pFrom = wFileName.c_str();
152
153 int r = SHFileOperation(&fileOp);
154 if (r != 0) {
155 // Unfortunately there's no adequate way to get message from SHFileOperation failure
156 error = QString("SHFileOperation failed with code %1").arg(r);
157 return Trash::Error;
158 }
159 return Trash::Success;
160 }
161 #else
162
moveToTrash(const QString & path,QString & error,Trash::Options trashOptions)163 Trash::Result Trash::moveToTrash(const QString &path, QString &error, Trash::Options trashOptions)
164 {
165 error = "Putting files into trashcan is not supported for this platform yet";
166 return Trash::Error;
167 }
168
169 #endif
170