feat: reflink / Clone support!
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
parent
c9105e525e
commit
c5bbe42b57
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user