1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2012  Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program 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 this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL".  If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "fs.h"
30 
31 #include <cerrno>
32 #include <cstring>
33 
34 #if defined(Q_OS_WIN)
35 #include <memory>
36 #endif
37 
38 #include <sys/stat.h>
39 #include <sys/types.h>
40 
41 #if defined(Q_OS_WIN)
42 #include <Windows.h>
43 #elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
44 #include <sys/param.h>
45 #include <sys/mount.h>
46 #elif defined(Q_OS_HAIKU)
47 #include <kernel/fs_info.h>
48 #else
49 #include <sys/vfs.h>
50 #include <unistd.h>
51 #endif
52 
53 #include <QDebug>
54 #include <QDir>
55 #include <QDirIterator>
56 #include <QFile>
57 #include <QFileInfo>
58 #include <QMimeDatabase>
59 #include <QStorageInfo>
60 #include <QRegularExpression>
61 
62 #include "base/bittorrent/common.h"
63 #include "base/global.h"
64 
toNativePath(const QString & path)65 QString Utils::Fs::toNativePath(const QString &path)
66 {
67     return QDir::toNativeSeparators(path);
68 }
69 
toUniformPath(const QString & path)70 QString Utils::Fs::toUniformPath(const QString &path)
71 {
72     return QDir::fromNativeSeparators(path);
73 }
74 
75 /**
76  * Returns the file extension part of a file name.
77  */
fileExtension(const QString & filename)78 QString Utils::Fs::fileExtension(const QString &filename)
79 {
80     const QString name = filename.endsWith(QB_EXT)
81         ? filename.chopped(QB_EXT.length())
82         : filename;
83     return QMimeDatabase().suffixForFileName(name);
84 }
85 
fileName(const QString & filePath)86 QString Utils::Fs::fileName(const QString &filePath)
87 {
88     const QString path = toUniformPath(filePath);
89     const int slashIndex = path.lastIndexOf('/');
90     if (slashIndex == -1)
91         return path;
92     return path.mid(slashIndex + 1);
93 }
94 
folderName(const QString & filePath)95 QString Utils::Fs::folderName(const QString &filePath)
96 {
97     const QString path = toUniformPath(filePath);
98     const int slashIndex = path.lastIndexOf('/');
99     if (slashIndex == -1)
100         return {};
101     return path.left(slashIndex);
102 }
103 
104 /**
105  * This function will first check if there are only system cache files, e.g. `Thumbs.db`,
106  * `.DS_Store` and/or only temp files that end with '~', e.g. `filename~`.
107  * If they are the only files it will try to remove them and delete the folder.
108  * This action will be performed for each subfolder starting from the deepest folder.
109  * There is an inherent race condition here. A file might appear after it is checked
110  * that only the above mentioned "useless" files exist but before the whole folder is removed.
111  * In this case, the folder will not be removed but the "useless" files will be deleted.
112  */
smartRemoveEmptyFolderTree(const QString & path)113 bool Utils::Fs::smartRemoveEmptyFolderTree(const QString &path)
114 {
115     if (path.isEmpty() || !QDir(path).exists())
116         return true;
117 
118     const QStringList deleteFilesList =
119     {
120         // Windows
121         QLatin1String("Thumbs.db"),
122         QLatin1String("desktop.ini"),
123         // Linux
124         QLatin1String(".directory"),
125         // Mac OS
126         QLatin1String(".DS_Store")
127     };
128 
129     // travel from the deepest folder and remove anything unwanted on the way out.
130     QStringList dirList(path + '/');  // get all sub directories paths
131     QDirIterator iter(path, (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories);
132     while (iter.hasNext())
133         dirList << iter.next() + '/';
134     // sort descending by directory depth
135     std::sort(dirList.begin(), dirList.end()
136               , [](const QString &l, const QString &r) { return l.count('/') > r.count('/'); });
137 
138     for (const QString &p : asConst(dirList))
139     {
140         const QDir dir(p);
141         // A deeper folder may have not been removed in the previous iteration
142         // so don't remove anything from this folder either.
143         if (!dir.isEmpty(QDir::Dirs | QDir::NoDotAndDotDot))
144             continue;
145 
146         const QStringList tmpFileList = dir.entryList(QDir::Files);
147 
148         // deleteFilesList contains unwanted files, usually created by the OS
149         // temp files on linux usually end with '~', e.g. `filename~`
150         const bool hasOtherFiles = std::any_of(tmpFileList.cbegin(), tmpFileList.cend(), [&deleteFilesList](const QString &f)
151         {
152             return (!f.endsWith('~') && !deleteFilesList.contains(f, Qt::CaseInsensitive));
153         });
154         if (hasOtherFiles)
155             continue;
156 
157         for (const QString &f : tmpFileList)
158             forceRemove(p + f);
159 
160         // remove directory if empty
161         dir.rmdir(p);
162     }
163 
164     return QDir(path).exists();
165 }
166 
167 /**
168  * Removes the file with the given filePath.
169  *
170  * This function will try to fix the file permissions before removing it.
171  */
forceRemove(const QString & filePath)172 bool Utils::Fs::forceRemove(const QString &filePath)
173 {
174     QFile f(filePath);
175     if (!f.exists())
176         return true;
177     // Make sure we have read/write permissions
178     f.setPermissions(f.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
179     // Remove the file
180     return f.remove();
181 }
182 
183 /**
184  * Removes directory and its content recursively.
185  */
removeDirRecursive(const QString & path)186 void Utils::Fs::removeDirRecursive(const QString &path)
187 {
188     if (!path.isEmpty())
189         QDir(path).removeRecursively();
190 }
191 
192 /**
193  * Returns the size of a file.
194  * If the file is a folder, it will compute its size based on its content.
195  *
196  * Returns -1 in case of error.
197  */
computePathSize(const QString & path)198 qint64 Utils::Fs::computePathSize(const QString &path)
199 {
200     // Check if it is a file
201     const QFileInfo fi(path);
202     if (!fi.exists()) return -1;
203     if (fi.isFile()) return fi.size();
204 
205     // Compute folder size based on its content
206     qint64 size = 0;
207     QDirIterator iter(path, QDir::Files | QDir::Hidden | QDir::NoSymLinks, QDirIterator::Subdirectories);
208     while (iter.hasNext())
209     {
210         iter.next();
211         size += iter.fileInfo().size();
212     }
213     return size;
214 }
215 
216 /**
217  * Makes deep comparison of two files to make sure they are identical.
218  */
sameFiles(const QString & path1,const QString & path2)219 bool Utils::Fs::sameFiles(const QString &path1, const QString &path2)
220 {
221     QFile f1(path1), f2(path2);
222     if (!f1.exists() || !f2.exists()) return false;
223     if (f1.size() != f2.size()) return false;
224     if (!f1.open(QIODevice::ReadOnly)) return false;
225     if (!f2.open(QIODevice::ReadOnly)) return false;
226 
227     const int readSize = 1024 * 1024;  // 1 MiB
228     while (!f1.atEnd() && !f2.atEnd())
229     {
230         if (f1.read(readSize) != f2.read(readSize))
231             return false;
232     }
233     return true;
234 }
235 
toValidFileSystemName(const QString & name,const bool allowSeparators,const QString & pad)236 QString Utils::Fs::toValidFileSystemName(const QString &name, const bool allowSeparators, const QString &pad)
237 {
238     const QRegularExpression regex(allowSeparators ? "[:?\"*<>|]+" : "[\\\\/:?\"*<>|]+");
239 
240     QString validName = name.trimmed();
241     validName.replace(regex, pad);
242     qDebug() << "toValidFileSystemName:" << name << "=>" << validName;
243 
244     return validName;
245 }
246 
isValidFileSystemName(const QString & name,const bool allowSeparators)247 bool Utils::Fs::isValidFileSystemName(const QString &name, const bool allowSeparators)
248 {
249     if (name.isEmpty()) return false;
250 
251 #if defined(Q_OS_WIN)
252     const QRegularExpression regex
253     {allowSeparators
254         ? QLatin1String("[:?\"*<>|]")
255         : QLatin1String("[\\\\/:?\"*<>|]")};
256 #elif defined(Q_OS_MACOS)
257     const QRegularExpression regex
258     {allowSeparators
259         ? QLatin1String("[\\0:]")
260         : QLatin1String("[\\0/:]")};
261 #else
262     const QRegularExpression regex
263     {allowSeparators
264         ? QLatin1String("[\\0]")
265         : QLatin1String("[\\0/]")};
266 #endif
267     return !name.contains(regex);
268 }
269 
freeDiskSpaceOnPath(const QString & path)270 qint64 Utils::Fs::freeDiskSpaceOnPath(const QString &path)
271 {
272     return QStorageInfo(path).bytesAvailable();
273 }
274 
branchPath(const QString & filePath,QString * removed)275 QString Utils::Fs::branchPath(const QString &filePath, QString *removed)
276 {
277     QString ret = toUniformPath(filePath);
278     if (ret.endsWith('/'))
279         ret.chop(1);
280     const int slashIndex = ret.lastIndexOf('/');
281     if (slashIndex >= 0)
282     {
283         if (removed)
284             *removed = ret.mid(slashIndex + 1);
285         ret = ret.left(slashIndex);
286     }
287     return ret;
288 }
289 
sameFileNames(const QString & first,const QString & second)290 bool Utils::Fs::sameFileNames(const QString &first, const QString &second)
291 {
292 #if defined(Q_OS_UNIX) || defined(Q_WS_QWS)
293     return QString::compare(first, second, Qt::CaseSensitive) == 0;
294 #else
295     return QString::compare(first, second, Qt::CaseInsensitive) == 0;
296 #endif
297 }
298 
expandPath(const QString & path)299 QString Utils::Fs::expandPath(const QString &path)
300 {
301     const QString ret = path.trimmed();
302     if (ret.isEmpty())
303         return ret;
304 
305     return QDir::cleanPath(ret);
306 }
307 
expandPathAbs(const QString & path)308 QString Utils::Fs::expandPathAbs(const QString &path)
309 {
310     return QDir(expandPath(path)).absolutePath();
311 }
312 
tempPath()313 QString Utils::Fs::tempPath()
314 {
315     static const QString path = QDir::tempPath() + "/.qBittorrent/";
316     QDir().mkdir(path);
317     return path;
318 }
319 
isRegularFile(const QString & path)320 bool Utils::Fs::isRegularFile(const QString &path)
321 {
322     struct ::stat st;
323     if (::stat(path.toUtf8().constData(), &st) != 0)
324     {
325         //  analyse erno and log the error
326         const auto err = errno;
327         qDebug("Could not get file stats for path '%s'. Error: %s"
328                , qUtf8Printable(path), qUtf8Printable(strerror(err)));
329         return false;
330     }
331 
332     return (st.st_mode & S_IFMT) == S_IFREG;
333 }
334 
335 #if !defined Q_OS_HAIKU
isNetworkFileSystem(const QString & path)336 bool Utils::Fs::isNetworkFileSystem(const QString &path)
337 {
338 #if defined(Q_OS_WIN)
339     const std::wstring pathW {path.toStdWString()};
340     auto volumePath = std::make_unique<wchar_t[]>(path.length() + 1);
341     if (!::GetVolumePathNameW(pathW.c_str(), volumePath.get(), (path.length() + 1)))
342         return false;
343 
344     return (::GetDriveTypeW(volumePath.get()) == DRIVE_REMOTE);
345 #elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
346     QString file = path;
347     if (!file.endsWith('/'))
348         file += '/';
349     file += '.';
350 
351     struct statfs buf {};
352     if (statfs(file.toLocal8Bit().constData(), &buf) != 0)
353         return false;
354 
355     // XXX: should we make sure HAVE_STRUCT_FSSTAT_F_FSTYPENAME is defined?
356     return ((strncmp(buf.f_fstypename, "cifs", sizeof(buf.f_fstypename)) == 0)
357         || (strncmp(buf.f_fstypename, "nfs", sizeof(buf.f_fstypename)) == 0)
358         || (strncmp(buf.f_fstypename, "smbfs", sizeof(buf.f_fstypename)) == 0));
359 #else // Q_OS_WIN
360     QString file = path;
361     if (!file.endsWith('/'))
362         file += '/';
363     file += '.';
364 
365     struct statfs buf {};
366     if (statfs(file.toLocal8Bit().constData(), &buf) != 0)
367         return false;
368 
369     // Magic number references:
370     // 1. /usr/include/linux/magic.h
371     // 2. https://github.com/coreutils/coreutils/blob/master/src/stat.c
372     switch (static_cast<unsigned int>(buf.f_type))
373     {
374     case 0xFF534D42:  // CIFS_MAGIC_NUMBER
375     case 0x6969:  // NFS_SUPER_MAGIC
376     case 0x517B:  // SMB_SUPER_MAGIC
377     case 0xFE534D42:  // S_MAGIC_SMB2
378         return true;
379     default:
380         return false;
381     }
382 #endif // Q_OS_WIN
383 }
384 #endif // Q_OS_HAIKU
385