aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRachel Powers <508861+Ryex@users.noreply.github.com>2023-02-08 23:42:13 -0700
committerRachel Powers <508861+Ryex@users.noreply.github.com>2023-03-20 14:56:32 -0700
commitc5bbe42b57075a4b428d0be1c1ca9f51701a1a7c (patch)
treeba932b5c97e98b2a9a3693acdab62eecd9eb5793
parentc9105e525e175ee8181ab0a6998d0e21526f116d (diff)
downloadPrismLauncher-c5bbe42b57075a4b428d0be1c1ca9f51701a1a7c.tar.gz
PrismLauncher-c5bbe42b57075a4b428d0be1c1ca9f51701a1a7c.tar.bz2
PrismLauncher-c5bbe42b57075a4b428d0be1c1ca9f51701a1a7c.zip
feat: reflink / Clone support!
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
-rw-r--r--launcher/FileSystem.cpp206
-rw-r--r--launcher/FileSystem.h141
2 files changed, 347 insertions, 0 deletions
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index c94770ee..7b7fc80b 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,6 +36,9 @@
*/
#include "FileSystem.h"
+#include <qdebug.h>
+#include <qfileinfo.h>
+#include <qstorageinfo.h>
#include "BuildConfig.h"
@@ -48,6 +52,7 @@
#include <QTextStream>
#include <QUrl>
#include <QtNetwork>
+#include <system_error>
#include "DesktopServices.h"
#include "StringUtils.h"
@@ -91,6 +96,18 @@ namespace fs = std::filesystem;
namespace fs = ghc::filesystem;
#endif
+
+// clone
+#if defined(Q_OS_LINUX)
+#include <linux/fs.h>
+#include <fcntl.h> /* Definition of FICLONE* constants */
+#include <sys/ioctl.h>
+#include <errno.h>
+#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#include <sys/attr.h>
+#include <sys/clonefile.h>
+#endif
+
namespace FS {
void ensureExists(const QDir& dir)
@@ -831,4 +848,193 @@ bool overrideFolder(QString overwritten_path, QString override_path)
return err.value() == 0;
}
+
+/**
+ * @brief colect information about the filesystem under a file
+ *
+ */
+FilesystemInfo statFS(QString path)
+{
+
+ FilesystemInfo info;
+
+ QStorageInfo storage_info(path);
+
+ QString fsTypeName = QString::fromStdString(storage_info.fileSystemType().toStdString());
+
+ for (auto fs_type_pair : s_filesystem_type_names_inverse.toStdMap()) {
+ auto fs_type_name = fs_type_pair.first;
+ auto fs_type = fs_type_pair.second;
+
+ if(fsTypeName.contains(fs_type_name.toLower())) {
+ info.fsType = fs_type;
+ break;
+ }
+ }
+
+ info.blockSize = storage_info.blockSize();
+ info.bytesAvailable = storage_info.bytesAvailable();
+ info.bytesFree = storage_info.bytesFree();
+ info.bytesTotal = storage_info.bytesTotal();
+
+ info.name = storage_info.name();
+ info.rootPath = storage_info.rootPath();
+
+ return info;
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable
+ *
+ */
+bool canCloneOnFS(const QString& path)
+{
+ FilesystemInfo info = statFS(path);
+ return canCloneOnFS(info);
+}
+bool canCloneOnFS(const FilesystemInfo& info)
+{
+ return canCloneOnFS(info.fsType);
+}
+bool canCloneOnFS(FilesystemType type)
+{
+ return s_clone_filesystems.contains(type);
+}
+
+/**
+ * @brief if the Filesystem is reflink/clone capable and both paths are on the same device
+ *
+ */
+bool canClone(const QString& src, const QString& dst)
+{
+ auto srcVInfo = statFS(src);
+ auto dstVInfo = statFS(dst);
+
+ bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath;
+
+ return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo);
+}
+
+/**
+ * @brief reflink/clones a directory and it's contents from src to dest
+ * @param offset subdirectory form src to copy to dest
+ * @return if there was an error during the filecopy
+ */
+bool clone::operator()(const QString& offset, bool dryRun)
+{
+
+ if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) {
+ qWarning() << "Can not clone: not same device or not clone/reflink filesystem";
+ qDebug() << "Source path:" << m_src.absolutePath();
+ qDebug() << "Destination path:" << m_dst.absolutePath();
+ emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath());
+ return false;
+ }
+
+ m_cloned = 0; // reset counter
+
+ auto src = PathCombine(m_src.absolutePath(), offset);
+ auto dst = PathCombine(m_dst.absolutePath(), offset);
+
+ std::error_code err;
+
+ // Function that'll do the actual cloneing
+ auto cloneFile = [&](QString src_path, QString relative_dst_path) {
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
+ return;
+
+ auto dst_path = PathCombine(dst, relative_dst_path);
+ if (!dryRun) {
+ ensureFilePathExists(dst_path);
+ clone_file(src_path, dst_path, err);
+ }
+ if (err) {
+ qWarning() << "Failed to clone files:" << QString::fromStdString(err.message());
+ qDebug() << "Source file:" << src_path;
+ qDebug() << "Destination file:" << dst_path;
+ }
+ m_cloned++;
+ emit fileCloned(src_path, dst_path);
+ };
+
+ // We can't use copy_opts::recursive because we need to take into account the
+ // blacklisted paths, so we iterate over the source directory, and if there's no blacklist
+ // match, we copy the file.
+ QDir src_dir(src);
+ QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
+
+ while (source_it.hasNext()) {
+ auto src_path = source_it.next();
+ auto relative_path = src_dir.relativeFilePath(src_path);
+
+ cloneFile(src_path, relative_path);
+ }
+
+ // If the root src is not a directory, the previous iterator won't run.
+ if (!fs::is_directory(StringUtils::toStdString(src)))
+ cloneFile(src, "");
+
+ return err.value() == 0;
+}
+
+/**
+ * @brief clone/reflink file from src to dst
+ *
+ */
+bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
+{
+ auto src_path = StringUtils::toStdString(QFileInfo(src).absoluteFilePath());
+ auto dst_path = StringUtils::toStdString(QFileInfo(dst).absoluteFilePath());
+
+#if defined(Q_OS_WIN)
+ qWarning("clone/reflink not supported on windows!");
+ ec = std::make_error_code(std::errc::not_supported);
+ return false;
+#elif defined(Q_OS_LINUX)
+
+ // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html
+
+ int src_fd = open(src_path.c_str(), O_RDONLY);
+ if(!src_fd) {
+ qWarning() << "Failed to open file:" << src_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+ int dst_fd = open(dst_path.c_str(), O_WRONLY | O_TRUNC);
+ if(!dst_fd) {
+ qWarning() << "Failed to open file:" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+ // attempt to clone
+ if(!ioctl(dst_fd, FICLONE, src_fd)){
+ qWarning() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+
+#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ // TODO: use clonefile
+ // clonefile(const char * src, const char * dst, int flags);
+ // https://www.manpagez.com/man/2/clonefile/
+
+ if (!clonefile(src_path.c_str(), dst_path.c_str(), 0)) {
+ qWarning() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str();
+ qDebug() << "Error:" << strerror(errno);
+ ec = std::make_error_code(static_cast<std::errc>(errno));
+ return false;
+ }
+
+#else
+ qWarning("clone/reflink not supported! unknown OS");
+ ec = std::make_error_code(std::errc::not_supported);
+ return false;
+#endif
+
+ return true;
+}
+
}
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index 782a2f40..531036dd 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -313,4 +314,144 @@ bool overrideFolder(QString overwritten_path, QString override_path);
* Creates a shortcut to the specified target file at the specified destination path.
*/
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon);
+
+enum class FilesystemType {
+ FAT,
+ NTFS,
+ EXT,
+ EXT_2_OLD,
+ EXT_2_3_4,
+ XFS,
+ BTRFS,
+ NFS,
+ ZFS,
+ APFS,
+ HFS,
+ HFSPLUS,
+ HFSX,
+ UNKNOWN
+};
+
+static const QMap<FilesystemType, QString> s_filesystem_type_names = {
+ {FilesystemType::FAT, QString("FAT")},
+ {FilesystemType::NTFS, QString("NTFS")},
+ {FilesystemType::EXT, QString("EXT")},
+ {FilesystemType::EXT_2_OLD, QString("EXT2_OLD")},
+ {FilesystemType::EXT_2_3_4, QString("EXT2/3/4")},
+ {FilesystemType::XFS, QString("XFS")},
+ {FilesystemType::BTRFS, QString("BTRFS")},
+ {FilesystemType::NFS, QString("NFS")},
+ {FilesystemType::ZFS, QString("ZFS")},
+ {FilesystemType::APFS, QString("APFS")},
+ {FilesystemType::HFS, QString("HFS")},
+ {FilesystemType::HFSPLUS, QString("HFSPLUS")},
+ {FilesystemType::HFSX, QString("HFSX")},
+ {FilesystemType::UNKNOWN, QString("UNKNOWN")}
+};
+
+static const QMap<QString, FilesystemType> s_filesystem_type_names_inverse = {
+ {QString("FAT"), FilesystemType::FAT},
+ {QString("NTFS"), FilesystemType::NTFS},
+ {QString("EXT2_OLD"), FilesystemType::EXT_2_OLD},
+ {QString("EXT2"), FilesystemType::EXT_2_3_4},
+ {QString("EXT3"), FilesystemType::EXT_2_3_4},
+ {QString("EXT4"), FilesystemType::EXT_2_3_4},
+ {QString("EXT"), FilesystemType::EXT},
+ {QString("XFS"), FilesystemType::XFS},
+ {QString("BTRFS"), FilesystemType::BTRFS},
+ {QString("NFS"), FilesystemType::NFS},
+ {QString("ZFS"), FilesystemType::ZFS},
+ {QString("APFS"), FilesystemType::APFS},
+ {QString("HFSPLUS"), FilesystemType::HFSPLUS},
+ {QString("HFSX"), FilesystemType::HFSX},
+ {QString("HFS"), FilesystemType::HFS},
+ {QString("UNKNOWN"), FilesystemType::UNKNOWN}
+};
+
+inline QString getFilesystemTypeName(FilesystemType type) {
+ return s_filesystem_type_names.constFind(type).value();
+}
+
+struct FilesystemInfo {
+ FilesystemType fsType = FilesystemType::UNKNOWN;
+ int blockSize;
+ qint64 bytesAvailable;
+ qint64 bytesFree;
+ qint64 bytesTotal;
+ QString name;
+ QString rootPath;
+};
+
+/**
+ * @brief colect information about the filesystem under a file
+ *
+ */
+FilesystemInfo statFS(QString path);
+
+
+static const QList<FilesystemType> s_clone_filesystems = {
+ FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, FilesystemType::XFS
+};
+
+/**
+ * @brief if the Filesystem is reflink/clone capable
+ *
+ */
+bool canCloneOnFS(const QString& path);
+bool canCloneOnFS(const FilesystemInfo& info);
+bool canCloneOnFS(FilesystemType type);
+
+/**
+ * @brief if the Filesystem is reflink/clone capable and both are on the same device
+ *
+ */
+bool canClone(const QString& src, const QString& dst);
+
+/**
+ * @brief Copies a directory and it's contents from src to dest
+ */
+class clone : public QObject {
+ Q_OBJECT
+ public:
+ clone(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
+ {
+ m_src.setPath(src);
+ m_dst.setPath(dst);
+ }
+ clone& matcher(const IPathMatcher* filter)
+ {
+ m_matcher = filter;
+ return *this;
+ }
+ clone& whitelist(bool whitelist)
+ {
+ m_whitelist = whitelist;
+ return *this;
+ }
+
+ bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
+
+ int totalCloned() { return m_cloned; }
+
+ signals:
+ void fileCloned(const QString& src, const QString& dst);
+ void cloneFailed(const QString& src, const QString& dst);
+
+ private:
+ bool operator()(const QString& offset, bool dryRun = false);
+
+ private:
+ const IPathMatcher* m_matcher = nullptr;
+ bool m_whitelist = false;
+ QDir m_src;
+ QDir m_dst;
+ int m_cloned;
+};
+
+/**
+ * @brief clone/reflink file from src to dst
+ *
+ */
+bool clone_file(const QString& src, const QString& dst, std::error_code& ec);
+
}