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