feat: support reflink on windows via winbtrfs!

https://github.com/maharmstone/btrfs

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-02-10 05:34:48 -08:00
parent 3a0e4546c2
commit 2e8d04aad0
3 changed files with 200 additions and 23 deletions

View File

@ -108,6 +108,13 @@ namespace fs = ghc::filesystem;
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
// winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
// refs
#include <winioctl.h>
#endif
namespace FS {
@ -916,25 +923,44 @@ bool overrideFolder(QString overwritten_path, QString override_path)
return err.value() == 0;
}
/**
* @brief path to the near ancestor that exsists
*
*/
QString NearestExistentAncestor(const QString& path)
{
if(QFileInfo::exists(path)) return path;
QDir dir(path);
if(!dir.makeAbsolute()) return {};
do
{
dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral(".."))));
}
while(!dir.exists() && !dir.isRoot());
return dir.exists() ? dir.path() : QString();
}
/**
* @brief colect information about the filesystem under a file
*
*/
FilesystemInfo statFS(QString path)
FilesystemInfo statFS(const QString& path)
{
FilesystemInfo info;
QStorageInfo storage_info(path);
QStorageInfo storage_info(NearestExistentAncestor(path));
QString fsTypeName = QString::fromStdString(storage_info.fileSystemType().toStdString());
qDebug() << "Qt reports Filesystem at" << path << "root:" << storage_info.rootPath() << "as" << fsTypeName;
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())) {
if(fsTypeName.toLower().contains(fs_type_name.toLower())) {
info.fsType = fs_type;
break;
}
@ -948,9 +974,6 @@ FilesystemInfo statFS(QString path)
info.name = storage_info.name();
info.rootPath = storage_info.rootPath();
qDebug() << "Pulling filesystem info for" << info.rootPath;
qDebug() << "Filesystem: " << fsTypeName << "detected" << getFilesystemTypeName(info.fsType);
return info;
}
@ -1054,15 +1077,156 @@ bool clone::operator()(const QString& offset, bool dryRun)
*/
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());
auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath()));
auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath()));
#if defined(Q_OS_WIN)
qWarning("clone/reflink not supported on windows!");
FilesystemInfo srcinfo = statFS(src);
if (srcinfo.fsType == FilesystemType::BTRFS) {
FilesystemInfo dstinfo = statFS(dst);
if (dstinfo.fsType != FilesystemType::BTRFS || (srcinfo.rootPath != dstinfo.rootPath)){
qWarning() << "winbtrfs clone must be to the same device! src and dst root paths do not match.";
qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!";
ec = std::make_error_code(std::errc::not_supported);
return false;
}
qWarning() << "clone/reflink of btrfs on windows! assuming winbtrfs is in use and calling shellbtrfs.dll via rundll32.exe";
if (!winbtrfs_clone(src_path, dst_path, ec))
return false;
// There is no return value from rundll32.exe so we must check if the file exsists ourselves
QFileInfo dstInfo(dst);
if (!dstInfo.exists() || !dstInfo.isFile() || dstInfo.isSymLink()) {
// shellbtrfs.dll,ReflinkCopyW is curently broken https://github.com/maharmstone/btrfs/issues/556
// lets try a little workaround
// find the misnamed file
qDebug() << dst << "is missing. ReflinkCopyW may still be broken, trying workaround.";
QString badDst = QDir(dstInfo.absolutePath()).path() + dstInfo.fileName();
qDebug() << "trying" << badDst;
QFileInfo badDstInfo(badDst);
if (badDstInfo.exists() && badDstInfo.isFile()) {
qDebug() << badDst << "exists! moving it to the correct location.";
if(!move(badDstInfo.absoluteFilePath(), dstInfo.absoluteFilePath())) {
qDebug() << "move from" << badDst << "to" << dst << "failed";
ec = std::make_error_code(std::errc::no_such_file_or_directory);
return false;
}
} else {
// oof, clone failure?
qWarning() << "clone/reflink on winbtrfs did not succeed: file" << dst << "does not appear to exsist";
ec = std::make_error_code(std::errc::no_such_file_or_directory);
return false;
}
}
} else if (srcinfo.fsType == FilesystemType::REFS) {
qWarning() << "clone/reflink not yet supported on windows ReFS!";
ec = std::make_error_code(std::errc::not_supported);
return false;
} else {
qWarning() << "clone/reflink not supported on windows outside of winbtrfs or ReFS!";
qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!";
ec = std::make_error_code(std::errc::not_supported);
return false;
}
#elif defined(Q_OS_LINUX)
if(!linux_ficlone(src_path, dst_path, ec))
return false;
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if(!macos_bsd_clonefile(src_path, dst_path, ec))
return false;
#else
qWarning() << "clone/reflink not supported! unknown OS";
ec = std::make_error_code(std::errc::not_supported);
return false;
#endif
return true;
}
#if defined(Q_OS_WIN)
typedef void (__stdcall *f_ReflinkCopyW)(HWND hwnd, HINSTANCE hinst, LPWSTR lpszCmdLine, int nCmdShow);
bool winbtrfs_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec)
{
// https://github.com/maharmstone/btrfs
QString cmdLine = QString("\"%1\" \"%2\"").arg(src_path, dst_path);
std::wstring wstr = cmdLine.toStdWString(); // temp buffer to copy the data and avoid side effect of non const cast
LPWSTR cmdLineWin = (wchar_t*)wstr.c_str();
// https://github.com/maharmstone/btrfs/blob/9da54911dd6f3713a1c4c7be40338a3da126f4e6/src/shellext/contextmenu.cpp#L1609
HINSTANCE shellbtrfsDLL = LoadLibrary(L"shellbtrfs.dll");
if (shellbtrfsDLL == NULL) {
ec = std::make_error_code(std::errc::not_supported);
qWarning() << "cannot locate the shellbtrfs.dll file, reflink copy not supported";
return false;
}
f_ReflinkCopyW ReflinkCopyW = (f_ReflinkCopyW)GetProcAddress(shellbtrfsDLL, "ReflinkCopyW");
if (!ReflinkCopyW) {
ec = std::make_error_code(std::errc::not_supported);
qWarning() << "cannot locate the ReflinkCopyW function from shellbtrfs.dll, reflink copy not supported";
return false;
}
qDebug() << "Calling ReflinkCopyW from shellbtrfs.dll with:" << cmdLine;
ReflinkCopyW(0, 0, cmdLineWin, 1);
FreeLibrary(shellbtrfsDLL);
return true;
}
bool refs_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec)
{
//https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
//https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94
std::wstring existingFile = src_path.c_str();
std::wstring newLink = dst_path.c_str();
HANDLE hExistingFile = CreateFile(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hExistingFile == INVALID_HANDLE_VALUE)
{
return false;
}
HANDLE hNewFile = CreateFile(dst_path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL);
if (hNewFile == INVALID_HANDLE_VALUE)
{
CloseHandle(hExistingFile);
return false;
}
DWORD bytesReturned;
// FIXME: ReFS requires that cloned regions reside on a disk cluster boundary.
// FIXME: ERROR_BLOCK_TOO_MANY_REFERENCES can occure https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks
BOOL result = DeviceIoControl(hExistingFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, hNewFile, sizeof(hNewFile), NULL, 0, &bytesReturned, NULL);
CloseHandle(hNewFile);
CloseHandle(hExistingFile);
return (result != 0);
}
#elif defined(Q_OS_LINUX)
bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{
// https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html
int src_fd = open(src_path.c_str(), O_RDONLY);
@ -1097,9 +1261,12 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
qWarning() << "Failed to close file:" << dst_path.c_str();
qDebug() << "Error:" << strerror(errno);
}
return true;
}
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// TODO: use clonefile
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{
// clonefile(const char * src, const char * dst, int flags);
// https://www.manpagez.com/man/2/clonefile/
@ -1110,16 +1277,9 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
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;
}
#endif
/**
* @brief if the Filesystem is symlink capable

View File

@ -344,6 +344,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
enum class FilesystemType {
FAT,
NTFS,
REFS,
EXT,
EXT_2_OLD,
EXT_2_3_4,
@ -363,6 +364,7 @@ enum class FilesystemType {
static const QMap<FilesystemType, QString> s_filesystem_type_names = {
{FilesystemType::FAT, QString("FAT")},
{FilesystemType::NTFS, QString("NTFS")},
{FilesystemType::REFS, QString("REFS")},
{FilesystemType::EXT, QString("EXT")},
{FilesystemType::EXT_2_OLD, QString("EXT2_OLD")},
{FilesystemType::EXT_2_3_4, QString("EXT2/3/4")},
@ -382,6 +384,7 @@ static const QMap<FilesystemType, QString> s_filesystem_type_names = {
static const QMap<QString, FilesystemType> s_filesystem_type_names_inverse = {
{QString("FAT"), FilesystemType::FAT},
{QString("NTFS"), FilesystemType::NTFS},
{QString("REFS"), FilesystemType::REFS},
{QString("EXT2_OLD"), FilesystemType::EXT_2_OLD},
{QString("EXT2"), FilesystemType::EXT_2_3_4},
{QString("EXT3"), FilesystemType::EXT_2_3_4},
@ -414,15 +417,21 @@ struct FilesystemInfo {
QString rootPath;
};
/**
* @brief path to the near ancestor that exsists
*
*/
QString NearestExistentAncestor(const QString& path);
/**
* @brief colect information about the filesystem under a file
*
*/
FilesystemInfo statFS(QString path);
FilesystemInfo statFS(const QString& path);
static const QList<FilesystemType> s_clone_filesystems = {
FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, FilesystemType::XFS
FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, FilesystemType::XFS, FilesystemType::REFS
};
/**
@ -486,6 +495,14 @@ class clone : public QObject {
*/
bool clone_file(const QString& src, const QString& dst, std::error_code& ec);
#if defined(Q_OS_WIN)
bool winbtrfs_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec);
bool refs_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec);
#elif defined(Q_OS_LINUX)
bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
#endif
static const QList<FilesystemType> s_non_link_filesystems = {
FilesystemType::FAT,

View File

@ -865,7 +865,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
QString InstanceList::getStagedInstancePath()
{
QString key = QUuid::createUuid().toString();
QString key = QUuid::createUuid().toString().remove("{").remove("}");
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);