Enable customizing the save statistics time interval
[qBittorrent.git] / src / base / utils / fs.cpp
blobdbcfafb74e43ef4d75abc2ea2b96f85020b76c98
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "fs.h"
32 #include <filesystem>
34 #if defined(Q_OS_WIN)
35 #include <memory>
36 #endif
38 #include <sys/stat.h>
39 #include <sys/types.h>
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
53 #include <QCoreApplication>
54 #include <QDateTime>
55 #include <QDebug>
56 #include <QDir>
57 #include <QDirIterator>
58 #include <QFile>
59 #include <QFileInfo>
60 #include <QRegularExpression>
61 #include <QStorageInfo>
63 #include "base/global.h"
64 #include "base/path.h"
66 /**
67 * This function will first check if there are only system cache files, e.g. `Thumbs.db`,
68 * `.DS_Store` and/or only temp files that end with '~', e.g. `filename~`.
69 * If they are the only files it will try to remove them and delete the folder.
70 * This action will be performed for each subfolder starting from the deepest folder.
71 * There is an inherent race condition here. A file might appear after it is checked
72 * that only the above mentioned "useless" files exist but before the whole folder is removed.
73 * In this case, the folder will not be removed but the "useless" files will be deleted.
75 bool Utils::Fs::smartRemoveEmptyFolderTree(const Path &path)
77 if (!path.exists())
78 return true;
80 const QStringList deleteFilesList =
82 // Windows
83 u"Thumbs.db"_s,
84 u"desktop.ini"_s,
85 // Linux
86 u".directory"_s,
87 // Mac OS
88 u".DS_Store"_s
91 // travel from the deepest folder and remove anything unwanted on the way out.
92 QStringList dirList(path.data() + u'/'); // get all sub directories paths
93 QDirIterator iter {path.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
94 while (iter.hasNext())
95 dirList << iter.next() + u'/';
96 // sort descending by directory depth
97 std::sort(dirList.begin(), dirList.end()
98 , [](const QString &l, const QString &r) { return l.count(u'/') > r.count(u'/'); });
100 for (const QString &p : asConst(dirList))
102 const QDir dir {p};
103 // A deeper folder may have not been removed in the previous iteration
104 // so don't remove anything from this folder either.
105 if (!dir.isEmpty(QDir::Dirs | QDir::NoDotAndDotDot))
106 continue;
108 const QStringList tmpFileList = dir.entryList(QDir::Files);
110 // deleteFilesList contains unwanted files, usually created by the OS
111 // temp files on linux usually end with '~', e.g. `filename~`
112 const bool hasOtherFiles = std::any_of(tmpFileList.cbegin(), tmpFileList.cend(), [&deleteFilesList](const QString &f)
114 return (!f.endsWith(u'~') && !deleteFilesList.contains(f, Qt::CaseInsensitive));
116 if (hasOtherFiles)
117 continue;
119 for (const QString &f : tmpFileList)
120 removeFile(Path(p + f));
122 // remove directory if empty
123 dir.rmdir(p);
126 return path.exists();
130 * Removes directory and its content recursively.
132 void Utils::Fs::removeDirRecursively(const Path &path)
134 if (!path.isEmpty())
135 QDir(path.data()).removeRecursively();
139 * Returns the size of a file.
140 * If the file is a folder, it will compute its size based on its content.
142 * Returns -1 in case of error.
144 qint64 Utils::Fs::computePathSize(const Path &path)
146 // Check if it is a file
147 const QFileInfo fi {path.data()};
148 if (!fi.exists()) return -1;
149 if (fi.isFile()) return fi.size();
151 // Compute folder size based on its content
152 qint64 size = 0;
153 QDirIterator iter {path.data(), (QDir::Files | QDir::Hidden | QDir::NoSymLinks), QDirIterator::Subdirectories};
154 while (iter.hasNext())
156 const QFileInfo fileInfo = iter.nextFileInfo();
157 size += fileInfo.size();
159 return size;
163 * Makes deep comparison of two files to make sure they are identical.
164 * The point is about the file contents. If the files do not exist then
165 * the paths refers to nothing and therefore we cannot say the files are same
166 * (because there are no files!)
168 bool Utils::Fs::sameFiles(const Path &path1, const Path &path2)
170 QFile f1 {path1.data()};
171 QFile f2 {path2.data()};
173 if (!f1.exists() || !f2.exists())
174 return false;
175 if (path1 == path2)
176 return true;
177 if (f1.size() != f2.size())
178 return false;
179 if (!f1.open(QIODevice::ReadOnly) || !f2.open(QIODevice::ReadOnly))
180 return false;
182 const int readSize = 1024 * 1024; // 1 MiB
183 while (!f1.atEnd() && !f2.atEnd())
185 if (f1.read(readSize) != f2.read(readSize))
186 return false;
188 return true;
191 QString Utils::Fs::toValidFileName(const QString &name, const QString &pad)
193 const QRegularExpression regex {u"[\\\\/:?\"*<>|]+"_s};
195 QString validName = name.trimmed();
196 validName.replace(regex, pad);
198 return validName;
201 Path Utils::Fs::toValidPath(const QString &name, const QString &pad)
203 const QRegularExpression regex {u"[:?\"*<>|]+"_s};
205 QString validPathStr = name;
206 validPathStr.replace(regex, pad);
208 return Path(validPathStr);
211 qint64 Utils::Fs::freeDiskSpaceOnPath(const Path &path)
213 return QStorageInfo(path.data()).bytesAvailable();
216 Path Utils::Fs::tempPath()
218 static const Path path = Path(QDir::tempPath()) / Path(u".qBittorrent"_s);
219 mkdir(path);
220 return path;
223 bool Utils::Fs::isRegularFile(const Path &path)
225 std::error_code ec;
226 return std::filesystem::is_regular_file(path.toStdFsPath(), ec);
229 bool Utils::Fs::isNetworkFileSystem(const Path &path)
231 #if defined Q_OS_HAIKU
232 return false;
233 #elif defined(Q_OS_WIN)
234 const std::wstring pathW = path.toString().toStdWString();
235 auto volumePath = std::make_unique<wchar_t[]>(pathW.length() + 1);
236 if (!::GetVolumePathNameW(pathW.c_str(), volumePath.get(), static_cast<DWORD>(pathW.length() + 1)))
237 return false;
238 return (::GetDriveTypeW(volumePath.get()) == DRIVE_REMOTE);
239 #else
240 const QString file = (path.toString() + u"/.");
241 struct statfs buf {};
242 if (statfs(file.toLocal8Bit().constData(), &buf) != 0)
243 return false;
245 #if defined(Q_OS_OPENBSD)
246 return ((strncmp(buf.f_fstypename, "cifs", sizeof(buf.f_fstypename)) == 0)
247 || (strncmp(buf.f_fstypename, "nfs", sizeof(buf.f_fstypename)) == 0)
248 || (strncmp(buf.f_fstypename, "smbfs", sizeof(buf.f_fstypename)) == 0));
249 #else
250 // Magic number reference:
251 // https://github.com/coreutils/coreutils/blob/master/src/stat.c
252 switch (static_cast<quint32>(buf.f_type))
254 case 0x0000517B: // SMB
255 case 0x0000564C: // NCP
256 case 0x00006969: // NFS
257 case 0x00C36400: // CEPH
258 case 0x01161970: // GFS
259 case 0x013111A8: // IBRIX
260 case 0x0BD00BD0: // LUSTRE
261 case 0x19830326: // FHGFS
262 case 0x47504653: // GPFS
263 case 0x50495045: // PIPEFS
264 case 0x5346414F: // AFS
265 case 0x61636673: // ACFS
266 case 0x61756673: // AUFS
267 case 0x65735543: // FUSECTL
268 case 0x65735546: // FUSEBLK
269 case 0x6B414653: // KAFS
270 case 0x6E667364: // NFSD
271 case 0x73757245: // CODA
272 case 0x7461636F: // OCFS2
273 case 0x786F4256: // VBOXSF
274 case 0x794C7630: // OVERLAYFS
275 case 0x7C7C6673: // PRL_FS
276 case 0xA501FCF5: // VXFS
277 case 0xAAD7AAEA: // OVERLAYFS
278 case 0xBACBACBC: // VMHGFS
279 case 0xBEEFDEAD: // SNFS
280 case 0xFE534D42: // SMB2
281 case 0xFF534D42: // CIFS
282 return true;
283 default:
284 break;
287 return false;
288 #endif
289 #endif
292 bool Utils::Fs::copyFile(const Path &from, const Path &to)
294 if (!from.exists())
295 return false;
297 if (!mkpath(to.parentPath()))
298 return false;
300 return QFile::copy(from.data(), to.data());
303 bool Utils::Fs::renameFile(const Path &from, const Path &to)
305 return QFile::rename(from.data(), to.data());
309 * Removes the file with the given filePath.
311 * This function will try to fix the file permissions before removing it.
313 nonstd::expected<void, QString> Utils::Fs::removeFile(const Path &path)
315 QFile file {path.data()};
316 if (file.remove())
317 return {};
319 if (!file.exists())
320 return {};
322 // Make sure we have read/write permissions
323 file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
324 if (file.remove())
325 return {};
327 return nonstd::make_unexpected(file.errorString());
330 nonstd::expected<void, QString> Utils::Fs::moveFileToTrash(const Path &path)
332 QFile file {path.data()};
333 if (file.moveToTrash())
334 return {};
336 if (!file.exists())
337 return {};
339 // Make sure we have read/write permissions
340 file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
341 if (file.moveToTrash())
342 return {};
344 const QString errorMessage = file.errorString();
345 return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error"));
349 bool Utils::Fs::isReadable(const Path &path)
351 return QFileInfo(path.data()).isReadable();
354 bool Utils::Fs::isWritable(const Path &path)
356 return QFileInfo(path.data()).isWritable();
359 QDateTime Utils::Fs::lastModified(const Path &path)
361 return QFileInfo(path.data()).lastModified();
364 bool Utils::Fs::isDir(const Path &path)
366 return QFileInfo(path.data()).isDir();
369 Path Utils::Fs::toAbsolutePath(const Path &path)
371 return Path(QFileInfo(path.data()).absoluteFilePath());
374 Path Utils::Fs::toCanonicalPath(const Path &path)
376 return Path(QFileInfo(path.data()).canonicalFilePath());
379 Path Utils::Fs::homePath()
381 return Path(QDir::homePath());
384 bool Utils::Fs::mkdir(const Path &dirPath)
386 return QDir().mkdir(dirPath.data());
389 bool Utils::Fs::mkpath(const Path &dirPath)
391 return QDir().mkpath(dirPath.data());
394 bool Utils::Fs::rmdir(const Path &dirPath)
396 return QDir().rmdir(dirPath.data());