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