diff options
author | Rachel Powers <508861+Ryex@users.noreply.github.com> | 2023-05-07 13:21:21 -0700 |
---|---|---|
committer | Rachel Powers <508861+Ryex@users.noreply.github.com> | 2023-05-07 13:21:21 -0700 |
commit | 884ac7307817e10f04512a29213a017ca344c16d (patch) | |
tree | 37bcb97dfec8516edba3501a5475b1daf314d2b7 /launcher | |
parent | 718abaae0ef465050c81c0dfba63ce9f0fff17fc (diff) | |
parent | ce5bb29c442cee3654c5f4287a999d5d6593032f (diff) | |
download | PrismLauncher-884ac7307817e10f04512a29213a017ca344c16d.tar.gz PrismLauncher-884ac7307817e10f04512a29213a017ca344c16d.tar.bz2 PrismLauncher-884ac7307817e10f04512a29213a017ca344c16d.zip |
Merge remote-tracking branch 'upstream/develop' into better-tasks
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
Diffstat (limited to 'launcher')
50 files changed, 2910 insertions, 413 deletions
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 8680361c..a8fce879 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -40,6 +40,8 @@ #include <QDir> #include <QDebug> #include <QRegularExpression> +#include <QJsonDocument> +#include <QJsonObject> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -64,6 +66,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); + m_settings->registerSetting("linkedInstances", "[]"); + // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); @@ -182,6 +186,38 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const return m_settings->get("ConsoleOverflowStop").toBool(); } +QStringList BaseInstance::getLinkedInstances() const +{ + return m_settings->get("linkedInstances").toStringList(); +} + +void BaseInstance::setLinkedInstances(const QStringList& list) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + m_settings->set("linkedInstances", list); +} + +void BaseInstance::addLinkedInstanceId(const QString& id) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + linkedInstances.append(id); + setLinkedInstances(linkedInstances); +} + +bool BaseInstance::removeLinkedInstanceId(const QString& id) +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + int numRemoved = linkedInstances.removeAll(id); + setLinkedInstances(linkedInstances); + return numRemoved > 0; +} + +bool BaseInstance::isLinkedToInstanceId(const QString& id) const +{ + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + return linkedInstances.contains(id); +} + void BaseInstance::iconUpdated(QString key) { if(iconKey() == key) diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index a2a4f824..83a8064f 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -282,6 +282,12 @@ public: int getConsoleMaxLines() const; bool shouldStopOnConsoleOverflow() const; + QStringList getLinkedInstances() const; + void setLinkedInstances(const QStringList& list); + void addLinkedInstanceId(const QString& id); + bool removeLinkedInstanceId(const QString& id); + bool isLinkedToInstanceId(const QString& id) const; + protected: void changeStatus(Status newStatus); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a3ef20e8..273b5449 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -26,6 +26,7 @@ set(CORE_SOURCES MMCZip.cpp StringUtils.h StringUtils.cpp + QVariantUtils.h RuntimeContext.h # Basic instance manipulation tasks (derived from InstanceTask) @@ -554,6 +555,18 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +set(LINKEXE_SOURCES + filelink/FileLink.h + filelink/FileLink.cpp + FileSystem.h + FileSystem.cpp + Exception.h + StringUtils.h + StringUtils.cpp + DesktopServices.h + DesktopServices.cpp +) + ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -1145,6 +1158,41 @@ install(TARGETS ${Launcher_Name} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +if(WIN32) + add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) + target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(filelink_logic + systeminfo + BuildConfig + ghcFilesystem::ghc_filesystem + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + # Qt${QT_VERSION_MAJOR}::Concurrent + ${Launcher_QT_LIBS} + ) + + add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp) + + target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + + target_link_libraries("${Launcher_Name}_filelink" filelink_logic) + + if(DEFINED Launcher_APP_BINARY_NAME) + set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink") + endif() + if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") + endif() + + install(TARGETS "${Launcher_Name}_filelink" + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime + ) +endif() + if (UNIX AND APPLE) # Add Sparkle updater # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index 302eaf96..2984a1b4 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -37,7 +37,6 @@ #include <QDesktopServices> #include <QProcess> #include <QDebug> -#include "Application.h" /** * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index aee5245d..d98526df 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 @@ -36,6 +37,8 @@ #include "FileSystem.h" +#include "BuildConfig.h" + #include <QDebug> #include <QDir> #include <QDirIterator> @@ -43,13 +46,17 @@ #include <QFileInfo> #include <QSaveFile> #include <QStandardPaths> +#include <QStorageInfo> #include <QTextStream> #include <QUrl> +#include <QtNetwork> +#include <system_error> #include "DesktopServices.h" #include "StringUtils.h" #if defined Q_OS_WIN32 +#define NOMINMAX #define WIN32_LEAN_AND_MEAN #include <objbase.h> #include <objidl.h> @@ -61,6 +68,10 @@ #include <windows.h> #include <winnls.h> #include <string> +// for ShellExecute +#include <Shellapi.h> +#include <objbase.h> +#include <shlobj.h> #else #include <utime.h> #endif @@ -68,22 +79,96 @@ // Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header #ifdef __APPLE__ -#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ +#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) #if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) #define GHC_USE_STD_FS #include <filesystem> namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check +#endif // MacOS min version check +#endif // Other OSes version check #ifndef GHC_USE_STD_FS #include <ghc/filesystem.hpp> namespace fs = ghc::filesystem; #endif +// clone +#if defined(Q_OS_LINUX) +#include <errno.h> +#include <fcntl.h> /* Definition of FICLONE* constants */ +#include <linux/fs.h> +#include <sys/ioctl.h> +#include <unistd.h> +#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 <fileapi.h> +#include <stdio.h> +#include <tchar.h> +#include <windows.h> +// refs +#include <winioctl.h> +#if defined(__MINGW32__) +#include <crtdbg.h> +#endif +#endif + +#if defined(Q_OS_WIN) + +#if defined(__MINGW32__) + +typedef struct _DUPLICATE_EXTENTS_DATA { + HANDLE FileHandle; + LARGE_INTEGER SourceFileOffset; + LARGE_INTEGER TargetFileOffset; + LARGE_INTEGER ByteCount; +} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA; + +typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx + DWORD ChecksumChunkSizeInBytes; + DWORD ClusterSizeInBytes; +} FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER; + +typedef struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx +} FSCTL_SET_INTEGRITY_INFORMATION_BUFFER, *PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER; + +#endif + +#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE +#define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA) +#endif + +#ifndef FSCTL_GET_INTEGRITY_INFORMATION +#define FSCTL_GET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef FSCTL_SET_INTEGRITY_INFORMATION +#define FSCTL_SET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef ERROR_NOT_CAPABLE +#define ERROR_NOT_CAPABLE 775L +#endif + +#ifndef ERROR_BLOCK_TOO_MANY_REFERENCES +#define ERROR_BLOCK_TOO_MANY_REFERENCES 347L +#endif + +#endif + namespace FS { void ensureExists(const QDir& dir) @@ -152,9 +237,11 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -/// @brief Copies 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 +/** + * @brief Copies 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 copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; @@ -215,6 +302,271 @@ bool copy::operator()(const QString& offset, bool dryRun) return err.value() == 0; } +/// qDebug print support for the LinkPair struct +QDebug operator<<(QDebug debug, const LinkPair& lp) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }"; + return debug; +} + +bool create_link::operator()(const QString& offset, bool dryRun) +{ + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + m_path_results.clear(); + + make_link_list(offset); + + if (!dryRun) + return make_links(); + + return true; +} + +/** + * @brief Make a list of all the links to make + * @param offset subdirectory of src to link to dest + */ +void create_link::make_link_list(const QString& offset) +{ + for (auto pair : m_path_pairs) { + const QString& srcPath = pair.src; + const QString& dstPath = pair.dst; + + auto src = PathCombine(QDir(srcPath).absolutePath(), offset); + auto dst = PathCombine(QDir(dstPath).absolutePath(), offset); + + // you can't hard link a directory so make sure if we deal with a directory we do so recursively + if (m_useHardLinks) + m_recursive = true; + + // Function that'll do the actual linking + auto link_file = [&](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { + qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; + return; + } + + auto dst_path = PathCombine(dst, relative_dst_path); + LinkPair link = { src_path, dst_path }; + m_links_to_make.append(link); + }; + + if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) { + if (m_debug) + qDebug() << "linking single file or dir:" << src << "to" << dst; + link_file(src, ""); + } else { + if (m_debug) + qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth; + QDir src_dir(src); + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); + + QStringList linkedPaths; + + while (source_it.hasNext()) { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); + + if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth){ + relative_path = pathTruncate(relative_path, m_max_depth); + src_path = src_dir.filePath(relative_path); + if (linkedPaths.contains(src_path)) { + continue; + } + } + + linkedPaths.append(src_path); + + link_file(src_path, relative_path); + } + } + } +} + +bool create_link::make_links() +{ + for (auto link : m_links_to_make) { + QString src_path = link.src; + QString dst_path = link.dst; + auto src_path_std = StringUtils::toStdString(link.src); + auto dst_path_std = StringUtils::toStdString(link.dst); + + ensureFilePathExists(dst_path); + if (m_useHardLinks) { + if (m_debug) + qDebug() << "making hard link:" << src_path << "to" << dst_path; + fs::create_hard_link(src_path_std, dst_path_std, m_os_err); + } else if (fs::is_directory(src_path_std)) { + if (m_debug) + qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; + fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err); + } else { + if (m_debug) + qDebug() << "making symlink:" << src_path << "to" << dst_path; + fs::create_symlink(src_path_std, dst_path_std, m_os_err); + } + + if (m_os_err) { + qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + qDebug() << "Error category:" << m_os_err.category().name(); + qDebug() << "Error code:" << m_os_err.value(); + emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value()); + } else { + m_linked++; + emit fileLinked(src_path, dst_path); + } + if (m_os_err) + return false; + } + return true; +} + +void create_link::runPrivileged(const QString& offset) +{ + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + bool gotResults = false; + + make_link_list(offset); + + QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); + + connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() { + qDebug() << "Client connected, sending out pairs"; + // construct block of data to send + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + + qint32 blocksize = quint32(sizeof(quint32)); + for (auto link : m_links_to_make) { + blocksize += quint32(link.src.size()); + blocksize += quint32(link.dst.size()); + } + qDebug() << "About to write block of size:" << blocksize; + out << blocksize; + + out << quint32(m_links_to_make.length()); + for (auto link : m_links_to_make) { + out << link.src; + out << link.dst; + } + + QLocalSocket* clientConnection = m_linkServer.nextPendingConnection(); + connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); + + connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() { + QDataStream in; + quint32 blockSize = 0; + in.setDevice(clientConnection); + + qDebug() << "Reading path results from client"; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + + // Relies on the fact that QDataStream serializes a quint32 into + // sizeof(quint32) bytes + if (clientConnection->bytesAvailable() < (int)sizeof(quint32)) + return; + qDebug() << "reading block size"; + in >> blockSize; + + qDebug() << "blocksize is" << blockSize; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + if (clientConnection->bytesAvailable() < blockSize || in.atEnd()) + return; + + quint32 numResults; + in >> numResults; + qDebug() << "numResults" << numResults; + + for (quint32 i = 0; i < numResults; i++) { + FS::LinkResult result; + in >> result.src; + in >> result.dst; + in >> result.err_msg; + qint32 err_value; + in >> err_value; + result.err_value = err_value; + if (result.err_value) { + qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg; + emit linkFailed(result.src, result.dst, result.err_msg, result.err_value); + } else { + qDebug() << "privileged link success" << result.src << "to" << result.dst; + m_linked++; + emit fileLinked(result.src, result.dst); + } + m_path_results.append(result); + } + gotResults = true; + qDebug() << "results received, closing connection"; + clientConnection->close(); + }); + + qint64 byteswritten = clientConnection->write(block); + bool bytesflushed = clientConnection->flush(); + qDebug() << "block flushed" << byteswritten << bytesflushed; + }); + + qDebug() << "Listening on pipe" << serverName; + if (!m_linkServer.listen(serverName)) { + qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString(); + return; + } + + ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); + connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); + + linkFileProcess->start(); +} + +void ExternalLinkFileProcess::runLinkFile() +{ + QString fileLinkExe = + PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink"); + QString params = "-s " + m_server; + + params += " -H " + QVariant(m_useHardLinks).toString(); + +#if defined Q_OS_WIN32 + SHELLEXECUTEINFO ShExecInfo; + + fileLinkExe = fileLinkExe + ".exe"; + + qDebug() << "Running: runas" << fileLinkExe << params; + + LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16(); + LPCWSTR paramsWin = (const wchar_t*)params.utf16(); + + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa + ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce + // while executing this function. + ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC + ShExecInfo.lpFile = programNameWin; + ShExecInfo.lpParameters = paramsWin; + ShExecInfo.lpDirectory = NULL; + ShExecInfo.nShow = SW_HIDE; + ShExecInfo.hInstApp = NULL; + + ShellExecuteEx(&ShExecInfo); + + WaitForSingleObject(ShExecInfo.hProcess, INFINITE); + CloseHandle(ShExecInfo.hProcess); +#endif + + qDebug() << "Process exited"; +} + bool move(const QString& source, const QString& dest) { std::error_code err; @@ -244,7 +596,7 @@ bool deletePath(QString path) return err.value() == 0; } -bool trash(QString path, QString *pathInTrash) +bool trash(QString path, QString* pathInTrash) { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) return false; @@ -279,11 +631,60 @@ QString PathCombine(const QString& path1, const QString& path2, const QString& p return PathCombine(PathCombine(path1, path2, path3), path4); } -QString AbsolutePath(QString path) +QString AbsolutePath(const QString& path) { return QFileInfo(path).absolutePath(); } +int pathDepth(const QString& path) +{ + if (path.isEmpty()) + return 0; + + QFileInfo info(path); + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); +#else + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); +#endif + + int numParts = parts.length(); + numParts -= parts.count("."); + numParts -= parts.count("..") * 2; + + return numParts; +} + +QString pathTruncate(const QString& path, int depth) +{ + if (path.isEmpty() || (depth < 0)) + return ""; + + QString trunc = QFileInfo(path).path(); + + if (pathDepth(trunc) > depth ) { + return pathTruncate(trunc, depth); + } + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts); +#else + auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); +#endif + + if (parts.startsWith(".") && !path.startsWith(".")) { + parts.removeFirst(); + } + if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) { + parts.prepend(""); + } + + trunc = parts.join(QDir::separator()); + + return trunc; +} + QString ResolveExecutable(QString path) { if (path.isEmpty()) { @@ -381,11 +782,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri stream << "#!/bin/bash" << "\n"; - stream << "\"" - << target - << "\" " - << argstring - << "\n"; + stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); f.close(); @@ -408,8 +805,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; - if (!icon.isEmpty()) - { + if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; } @@ -422,55 +818,45 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); - if (!targetInfo.exists()) - { + if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; return false; } target = targetInfo.absoluteFilePath(); - if (target.length() >= MAX_PATH) - { + if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; return false; } - if (!icon.isEmpty() && icon.length() >= MAX_PATH) - { + if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; return false; } destination += ".lnk"; - if (destination.length() >= MAX_PATH) - { + if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; return false; } QString argStr; int argCount = args.count(); - for (int i = 0; i < argCount; i++) - { - if (args[i].contains(' ')) - { + for (int i = 0; i < argCount; i++) { + if (args[i].contains(' ')) { argStr.append('"').append(args[i]).append('"'); - } - else - { + } else { argStr.append(args[i]); } - if (i < argCount - 1) - { + if (i < argCount - 1) { argStr.append(" "); } } - if (argStr.length() >= MAX_PATH) - { + if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; return false; } @@ -479,8 +865,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri // ...yes, you need to initialize the entire COM stack just to make a shortcut hres = CoInitialize(nullptr); - if (FAILED(hres)) - { + if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; return false; } @@ -491,8 +876,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri // create an IShellLink instance - this stores the shortcut's attributes hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); - if (SUCCEEDED(hres)) - { + if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); target.toWCharArray(wsz); psl->SetPath(wsz); @@ -503,10 +887,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri wmemset(wsz, 0, MAX_PATH); targetInfo.absolutePath().toWCharArray(wsz); - psl->SetWorkingDirectory(wsz); // "Starts in" attribute + psl->SetWorkingDirectory(wsz); // "Starts in" attribute - if (!icon.isEmpty()) - { + if (!icon.isEmpty()) { wmemset(wsz, 0, MAX_PATH); icon.toWCharArray(wsz); psl->SetIconLocation(wsz, 0); @@ -516,27 +899,21 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri // this is the interface that will actually let us save the shortcut to disk! IPersistFile* ppf; hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); - if (SUCCEEDED(hres)) - { + if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); destination.toWCharArray(wsz); hres = ppf->Save(wsz, TRUE); - if (FAILED(hres)) - { + if (FAILED(hres)) { qWarning() << "IPresistFile->Save() failed"; qWarning() << "hres = " << hres; } ppf->Release(); - } - else - { + } else { qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; qWarning() << "hres = " << hres; } psl->Release(); - } - else - { + } else { qWarning() << "Failed to create IShellLink instance"; qWarning() << "hres = " << hres; } @@ -572,4 +949,490 @@ bool overrideFolder(QString overwritten_path, QString override_path) return err.value() == 0; } +QString getFilesystemTypeName(FilesystemType type) +{ + auto iter = s_filesystem_type_names.constFind(type); + if (iter != s_filesystem_type_names.constEnd()) { + return iter.value().constFirst(); + } + return getFilesystemTypeName(FilesystemType::UNKNOWN); +} + +FilesystemType getFilesystemTypeFuzzy(const QString& name) +{ + for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { + auto fs_names = iter.value(); + for (auto fs_name : fs_names) { + if (name.toUpper().contains(fs_name.toUpper())) + return iter.key(); + } + } + return FilesystemType::UNKNOWN; +} + +FilesystemType getFilesystemType(const QString& name) +{ + for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { + auto fs_names = iter.value(); + if(fs_names.contains(name.toUpper())) + return iter.key(); + } + return FilesystemType::UNKNOWN; } + +/** + * @brief path to the near ancestor that exists + * + */ +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(const QString& path) +{ + FilesystemInfo info; + + QStorageInfo storage_info(nearestExistentAncestor(path)); + + info.fsTypeName = storage_info.fileSystemType(); + + info.fsType = getFilesystemTypeFuzzy(info.fsTypeName); + + 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) { + qDebug() << "Failed to clone files: error" << err.value() << "message" << 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(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath())); + auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath())); + + FilesystemInfo srcinfo = statFS(src); + FilesystemInfo dstinfo = statFS(dst); + + if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) { + ec = std::make_error_code(std::errc::not_supported); + qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do not match."; + return false; + } + +#if defined(Q_OS_WIN) + + if (!win_ioctl_clone(src_path, dst_path, ec)) { + qDebug() << "failed win_ioctl_clone"; + qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!"; + qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!"; + return false; + } + +#elif defined(Q_OS_LINUX) + + if (!linux_ficlone(src_path, dst_path, ec)) { + qDebug() << "failed linux_ficlone:"; + return false; + } + +#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + + if (!macos_bsd_clonefile(src_path, dst_path, ec)) { + qDebug() << "failed macos_bsd_clonefile:"; + 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) + +static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2) +{ + long mask = roundingMultiplePowerOf2 - 1; + return (originalValue + mask) & ~mask; +} + +bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec) +{ + /** + * This algorithm inspired from https://github.com/0xbadfca11/reflink + * LICENSE MIT + * + * Additional references + * 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 + */ + + HANDLE hSourceFile = CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); + if (hSourceFile == INVALID_HANDLE_VALUE) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to open source file" << src_path.c_str(); + return false; + } + + ULONG fs_flags; + if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to get Filesystem information for " << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { + SetLastError(ERROR_NOT_CAPABLE); + ec = std::error_code(GetLastError(), std::system_category()); + qWarning() << "Filesystem at " << src_path.c_str() << " does not support reflink"; + CloseHandle(hSourceFile); + return false; + } + + FILE_END_OF_FILE_INFO sourceFileLength; + if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to size of source file" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + FILE_BASIC_INFO sourceFileBasicInfo; + if (!GetFileInformationByHandleEx(hSourceFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to source file info" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + ULONG junk; + FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity; + if (!DeviceIoControl(hSourceFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &sourceFileIntegrity, sizeof(sourceFileIntegrity), &junk, + nullptr)) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to source file integrity info" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + + HANDLE hDestFile = CreateFileW(dst_path.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, nullptr, CREATE_NEW, 0, hSourceFile); + + if (hDestFile == INVALID_HANDLE_VALUE) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to open dest file" << dst_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + FILE_DISPOSITION_INFO destFileDispose = { TRUE }; + if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file info" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + + if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest sparseness" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, sourceFileIntegrity.Reserved, + sourceFileIntegrity.Flags }; + if (!DeviceIoControl(hDestFile, FSCTL_SET_INTEGRITY_INFORMATION, &setDestFileintegrity, sizeof(setDestFileintegrity), nullptr, 0, + nullptr, nullptr)) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file integrity info" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file size" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + + const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes; + + DUPLICATE_EXTENTS_DATA dupExtent; + dupExtent.FileHandle = hSourceFile; + for (LONG64 offset = 0, remain = RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes); + remain > 0; offset += splitThreshold, remain -= splitThreshold) { + dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset; + dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain); + + if (!DeviceIoControl(hDestFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dupExtent, sizeof(dupExtent), nullptr, 0, &junk, nullptr)) { + DWORD err = GetLastError(); + QString additionalMessage; + if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) { + static const int MaxClonesPerFile = 8175; + additionalMessage = + QString( + " This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum " + "allowed %1 references for a single file. " + "See " + "https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks") + .arg(MaxClonesPerFile); + } + ec = std::error_code(err, std::system_category()); + qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() << "with error" << err + << additionalMessage; + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + } + + if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) { + FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE }; + if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, &setDestSparse, sizeof(setDestSparse), nullptr, 0, &junk, nullptr)) { + qDebug() << "Failed to set dest file sparseness" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + } + + sourceFileBasicInfo.CreationTime.QuadPart = 0; + if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { + qDebug() << "Failed to set dest file creation time" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + if (!FlushFileBuffers(hDestFile)) { + qDebug() << "Failed to flush dest file buffer" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + destFileDispose = { FALSE }; + bool result = !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose)); + + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + + return result; +} + +#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); + if (src_fd == -1) { + qDebug() << "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_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + if (dst_fd == -1) { + qDebug() << "Failed to open file:" << dst_path.c_str(); + qDebug() << "Error:" << strerror(errno); + ec = std::make_error_code(static_cast<std::errc>(errno)); + close(src_fd); + return false; + } + // attempt to clone + if (ioctl(dst_fd, FICLONE, src_fd) == -1) { + qDebug() << "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)); + close(src_fd); + close(dst_fd); + return false; + } + if (close(src_fd)) { + qDebug() << "Failed to close file:" << src_path.c_str(); + qDebug() << "Error:" << strerror(errno); + } + if (close(dst_fd)) { + qDebug() << "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) + +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/ + + qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str(); + if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) { + qDebug() << "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; + } + return true; +} +#endif + +/** + * @brief if the Filesystem is symlink capable + * + */ +bool canLinkOnFS(const QString& path) +{ + FilesystemInfo info = statFS(path); + return canLinkOnFS(info); +} +bool canLinkOnFS(const FilesystemInfo& info) +{ + return canLinkOnFS(info.fsType); +} +bool canLinkOnFS(FilesystemType type) +{ + return !s_non_link_filesystems.contains(type); +} +/** + * @brief if the Filesystem is symlink capable on both ends + * + */ +bool canLink(const QString& src, const QString& dst) +{ + return canLinkOnFS(src) && canLinkOnFS(dst); +} + +uintmax_t hardLinkCount(const QString& path) +{ + std::error_code err; + int count = fs::hard_link_count(StringUtils::toStdString(path), err); + if (err) { + qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message()); + count = 0; + } + return count; +} + +} // namespace FS diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index f083f3c7..cb581d0c 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 @@ -39,9 +40,13 @@ #include "Exception.h" #include "pathmatcher/IPathMatcher.h" +#include <system_error> + #include <QDir> #include <QFlags> +#include <QLocalServer> #include <QObject> +#include <QThread> namespace FS { @@ -77,7 +82,9 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -/// @brief Copies a directory and it's contents from src to dest +/** + * @brief Copies a directory and it's contents from src to dest + */ class copy : public QObject { Q_OBJECT public: @@ -122,13 +129,134 @@ class copy : public QObject { int m_copied; }; +struct LinkPair { + QString src; + QString dst; +}; + +struct LinkResult { + QString src; + QString dst; + QString err_msg; + int err_value; +}; + +class ExternalLinkFileProcess : public QThread { + Q_OBJECT + public: + ExternalLinkFileProcess(QString server, bool useHardLinks, QObject* parent = nullptr) + : QThread(parent), m_useHardLinks(useHardLinks), m_server(server) + {} + + void run() override + { + runLinkFile(); + emit processExited(); + } + + signals: + void processExited(); + + private: + void runLinkFile(); + + bool m_useHardLinks = false; + + QString m_server; +}; + +/** + * @brief links (a file / a directory and it's contents) from src to dest + */ +class create_link : public QObject { + Q_OBJECT + public: + create_link(const QList<LinkPair> path_pairs, QObject* parent = nullptr) : QObject(parent) { m_path_pairs.append(path_pairs); } + create_link(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) + { + LinkPair pair = { src, dst }; + m_path_pairs.append(pair); + } + create_link& useHardLinks(const bool useHard) + { + m_useHardLinks = useHard; + return *this; + } + create_link& matcher(const IPathMatcher* filter) + { + m_matcher = filter; + return *this; + } + create_link& whitelist(bool whitelist) + { + m_whitelist = whitelist; + return *this; + } + create_link& linkRecursively(bool recursive) + { + m_recursive = recursive; + return *this; + } + create_link& setMaxDepth(int depth) + { + m_max_depth = depth; + return *this; + } + create_link& debug(bool d) + { + m_debug = d; + return *this; + } + + std::error_code getOSError() { return m_os_err; } + + bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } + + int totalLinked() { return m_linked; } + + void runPrivileged() { runPrivileged(QString()); } + void runPrivileged(const QString& offset); + + QList<LinkResult> getResults() { return m_path_results; } + + signals: + void fileLinked(const QString& srcName, const QString& dstName); + void linkFailed(const QString& srcName, const QString& dstName, const QString& err_msg, int err_value); + void finished(); + void finishedPrivileged(bool gotResults); + + private: + bool operator()(const QString& offset, bool dryRun = false); + void make_link_list(const QString& offset); + bool make_links(); + + private: + bool m_useHardLinks = false; + const IPathMatcher* m_matcher = nullptr; + bool m_whitelist = false; + bool m_recursive = true; + + /// @brief >= -1 = infinite, 0 = link files at src/* to dest/*, 1 = link files at src/*/* to dest/*/*, etc. + int m_max_depth = -1; + + QList<LinkPair> m_path_pairs; + QList<LinkResult> m_path_results; + QList<LinkPair> m_links_to_make; + + int m_linked; + bool m_debug = false; + std::error_code m_os_err; + + QLocalServer m_linkServer; +}; + /** * @brief moves a file by renaming it * @param source source file path * @param dest destination filepath - * + * */ -bool move(const QString& source, const QString& dest); +bool move(const QString& source, const QString& dest); /** * Delete a folder recursively @@ -138,13 +266,30 @@ bool deletePath(QString path); /** * Trash a folder / file */ -bool trash(QString path, QString *pathInTrash = nullptr); +bool trash(QString path, QString* pathInTrash = nullptr); QString PathCombine(const QString& path1, const QString& path2); QString PathCombine(const QString& path1, const QString& path2, const QString& path3); QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); -QString AbsolutePath(QString path); +QString AbsolutePath(const QString& path); + +/** + * @brief depth of path. "foo.txt" -> 0 , "bar/foo.txt" -> 1, /baz/bar/foo.txt -> 2, etc. + * + * @param path path to measure + * @return int number of components before base path + */ +int pathDepth(const QString& path); + +/** + * @brief cut off segments of path until it is a max of length depth + * + * @param path path to truncate + * @param depth max depth of new path + * @return QString truncated path + */ +QString pathTruncate(const QString& path, int depth); /** * Resolve an executable @@ -186,4 +331,194 @@ 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, + REFS, + EXT, + EXT_2_OLD, + EXT_2_3_4, + XFS, + BTRFS, + NFS, + ZFS, + APFS, + HFS, + HFSPLUS, + HFSX, + FUSEBLK, + F2FS, + UNKNOWN +}; + +/** + * @brief Ordered Mapping of enum types to reported filesystem names + * this mapping is non exsaustive, it just attempts to capture the filesystems which could be reasonalbly be in use . + * all string values are in uppercase, use `QString.toUpper()` or equivalent during lookup. + * + * QMap is ordered + * + */ +static const QMap<FilesystemType, QStringList> s_filesystem_type_names = { + {FilesystemType::FAT, { "FAT" }}, + {FilesystemType::NTFS, { "NTFS" }}, + {FilesystemType::REFS, { "REFS" }}, + {FilesystemType::EXT_2_OLD, { "EXT_2_OLD", "EXT2_OLD" }}, + {FilesystemType::EXT_2_3_4, { "EXT2/3/4", "EXT_2_3_4", "EXT2", "EXT3", "EXT4" }}, + {FilesystemType::EXT, { "EXT" }}, + {FilesystemType::XFS, { "XFS" }}, + {FilesystemType::BTRFS, { "BTRFS" }}, + {FilesystemType::NFS, { "NFS" }}, + {FilesystemType::ZFS, { "ZFS" }}, + {FilesystemType::APFS, { "APFS" }}, + {FilesystemType::HFS, { "HFS" }}, + {FilesystemType::HFSPLUS, { "HFSPLUS" }}, + {FilesystemType::HFSX, { "HFSX" }}, + {FilesystemType::FUSEBLK, { "FUSEBLK" }}, + {FilesystemType::F2FS, { "F2FS" }}, + {FilesystemType::UNKNOWN, { "UNKNOWN" }} +}; + +/** + * @brief Get the string name of Filesystem enum object + * + * @param type + * @return QString + */ +QString getFilesystemTypeName(FilesystemType type); + +/** + * @brief Get the Filesystem enum object from a name + * Does a lookup of the type name and returns an exact match + * + * @param name + * @return FilesystemType + */ +FilesystemType getFilesystemType(const QString& name); + +/** + * @brief Get the Filesystem enum object from a name + * Does a fuzzy lookup of the type name and returns an apropreate match + * + * @param name + * @return FilesystemType + */ +FilesystemType getFilesystemTypeFuzzy(const QString& name); + +struct FilesystemInfo { + FilesystemType fsType = FilesystemType::UNKNOWN; + QString fsTypeName; + int blockSize; + qint64 bytesAvailable; + qint64 bytesFree; + qint64 bytesTotal; + QString name; + QString rootPath; +}; + +/** + * @brief path to the near ancestor that exists + * + */ +QString nearestExistentAncestor(const QString& path); + +/** + * @brief colect information about the filesystem under a file + * + */ +FilesystemInfo statFS(const QString& path); + +static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, + FilesystemType::XFS, FilesystemType::REFS }; + +/** + * @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 Filesystems are 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); + +#if defined(Q_OS_WIN) +bool win_ioctl_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, +}; + +/** + * @brief if the Filesystem is symlink capable + * + */ +bool canLinkOnFS(const QString& path); +bool canLinkOnFS(const FilesystemInfo& info); +bool canLinkOnFS(FilesystemType type); + +/** + * @brief if the Filesystem is symlink capable on both ends + * + */ +bool canLink(const QString& src, const QString& dst); + +uintmax_t hardLinkCount(const QString& path); + +} // namespace FS diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index 7b93a516..0650002b 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -16,9 +16,14 @@ bool InstanceCopyPrefs::allTrue() const copyScreenshots; } + // Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const { + return getSelectedFiltersAsRegex({}); +} +QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const +{ QStringList filters; if(!copySaves) @@ -42,6 +47,10 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const if(!copyScreenshots) filters << "screenshots"; + for (auto filter : additionalFilters) { + filters << filter; + } + // If we have any filters to add, join them as a single regex string to return: if (!filters.isEmpty()) { const QString MC_ROOT = "[.]?minecraft/"; @@ -93,6 +102,31 @@ bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const return copyScreenshots; } +bool InstanceCopyPrefs::isUseSymLinksEnabled() const +{ + return useSymLinks; +} + +bool InstanceCopyPrefs::isUseHardLinksEnabled() const +{ + return useHardLinks; +} + +bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const +{ + return linkRecursively; +} + +bool InstanceCopyPrefs::isDontLinkSavesEnabled() const +{ + return dontLinkSaves; +} + +bool InstanceCopyPrefs::isUseCloneEnabled() const +{ + return useClone; +} + // ======= Setters ======= void InstanceCopyPrefs::enableCopySaves(bool b) { @@ -133,3 +167,28 @@ void InstanceCopyPrefs::enableCopyScreenshots(bool b) { copyScreenshots = b; } + +void InstanceCopyPrefs::enableUseSymLinks(bool b) +{ + useSymLinks = b; +} + +void InstanceCopyPrefs::enableLinkRecursively(bool b) +{ + linkRecursively = b; +} + +void InstanceCopyPrefs::enableUseHardLinks(bool b) +{ + useHardLinks = b; +} + +void InstanceCopyPrefs::enableDontLinkSaves(bool b) +{ + dontLinkSaves = b; +} + +void InstanceCopyPrefs::enableUseClone(bool b) +{ + useClone = b; +}
\ No newline at end of file diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 6988b2df..c7bde068 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -10,6 +10,7 @@ struct InstanceCopyPrefs { public: [[nodiscard]] bool allTrue() const; [[nodiscard]] QString getSelectedFiltersAsRegex() const; + [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters [[nodiscard]] bool isCopySavesEnabled() const; [[nodiscard]] bool isKeepPlaytimeEnabled() const; @@ -19,6 +20,11 @@ struct InstanceCopyPrefs { [[nodiscard]] bool isCopyServersEnabled() const; [[nodiscard]] bool isCopyModsEnabled() const; [[nodiscard]] bool isCopyScreenshotsEnabled() const; + [[nodiscard]] bool isUseSymLinksEnabled() const; + [[nodiscard]] bool isLinkRecursivelyEnabled() const; + [[nodiscard]] bool isUseHardLinksEnabled() const; + [[nodiscard]] bool isDontLinkSavesEnabled() const; + [[nodiscard]] bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); @@ -28,6 +34,11 @@ struct InstanceCopyPrefs { void enableCopyServers(bool b); void enableCopyMods(bool b); void enableCopyScreenshots(bool b); + void enableUseSymLinks(bool b); + void enableLinkRecursively(bool b); + void enableUseHardLinks(bool b); + void enableDontLinkSaves(bool b); + void enableUseClone(bool b); protected: // data bool copySaves = true; @@ -38,4 +49,9 @@ struct InstanceCopyPrefs { bool copyServers = true; bool copyMods = true; bool copyScreenshots = true; + bool useSymLinks = false; + bool linkRecursively = false; + bool useHardLinks = false; + bool dontLinkSaves = false; + bool useClone = false; }; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 188d163b..4ac3b51a 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -1,18 +1,31 @@ #include "InstanceCopyTask.h" -#include "settings/INISettingsObject.h" +#include <QDebug> +#include <QtConcurrentRun> #include "FileSystem.h" #include "NullInstance.h" #include "pathmatcher/RegexpMatcher.h" -#include <QtConcurrentRun> +#include "settings/INISettingsObject.h" InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); + m_useLinks = prefs.isUseSymLinksEnabled(); + m_linkRecursively = prefs.isLinkRecursivelyEnabled(); + m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled(); + m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled(); + m_useClone = prefs.isUseCloneEnabled(); QString filters = prefs.getSelectedFiltersAsRegex(); - if (!filters.isEmpty()) - { + if (m_useLinks || m_useHardLinks) { + if (!filters.isEmpty()) + filters += "|"; + filters += "instance.cfg"; + } + + qDebug() << "CopyFilters:" << filters; + + if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... auto matcherReal = new RegexpMatcher(filters); @@ -25,11 +38,78 @@ void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{ - FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher.get()); + auto copySaves = [&]() { + FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves")); + savesCopy.followSymlinks(true); + + return savesCopy(); + }; + + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] { + if (m_useClone) { + FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); + folderClone.matcher(m_matcher.get()); + + return folderClone(); + } else if (m_useLinks || m_useHardLinks) { + FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); + int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder + folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); + + bool there_were_errors = false; + + if (!folderLink()) { +#if defined Q_OS_WIN32 + if (!m_useHardLinks) { + qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; + + qDebug() << "attempting to run with privelage"; + + QEventLoop loop; + bool got_priv_results = false; + + connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { + if (!gotResults) { + qDebug() << "Privileged run exited without results!"; + } + got_priv_results = gotResults; + loop.quit(); + }); + folderLink.runPrivileged(); + + loop.exec(); // wait for the finished signal + + for (auto result : folderLink.getResults()) { + if (result.err_value != 0) { + there_were_errors = true; + } + } + + if (m_copySaves) { + there_were_errors |= !copySaves(); + } + + return got_priv_results && !there_were_errors; + } else { + qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); + } +#else + qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); +#endif + return false; + } + + if (m_copySaves) { + there_were_errors |= !copySaves(); + } + + return !there_were_errors; + } else { + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).matcher(m_matcher.get()); - return folderCopy(); + return folderCopy(); + } }); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted); @@ -39,8 +119,7 @@ void InstanceCopyTask::executeTask() void InstanceCopyTask::copyFinished() { auto successful = m_copyFuture.result(); - if(!successful) - { + if (!successful) { emitFailed(tr("Instance folder copy failed.")); return; } @@ -50,9 +129,11 @@ void InstanceCopyTask::copyFinished() InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); - if(!m_keepPlaytime) { + if (!m_keepPlaytime) { inst->resetTimePlayed(); } + if (m_useLinks) + inst->addLinkedInstanceId(m_origInstance->id()); emitSucceeded(); } diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 1f29b854..aea9d99a 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -30,4 +30,9 @@ private: QFutureWatcher<bool> m_copyFutureWatcher; std::unique_ptr<IPathMatcher> m_matcher; bool m_keepPlaytime; + bool m_useLinks = false; + bool m_useHardLinks = false; + bool m_copySaves = false; + bool m_linkRecursively = false; + bool m_useClone = false; }; diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 5f98a184..b4c520cd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -129,6 +129,16 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const return mimeData; } +QStringList InstanceList::getLinkedInstancesById(const QString &id) const +{ + QStringList linkedInstances; + for (auto inst : m_instances) { + if (inst->isLinkedToInstanceId(id)) + linkedInstances.append(inst->id()); + } + return linkedInstances; +} + int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); @@ -867,7 +877,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task) QString InstanceList::getStagedInstancePath() { - QString key = QUuid::createUuid().toString(); + QString key = QUuid::createUuid().toString(QUuid::WithoutBraces); QString tempDir = ".LAUNCHER_TEMP/"; QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index edacba3c..48bede07 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -154,6 +154,8 @@ public: QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; + QStringList getLinkedInstancesById(const QString &id) const; + signals: void dataIsInvalid(); void instancesChanged(); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 1eda43fe..1a336375 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -94,20 +94,28 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe return true; } -bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files) +bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks) { QDir directory(dir); if (!directory.exists()) return false; for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); - if( !JlCompress::compressFile(zip, e.absoluteFilePath(), filePath)) return false; + auto srcPath = e.absoluteFilePath(); + if (followSymlinks) { + if (e.isSymLink()) { + srcPath = e.symLinkTarget(); + } else { + srcPath = e.canonicalFilePath(); + } + } + if( !JlCompress::compressFile(zip, srcPath, filePath)) return false; } return true; } -bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files) +bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) { QuaZip zip(fileCompressed); QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); @@ -116,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList return false; } - auto result = compressDirFiles(&zip, dir, files); + auto result = compressDirFiles(&zip, dir, files, followSymlinks); zip.close(); if(zip.getZipError()!=0) { diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 81f9cb90..2a78f830 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -59,18 +59,20 @@ namespace MMCZip * \param zip target archive * \param dir directory that will be compressed (to compress with relative paths) * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data * \return true for success or false for failure */ - bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files); + bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks = false); /** * Compress directory, by providing a list of files to compress * \param fileCompressed target archive file * \param dir directory that will be compressed (to compress with relative paths) * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data * \return true for success or false for failure */ - bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files); + bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); /** * take a source jar, add mods to it, resulting in target jar diff --git a/launcher/QVariantUtils.h b/launcher/QVariantUtils.h new file mode 100644 index 00000000..7e422c3e --- /dev/null +++ b/launcher/QVariantUtils.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 flowln <flowlnlnln@gmail.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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + + +#include <QVariant> +#include <QList> + +namespace QVariantUtils { + +template <typename T> +inline QList<T> toList(QVariant src) { + QVariantList variantList = src.toList(); + + QList<T> list_t; + list_t.reserve(variantList.size()); + for (const QVariant& v : variantList) + { + list_t.append(v.value<T>()); + } + return list_t; +} + +template <typename T> +inline QVariant fromList(QList<T> val) { + QVariantList variantList; + variantList.reserve(val.size()); + for (const T& v : val) + { + variantList.append(v); + } + + return variantList; +} + +}
\ No newline at end of file diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index 5d9e32b6..84820eb0 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -1,7 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 flowln <flowlnlnln@gmail.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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "StringUtils.h" -#include <cmath> #include <QRegularExpression> +#include <QUuid> +#include <cmath> /// If you're wondering where these came from exactly, then know you're not the only one =D @@ -78,7 +115,7 @@ int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSe return QString::compare(s1, s2, cs); } -QString StringUtils::truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit) +QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit) { auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments; auto str_url = url.toDisplayString(display_options); @@ -120,18 +157,18 @@ QString StringUtils::truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_ } return url_compact; - } -static const QStringList s_units_si {"KB", "MB", "GB", "TB"}; -static const QStringList s_units_kibi {"KiB", "MiB", "Gib", "TiB"}; +static const QStringList s_units_si{ "KB", "MB", "GB", "TB" }; +static const QStringList s_units_kibi{ "KiB", "MiB", "Gib", "TiB" }; -QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points) { +QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points) +{ const QStringList units = use_si ? s_units_si : s_units_kibi; const int scale = use_si ? 1000 : 1024; int u = -1; - double r = pow(10, decimal_points); + double r = pow(10, decimal_points); do { bytes /= scale; @@ -139,4 +176,9 @@ QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decima } while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1); return QString::number(bytes, 'f', 2) + " " + units[u]; -}
\ No newline at end of file +} + +QString StringUtils::getRandomAlphaNumeric() +{ + return QUuid::createUuid().toString(QUuid::Id128); +} diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index 271e5099..f90a6ac7 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 flowln <flowlnlnln@gmail.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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> @@ -41,4 +77,6 @@ QString truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit = false QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); + +QString getRandomAlphaNumeric(); } // namespace StringUtils diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp new file mode 100644 index 00000000..c9599b82 --- /dev/null +++ b/launcher/filelink/FileLink.cpp @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * 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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +#include "FileLink.h" +#include "BuildConfig.h" + +#include "StringUtils.h" + +#include <iostream> + +#include <QAccessible> +#include <QCommandLineParser> + +#include <QDebug> + +#include <DesktopServices.h> + +#include <sys.h> + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <stdio.h> +#include <windows.h> +#endif + +// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header + +#ifdef __APPLE__ +#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ + +#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) +#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) +#define GHC_USE_STD_FS +#include <filesystem> +namespace fs = std::filesystem; +#endif // MacOS min version check +#endif // Other OSes version check + +#ifndef GHC_USE_STD_FS +#include <ghc/filesystem.hpp> +namespace fs = ghc::filesystem; +#endif + +FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) +{ +#if defined Q_OS_WIN32 + // attach the parent console + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + // if attach succeeds, reopen and sync all the i/o + if (freopen("CON", "w", stdout)) { + std::cout.sync_with_stdio(); + } + if (freopen("CON", "w", stderr)) { + std::cerr.sync_with_stdio(); + } + if (freopen("CON", "r", stdin)) { + std::cin.sync_with_stdio(); + } + auto out = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD written; + const char* endline = "\n"; + WriteConsole(out, endline, strlen(endline), &written, NULL); + consoleAttached = true; + } +#endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); + + // Commandline parsing + QCommandLineParser parser; + parser.setApplicationDescription(QObject::tr("a batch MKLINK program for windows to be used with prismlauncher")); + + parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" }, + { { "H", "hard" }, "use hard links instead of symbolic", "true/false" } }); + parser.addHelpOption(); + parser.addVersionOption(); + + parser.process(arguments()); + + QString serverToJoin = parser.value("server"); + m_useHardLinks = QVariant(parser.value("hard")).toBool(); + + qDebug() << "link program launched"; + + if (!serverToJoin.isEmpty()) { + qDebug() << "joining server" << serverToJoin; + joinServer(serverToJoin); + } else { + qDebug() << "no server to join"; + exit(); + } +} + +void FileLinkApp::joinServer(QString server) +{ + blockSize = 0; + + in.setDevice(&socket); + + connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; }); + + connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); + + connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { + switch (socketError) { + case QLocalSocket::ServerNotFoundError: + qDebug() + << ("The host was not found. Please make sure " + "that the server is running and that the " + "server name is correct."); + break; + case QLocalSocket::ConnectionRefusedError: + qDebug() + << ("The connection was refused by the peer. " + "Make sure the server is running, " + "and check that the server name " + "is correct."); + break; + case QLocalSocket::PeerClosedError: + qDebug() << ("The connection was closed by the peer. "); + break; + default: + qDebug() << "The following error occurred: " << socket.errorString(); + } + }); + + connect(&socket, &QLocalSocket::disconnected, this, [&]() { + qDebug() << "disconnected from server, should exit"; + exit(); + }); + + socket.connectToServer(server); +} + +void FileLinkApp::runLink() +{ + std::error_code os_err; + + qDebug() << "creating links"; + + for (auto link : m_links_to_make) { + QString src_path = link.src; + QString dst_path = link.dst; + + FS::ensureFilePathExists(dst_path); + if (m_useHardLinks) { + qDebug() << "making hard link:" << src_path << "to" << dst_path; + fs::create_hard_link(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); + } else if (fs::is_directory(StringUtils::toStdString(src_path))) { + qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; + fs::create_directory_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); + } else { + qDebug() << "making symlink:" << src_path << "to" << dst_path; + fs::create_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); + } + + if (os_err) { + qWarning() << "Failed to link files:" << QString::fromStdString(os_err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + qDebug() << "Error category:" << os_err.category().name(); + qDebug() << "Error code:" << os_err.value(); + + FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() }; + m_path_results.append(result); + } else { + FS::LinkResult result = { src_path, dst_path }; + m_path_results.append(result); + } + } + + sendResults(); + qDebug() << "done, should exit soon"; +} + +void FileLinkApp::sendResults() +{ + // construct block of data to send + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + + qint32 blocksize = quint32(sizeof(quint32)); + for (auto result : m_path_results) { + blocksize += quint32(result.src.size()); + blocksize += quint32(result.dst.size()); + blocksize += quint32(result.err_msg.size()); + blocksize += quint32(sizeof(quint32)); + } + qDebug() << "About to write block of size:" << blocksize; + out << blocksize; + + out << quint32(m_path_results.length()); + for (auto result : m_path_results) { + out << result.src; + out << result.dst; + out << result.err_msg; + out << quint32(result.err_value); + } + + qint64 byteswritten = socket.write(block); + bool bytesflushed = socket.flush(); + qDebug() << "block flushed" << byteswritten << bytesflushed; +} + +void FileLinkApp::readPathPairs() +{ + m_links_to_make.clear(); + qDebug() << "Reading path pairs from server"; + qDebug() << "bytes available" << socket.bytesAvailable(); + if (blockSize == 0) { + // Relies on the fact that QDataStream serializes a quint32 into + // sizeof(quint32) bytes + if (socket.bytesAvailable() < (int)sizeof(quint32)) + return; + qDebug() << "reading block size"; + in >> blockSize; + } + qDebug() << "blocksize is" << blockSize; + qDebug() << "bytes available" << socket.bytesAvailable(); + if (socket.bytesAvailable() < blockSize || in.atEnd()) + return; + + quint32 numLinks; + in >> numLinks; + qDebug() << "numLinks" << numLinks; + + for (int i = 0; i < numLinks; i++) { + FS::LinkPair pair; + in >> pair.src; + in >> pair.dst; + qDebug() << "link" << pair.src << "to" << pair.dst; + m_links_to_make.append(pair); + } + + runLink(); +} + +FileLinkApp::~FileLinkApp() +{ + qDebug() << "link program shutting down"; + // Shut down logger by setting the logger function to nothing + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + // Detach from Windows console + if (consoleAttached) { + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); + } +#endif +} diff --git a/launcher/filelink/FileLink.h b/launcher/filelink/FileLink.h new file mode 100644 index 00000000..4c47d9bb --- /dev/null +++ b/launcher/filelink/FileLink.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * 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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +#pragma once + +#include <QtCore> + +#include <QApplication> +#include <QDataStream> +#include <QDateTime> +#include <QDebug> +#include <QFlag> +#include <QIcon> +#include <QLocalSocket> +#include <QUrl> +#include <memory> + +#define PRISM_EXTERNAL_EXE +#include "FileSystem.h" + +class FileLinkApp : public QCoreApplication { + // friends for the purpose of limiting access to deprecated stuff + Q_OBJECT + public: + FileLinkApp(int& argc, char** argv); + virtual ~FileLinkApp(); + + private: + void joinServer(QString server); + void readPathPairs(); + void runLink(); + void sendResults(); + + bool m_useHardLinks = false; + + QDateTime m_startTime; + QLocalSocket socket; + QDataStream in; + quint32 blockSize; + + QList<FS::LinkPair> m_links_to_make; + QList<FS::LinkResult> m_path_results; + +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + bool consoleAttached = false; +#endif +}; diff --git a/launcher/filelink/filelink.exe.manifest b/launcher/filelink/filelink.exe.manifest new file mode 100644 index 00000000..239aa978 --- /dev/null +++ b/launcher/filelink/filelink.exe.manifest @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <application xmlns="urn:schemas-microsoft-com:asm.v3"> + <windowsSettings> + </windowsSettings> + </application> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10, Windows 11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <!-- Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <!-- Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> + </compatibility> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> + <security> + <requestedPrivileges> + <requestedExecutionLevel + level="requireAdministrator" + uiAccess="false"/> + </requestedPrivileges> + </security> + </trustInfo> +</assembly> diff --git a/launcher/filelink/main.cpp b/launcher/filelink/main.cpp new file mode 100644 index 00000000..83566a3c --- /dev/null +++ b/launcher/filelink/main.cpp @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * 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 + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +#include "FileLink.h" + +int main(int argc, char* argv[]) +{ + FileLinkApp ldh(argc, argv); + + return ldh.exec(); +} diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index b2171a85..35bef05e 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1114,7 +1114,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), shared_from_this(), is_indexed)); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1126,7 +1126,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), shared_from_this(), is_indexed)); m_core_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1138,7 +1138,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() const if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), is_indexed, false)); + m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), shared_from_this(), is_indexed, false)); m_nil_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1149,7 +1149,7 @@ std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() c { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), shared_from_this())); } return m_resource_pack_list; } @@ -1158,7 +1158,7 @@ std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() con { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), shared_from_this())); } return m_texture_pack_list; } @@ -1167,7 +1167,7 @@ std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const { if (!m_shader_pack_list) { - m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir())); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), shared_from_this())); } return m_shader_pack_list; } @@ -1176,7 +1176,7 @@ std::shared_ptr<WorldList> MinecraftInstance::worldList() const { if (!m_world_list) { - m_world_list.reset(new WorldList(worldDir())); + m_world_list.reset(new WorldList(worldDir(), shared_from_this())); } return m_world_list; } diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index d310f8b9..54fb9434 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -56,6 +56,8 @@ #include <optional> +#include "FileSystem.h" + using std::optional; using std::nullopt; @@ -567,3 +569,25 @@ bool World::operator==(const World &other) const { return is_valid == other.is_valid && folderName() == other.folderName(); } + +bool World::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool World::isMoreThanOneHardLink() const +{ + if (m_containerFile.isDir()) + { + return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; + } + return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; +} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 8327253a..10328cce 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -95,6 +95,21 @@ public: // WEAK compare operator - used for replacing worlds bool operator==(const World &other) const; + [[nodiscard]] auto isSymLink() const -> bool{ return m_containerFile.isSymLink(); } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + + [[nodiscard]] bool isMoreThanOneHardLink() const; + + QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } + private: void readFromZip(const QFileInfo &file); void readFromFS(const QFileInfo &file); diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index ae29a972..62e55cd4 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -45,8 +45,8 @@ #include <QFileSystemWatcher> #include <QDebug> -WorldList::WorldList(const QString &dir) - : QAbstractListModel(), m_dir(dir) +WorldList::WorldList(const QString &dir, std::shared_ptr<const BaseInstance> instance) + : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); @@ -128,6 +128,10 @@ bool WorldList::isValid() return m_dir.exists() && m_dir.isReadable(); } +QString WorldList::instDirPath() const { + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + bool WorldList::deleteWorld(int index) { if (index >= worlds.size() || index < 0) @@ -173,7 +177,7 @@ bool WorldList::resetIcon(int row) int WorldList::columnCount(const QModelIndex &parent) const { - return parent.isValid()? 0 : 4; + return parent.isValid()? 0 : 5; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -207,6 +211,14 @@ QVariant WorldList::data(const QModelIndex &index, int role) const case SizeColumn: return locale.formattedDataSize(world.bytes()); + case InfoColumn: + if (world.isSymLinkUnder(instDirPath())) { + return tr("This world is symbolically linked from elsewhere."); + } + if (world.isMoreThanOneHardLink()) { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; default: return QVariant(); } @@ -222,7 +234,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: - { + { + if (column == InfoColumn) { + if (world.isSymLinkUnder(instDirPath())) { + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1").arg(world.canonicalFilePath()); + } + if (world.isMoreThanOneHardLink()) { + return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } return world.folderName(); } case ObjectRole: @@ -274,6 +295,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol case SizeColumn: //: World size on disk return tr("Size"); + case InfoColumn: + //: special warnings? + return tr("Info"); default: return QVariant(); } @@ -289,6 +313,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol return tr("Date and time the world was last played."); case SizeColumn: return tr("Size of the world on disk."); + case InfoColumn: + return tr("Information and warnings about the world."); default: return QVariant(); } diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 08294755..10fb4e3c 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -21,6 +21,7 @@ #include <QAbstractListModel> #include <QMimeData> #include "minecraft/World.h" +#include "BaseInstance.h" class QFileSystemWatcher; @@ -33,7 +34,8 @@ public: NameColumn, GameModeColumn, LastPlayedColumn, - SizeColumn + SizeColumn, + InfoColumn }; enum Roles @@ -48,7 +50,7 @@ public: IconFileRole }; - WorldList(const QString &dir); + WorldList(const QString &dir, std::shared_ptr<const BaseInstance> instance); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; @@ -112,6 +114,8 @@ public: return m_dir; } + QString instDirPath() const; + const QList<World> &allWorlds() const { return worlds; @@ -124,6 +128,7 @@ signals: void changed(); protected: + std::shared_ptr<const BaseInstance> m_instance; QFileSystemWatcher *m_watcher; bool is_watching; QDir m_dir; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 3f31b93c..6ae25d33 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -39,18 +39,23 @@ #include <FileSystem.h> #include <QDebug> #include <QFileSystemWatcher> +#include <QIcon> #include <QMimeData> #include <QString> +#include <QStyle> #include <QThreadPool> #include <QUrl> #include <QUuid> #include <algorithm> +#include "Application.h" + #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "modplatform/ModIndex.h" -ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), nullptr, create_dir), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance, bool is_indexed, bool create_dir) + : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) { m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; } @@ -97,8 +102,25 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: + if (column == NAME_COLUMN) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath()); + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + return {}; + } case Qt::CheckStateRole: switch (column) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 84e70db9..46f5087f 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -75,7 +75,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir, bool is_indexed = false, bool create_dir = true); + ModFolderModel(const QString &dir, std::shared_ptr<const BaseInstance> instance, bool is_indexed = false, bool create_dir = true); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 0d35d755..a0b8a4bb 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -1,6 +1,8 @@ #include "Resource.h" + #include <QRegularExpression> +#include <QFileInfo> #include "FileSystem.h" @@ -152,3 +154,21 @@ bool Resource::destroy() return FS::deletePath(m_file_info.filePath()); } + +bool Resource::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool Resource::isMoreThanOneHardLink() const +{ + return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 0c37f3a3..a5e9ae91 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -94,6 +94,19 @@ class Resource : public QObject { // Delete all files of this resource. bool destroy(); + [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + + [[nodiscard]] bool isMoreThanOneHardLink() const; + protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index f2a77c12..e1973468 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -2,17 +2,22 @@ #include <QCoreApplication> #include <QDebug> +#include <QFileInfo> +#include <QIcon> #include <QMimeData> +#include <QStyle> #include <QThreadPool> #include <QUrl> +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "tasks/Task.h" -ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent, bool create_dir) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) +ResourceFolderModel::ResourceFolderModel(QDir dir, std::shared_ptr<const BaseInstance> instance, QObject* parent, bool create_dir) + : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) { if (create_dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); @@ -22,7 +27,7 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent, bool create_ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); - connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); }); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); } ResourceFolderModel::~ResourceFolderModel() @@ -417,7 +422,26 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return {}; } case Qt::ToolTipRole: + if (column == NAME_COLUMN) { + if (at(row).isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath());; + } + if (at(row).isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + + return {}; + } case Qt::CheckStateRole: switch (column) { case ACTIVE_COLUMN: @@ -531,3 +555,7 @@ void ResourceFolderModel::enableInteraction(bool enabled) return (compare_result.first < 0); return (compare_result.first > 0); } + +QString ResourceFolderModel::instDirPath() const { + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 3bd78870..fdf5f331 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -9,6 +9,8 @@ #include "Resource.h" +#include "BaseInstance.h" + #include "tasks/Task.h" #include "tasks/ConcurrentTask.h" @@ -24,7 +26,7 @@ class QSortFilterProxyModel; class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: - ResourceFolderModel(QDir, QObject* parent = nullptr, bool create_dir = true); + ResourceFolderModel(QDir, std::shared_ptr<const BaseInstance>, QObject* parent = nullptr, bool create_dir = true); ~ResourceFolderModel() override; /** Starts watching the paths for changes. @@ -125,6 +127,8 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; + QString instDirPath() const; + public slots: void enableInteraction(bool enabled); void disableInteraction(bool disabled) { enableInteraction(!disabled); } @@ -187,6 +191,7 @@ class ResourceFolderModel : public QAbstractListModel { bool m_can_interact = true; QDir m_dir; + std::shared_ptr<const BaseInstance> m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index da4bd091..6eba4e2e 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -36,12 +36,17 @@ #include "ResourcePackFolderModel.h" +#include <QIcon> +#include <QStyle> + +#include "Application.h" #include "Version.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) +ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance) + : ResourceFolderModel(QDir(dir), instance) { m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; } @@ -78,12 +83,29 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const default: return {}; } + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + return {}; + } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } + if (column == NAME_COLUMN) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath());; + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } return m_resources[row]->internal_id(); } case Qt::CheckStateRole: diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index cb620ce2..66d5a074 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -17,7 +17,7 @@ public: NUM_COLUMNS }; - explicit ResourcePackFolderModel(const QString &dir); + explicit ResourcePackFolderModel(const QString &dir, std::shared_ptr<const BaseInstance> instance); [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index a3aa958f..6f3f2811 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -6,5 +6,7 @@ class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: - explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {} + explicit ShaderPackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance) + : ResourceFolderModel(QDir(dir), instance) + {} }; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 5a32cfaf..1e218537 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -39,7 +39,9 @@ #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" -TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} +TexturePackFolderModel::TexturePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance) + : ResourceFolderModel(QDir(dir), instance) +{} Task* TexturePackFolderModel::createUpdateTask() { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 261f83b4..246997bd 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -43,7 +43,7 @@ class TexturePackFolderModel : public ResourceFolderModel Q_OBJECT public: - explicit TexturePackFolderModel(const QString &dir); + explicit TexturePackFolderModel(const QString &dir, std::shared_ptr<const BaseInstance> instance); [[nodiscard]] Task* createUpdateTask() override; [[nodiscard]] Task* createParseTask(Resource&) override; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index da27a505..5342d693 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -242,7 +242,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents) return details; } -ModDetails ReadForgeInfo(QByteArray contents) +ModDetails ReadForgeInfo(QString fileName) { ModDetails details; // Read the data @@ -250,7 +250,7 @@ ModDetails ReadForgeInfo(QByteArray contents) details.mod_id = "Forge"; details.homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; - if (!ini.loadFile(contents)) + if (!ini.loadFile(fileName)) return details; QString major = ini.get("forge.major.number", "0").toString(); @@ -422,7 +422,7 @@ bool processZIP(Mod& mod, ProcessingLevel level) return false; } - details = ReadForgeInfo(file.readAll()); + details = ReadForgeInfo(file.getFileName()); file.close(); zip.close(); diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 733cd444..f0347cab 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 flowln <flowlnlnln@gmail.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 @@ -42,132 +43,51 @@ #include <QSaveFile> #include <QDebug> -INIFile::INIFile() -{ -} - -QString INIFile::unescape(QString orig) -{ - QString out; - QChar prev = QChar::Null; - for(auto c: orig) - { - if(prev == '\\') - { - if(c == 'n') - out += '\n'; - else if(c == 't') - out += '\t'; - else if(c == '#') - out += '#'; - else - out += c; - prev = QChar::Null; - } - else - { - if(c == '\\') - { - prev = c; - continue; - } - out += c; - prev = QChar::Null; - } - } - return out; -} +#include <QSettings> -QString INIFile::escape(QString orig) +INIFile::INIFile() { - QString out; - for(auto c: orig) - { - if(c == '\n') - out += "\\n"; - else if (c == '\t') - out += "\\t"; - else if(c == '\\') - out += "\\\\"; - else if(c == '#') - out += "\\#"; - else - out += c; - } - return out; } bool INIFile::saveFile(QString fileName) { - QByteArray outArray; + QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; + _settings_obj.setFallbacksEnabled(false); + for (Iterator iter = begin(); iter != end(); iter++) - { - QString value = iter.value().toString(); - value = escape(value); - outArray.append(iter.key().toUtf8()); - outArray.append('='); - outArray.append(value.toUtf8()); - outArray.append('\n'); - } + _settings_obj.setValue(iter.key(), iter.value()); + + _settings_obj.sync(); + + if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { + // Shouldn't be possible! + Q_ASSERT(status != QSettings::Status::FormatError); + + if (status == QSettings::Status::AccessError) + qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; - try - { - FS::write(fileName, outArray); - } - catch (const Exception &e) - { - qCritical() << e.what(); return false; } return true; } - bool INIFile::loadFile(QString fileName) { - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) + QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; + _settings_obj.setFallbacksEnabled(false); + + if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { + if (status == QSettings::Status::AccessError) + qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; + if (status == QSettings::Status::FormatError) + qCritical() << "A format error occurred (e.g. loading a malformed INI file)."; return false; - bool success = loadFile(file.readAll()); - file.close(); - return success; -} - -bool INIFile::loadFile(QByteArray file) -{ - QTextStream in(file); -#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) - in.setCodec("UTF-8"); -#endif - - QStringList lines = in.readAll().split('\n'); - for (int i = 0; i < lines.count(); i++) - { - QString &lineRaw = lines[i]; - // Ignore comments. - int commentIndex = 0; - QString line = lineRaw; - // Search for comments until no more escaped # are available - while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { - if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') { - continue; - } - line = line.left(lineRaw.indexOf('#')).trimmed(); - } - - int eqPos = line.indexOf('='); - if (eqPos == -1) - continue; - QString key = line.left(eqPos).trimmed(); - QString valueStr = line.right(line.length() - eqPos - 1).trimmed(); - - valueStr = unescape(valueStr); - - QVariant value(valueStr); - this->operator[](key) = value; } + for (auto&& key : _settings_obj.allKeys()) + insert(key, _settings_obj.value(key)); + return true; } @@ -183,3 +103,4 @@ void INIFile::set(QString key, QVariant val) { this->operator[](key) = val; } + diff --git a/launcher/settings/INIFile.h b/launcher/settings/INIFile.h index 4313e829..0d5c05eb 100644 --- a/launcher/settings/INIFile.h +++ b/launcher/settings/INIFile.h @@ -1,16 +1,37 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -19,18 +40,18 @@ #include <QVariant> #include <QIODevice> +#include <QJsonDocument> +#include <QJsonArray> + // Sectionless INI parser (for instance config files) class INIFile : public QMap<QString, QVariant> { public: explicit INIFile(); - bool loadFile(QByteArray file); bool loadFile(QString fileName); bool saveFile(QString fileName); QVariant get(QString key, QVariant def) const; void set(QString key, QVariant val); - static QString unescape(QString orig); - static QString escape(QString orig); }; diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index 6200bc3a..4d735511 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -19,6 +19,8 @@ #include <QMap> #include <QStringList> #include <QVariant> +#include <QJsonDocument> +#include <QJsonArray> #include <memory> class Setting; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 8490b292..72b7db64 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1337,6 +1337,20 @@ void MainWindow::on_actionDeleteInstance_triggered() if (response != QMessageBox::Yes) return; + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) { + response = CustomMessageBox::selectable( + this, tr("There are linked instances"), + tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "Deleting it could break the other instance(s), \n\n" + "Do you wish to proceed?", nullptr, linkedInstances.count()).arg(linkedInstances.join("\n")), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No + )->exec(); + if (response != QMessageBox::Yes) + return; + } + if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); return; diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index ba453df6..fdfae597 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -195,7 +195,7 @@ void BlockedModsDialog::watchPath(QString path, bool watch_recursive) auto to_watch = QFileInfo(path); auto to_watch_path = to_watch.canonicalFilePath(); if (m_watcher.directories().contains(to_watch_path)) - return; // don't watch the same path twice (no loops!) + return; // don't watch the same path twice (no loops!) qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path; m_watcher.addPath(to_watch_path); @@ -203,10 +203,9 @@ void BlockedModsDialog::watchPath(QString path, bool watch_recursive) if (!to_watch.isDir() || !watch_recursive) return; - QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags); while (it.hasNext()) { - QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths + QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths watchPath(watch_dir, watch_recursive); } } @@ -302,11 +301,35 @@ bool BlockedModsDialog::checkValidPath(QString path) { const QFileInfo file = QFileInfo(path); const QString filename = file.fileName(); - QString laxFilename(filename); - laxFilename.replace('+', ' '); - auto compare = [](QString fsfilename, QString metadataFilename) { - return metadataFilename.compare(fsfilename, Qt::CaseInsensitive) == 0; + auto compare = [](QString fsFilename, QString metadataFilename) { + return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0; + }; + + // super lax compare (but not fuzzy) + // convert to lowercase + // convert all speratores to whitespace + // simplify sequence of internal whitespace to a single space + // efectivly compare two strings ignoring all separators and case + auto laxCompare = [](QString fsfilename, QString metadataFilename) { + // allowed character seperators + QList<QChar> allowedSeperators = { '-', '+', '.' , '_'}; + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; }; for (auto& mod : m_mods) { @@ -314,7 +337,7 @@ bool BlockedModsDialog::checkValidPath(QString path) qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } - if (compare(laxFilename, mod.name)) { + if (laxCompare(filename, mod.name)) { qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; return true; } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 3f5122f6..d75bb5fe 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -37,18 +37,21 @@ #include <QPushButton> #include "Application.h" +#include "BuildConfig.h" #include "CopyInstanceDialog.h" #include "ui_CopyInstanceDialog.h" #include "ui/dialogs/IconPickerDialog.h" -#include "BaseVersion.h" -#include "icons/IconList.h" #include "BaseInstance.h" +#include "BaseVersion.h" +#include "DesktopServices.h" +#include "FileSystem.h" #include "InstanceList.h" +#include "icons/IconList.h" -CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) - :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) + : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) { ui->setupUi(this); resize(minimumSizeHint()); @@ -71,8 +74,7 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) groupList.push_front(""); ui->groupBox->addItems(groupList); int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); - if(index == -1) - { + if (index == -1) { index = 0; } ui->groupBox->setCurrentIndex(index); @@ -85,6 +87,35 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled()); + + ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled()); + ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled()); + + ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled()); + + auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType; + + m_cloneSupported = FS::canCloneOnFS(detectedFS); + m_linkSupported = FS::canLinkOnFS(detectedFS); + + if (m_cloneSupported) { + ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } else { + ui->cloneSupportedLabel->setText(tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } + +#if defined(Q_OS_WIN) + ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield)); + ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") + + "\n" + tr("On Windows, symbolic links may require admin permission to create.")); +#endif + + updateLinkOptions(); + updateUseCloneCheckbox(); + + auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); } CopyInstanceDialog::~CopyInstanceDialog() @@ -96,8 +127,7 @@ void CopyInstanceDialog::updateDialogState() { auto allowOK = !instName().isEmpty(); auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); - if(OkButton->isEnabled() != allowOK) - { + if (OkButton->isEnabled() != allowOK) { OkButton->setEnabled(allowOK); } } @@ -105,8 +135,7 @@ void CopyInstanceDialog::updateDialogState() QString CopyInstanceDialog::instName() const { auto result = ui->instNameTextBox->text().trimmed(); - if(result.size()) - { + if (result.size()) { return result; } return QString(); @@ -127,6 +156,11 @@ const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const return m_selectedOptions; } +void CopyInstanceDialog::help() +{ + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy"))); +} + void CopyInstanceDialog::checkAllCheckboxes(const bool& b) { ui->keepPlaytimeCheckbox->setChecked(b); @@ -147,20 +181,46 @@ void CopyInstanceDialog::updateSelectAllCheckbox() ui->selectAllCheckbox->blockSignals(false); } +void CopyInstanceDialog::updateUseCloneCheckbox() +{ + ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked()); + ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled() && !ui->symbolicLinksCheckbox->isChecked() && + !ui->hardLinksCheckbox->isChecked()); +} + +void CopyInstanceDialog::updateLinkOptions() +{ + ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); + + ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled() && + !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled() && !ui->useCloneCheckbox->isChecked()); + + bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked()); + ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked()); + ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse); + ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled()); + +#if defined(Q_OS_WIN) + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield) : QIcon()); +#endif +} + void CopyInstanceDialog::on_iconButton_clicked() { IconPickerDialog dlg(this); dlg.execWithSelection(InstIconKey); - if (dlg.result() == QDialog::Accepted) - { + if (dlg.result() == QDialog::Accepted) { InstIconKey = dlg.selectedIconKey; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); } } - -void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) { updateDialogState(); } @@ -175,10 +235,10 @@ void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { m_selectedOptions.enableCopySaves(state == Qt::Checked); + ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked()); updateSelectAllCheckbox(); } - void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); @@ -220,3 +280,38 @@ void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state) m_selectedOptions.enableCopyScreenshots(state == Qt::Checked); updateSelectAllCheckbox(); } + +void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseSymLinks(state == Qt::Checked); + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseHardLinks(state == Qt::Checked); + if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked()) { + ui->recursiveLinkCheckbox->setChecked(true); + } + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableLinkRecursively(state == Qt::Checked); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableDontLinkSaves(state == Qt::Checked); +} + +void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked)); + updateUseCloneCheckbox(); + updateLinkOptions(); +} diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index 884501d1..698c6e93 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -16,22 +16,21 @@ #pragma once #include <QDialog> +#include "BaseInstance.h" #include "BaseVersion.h" #include "InstanceCopyPrefs.h" class BaseInstance; -namespace Ui -{ +namespace Ui { class CopyInstanceDialog; } -class CopyInstanceDialog : public QDialog -{ +class CopyInstanceDialog : public QDialog { Q_OBJECT -public: - explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0); + public: + explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); ~CopyInstanceDialog(); void updateDialogState(); @@ -41,10 +40,12 @@ public: QString iconKey() const; const InstanceCopyPrefs& getChosenOptions() const; -private -slots: + public slots: + void help(); + + private slots: void on_iconButton_clicked(); - void on_instNameTextBox_textChanged(const QString &arg1); + void on_instNameTextBox_textChanged(const QString& arg1); // Checkboxes void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); @@ -55,13 +56,23 @@ slots: void on_copyServersCheckbox_stateChanged(int state); void on_copyModsCheckbox_stateChanged(int state); void on_copyScreenshotsCheckbox_stateChanged(int state); + void on_symbolicLinksCheckbox_stateChanged(int state); + void on_hardLinksCheckbox_stateChanged(int state); + void on_recursiveLinkCheckbox_stateChanged(int state); + void on_dontLinkSavesCheckbox_stateChanged(int state); + void on_useCloneCheckbox_stateChanged(int state); -private: + private: void checkAllCheckboxes(const bool& b); void updateSelectAllCheckbox(); + void updateUseCloneCheckbox(); + void updateLinkOptions(); + /* data */ - Ui::CopyInstanceDialog *ui; + Ui::CopyInstanceDialog* ui; QString InstIconKey; InstancePtr m_original; InstanceCopyPrefs m_selectedOptions; + bool m_cloneSupported = false; + bool m_linkSupported = false; }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index b7828fe3..5060debc 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -9,8 +9,8 @@ <rect> <x>0</x> <y>0</y> - <width>341</width> - <height>399</height> + <width>575</width> + <height>695</height> </rect> </property> <property name="windowTitle"> @@ -113,93 +113,268 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="selectAllButtonLayout"> - <item> - <widget class="QCheckBox" name="selectAllCheckbox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="layoutDirection"> - <enum>Qt::LeftToRight</enum> - </property> - <property name="text"> - <string>Select all</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> + <widget class="QGroupBox" name="copyOptionsGroup"> + <property name="title"> + <string>Instance Copy Options</string> + </property> + <layout class="QGridLayout" name="copyOptionsLayout"> + <item row="1" column="0"> + <widget class="QCheckBox" name="keepPlaytimeCheckbox"> + <property name="text"> + <string>Keep play time</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QCheckBox" name="copyModsCheckbox"> + <property name="toolTip"> + <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string> + </property> + <property name="text"> + <string>Copy mods</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="copyResPacksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Copy resource packs</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="copyGameOptionsCheckbox"> + <property name="toolTip"> + <string>Copy the in-game options like FOV, max framerate, etc.</string> + </property> + <property name="text"> + <string>Copy game options</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="copyShaderPacksCheckbox"> + <property name="text"> + <string>Copy shader packs</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QCheckBox" name="copyServersCheckbox"> + <property name="text"> + <string>Copy servers</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="copySavesCheckbox"> + <property name="text"> + <string>Copy saves</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="copyScreenshotsCheckbox"> + <property name="text"> + <string>Copy screenshots</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QCheckBox" name="selectAllCheckbox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Select all</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> </item> <item> - <layout class="QGridLayout" name="copyOptionsLayout"> - <item row="6" column="1"> - <widget class="QCheckBox" name="copyModsCheckbox"> - <property name="toolTip"> - <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string> - </property> - <property name="text"> - <string>Copy mods</string> - </property> - </widget> - </item> - <item row="5" column="0"> - <widget class="QCheckBox" name="copyGameOptionsCheckbox"> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="advancedOptionsLabel"> + <property name="text"> + <string>Advanced Copy Options</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="copyModeLayout"> + <item> + <widget class="QGroupBox" name="linkFilesGroup"> <property name="toolTip"> - <string>Copy the in-game options like FOV, max framerate, etc.</string> + <string>Use symbolic or hard links instead of copying files.</string> </property> - <property name="text"> - <string>Copy game options</string> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QCheckBox" name="copySavesCheckbox"> - <property name="text"> - <string>Copy saves</string> - </property> - </widget> - </item> - <item row="3" column="1"> - <widget class="QCheckBox" name="copyShaderPacksCheckbox"> - <property name="text"> - <string>Copy shader packs</string> - </property> - </widget> - </item> - <item row="5" column="1"> - <widget class="QCheckBox" name="copyServersCheckbox"> - <property name="text"> - <string>Copy servers</string> + <property name="title"> + <string>Symbolic and Hard Link Options</string> </property> - </widget> - </item> - <item row="6" column="0"> - <widget class="QCheckBox" name="copyResPacksCheckbox"> - <property name="enabled"> - <bool>true</bool> + <property name="flat"> + <bool>false</bool> </property> - <property name="text"> - <string>Copy resource packs</string> + <property name="checkable"> + <bool>false</bool> </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QCheckBox" name="keepPlaytimeCheckbox"> - <property name="text"> - <string>Keep play time</string> + <property name="checked"> + <bool>false</bool> </property> + <layout class="QVBoxLayout" name="linkOptionsLayout"> + <item> + <widget class="QLabel" name="linkOptionsLabel"> + <property name="text"> + <string>Links are supported on most filesystems except FAT</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="linkOptionsGridLayout" rowstretch="0,0,0,0" columnstretch="0,0" rowminimumheight="0,0,0,0" columnminimumwidth="0,0"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item row="2" column="1"> + <widget class="QCheckBox" name="recursiveLinkCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Link each resource individually instead of linking whole folders at once</string> + </property> + <property name="text"> + <string>Link files recursively</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="dontLinkSavesCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>If "copy saves" is selected world save data will be copied instead of linked and thus not shared between instances.</string> + </property> + <property name="text"> + <string>Don't link saves</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="hardLinksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="toolTip"> + <string>Use hard links instead of copying files.</string> + </property> + <property name="text"> + <string>Use hard links</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="symbolicLinksCheckbox"> + <property name="toolTip"> + <string>Use symbolic links instead of copying files.</string> + </property> + <property name="text"> + <string>Use symbolic links</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> </widget> </item> - <item row="1" column="1"> - <widget class="QCheckBox" name="copyScreenshotsCheckbox"> - <property name="text"> - <string>Copy screenshots</string> + <item> + <widget class="QGroupBox" name="horizontalGroupBox"> + <property name="title"> + <string>CoW (Copy-on-Write) Options</string> </property> + <layout class="QHBoxLayout" name="useCloneLayout"> + <item> + <widget class="QCheckBox" name="useCloneCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Files cloned with reflinks take up no extra space until they are modified.</string> + </property> + <property name="text"> + <string>Clone instead of copying</string> + </property> + </widget> + </item> + <item> + <spacer name="CoWSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="cloneSupportedLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Your filesystem and/or OS doesn't support reflinks</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="margin"> + <number>4</number> + </property> + </widget> + </item> + </layout> </widget> </item> </layout> @@ -210,7 +385,7 @@ <enum>Qt::Horizontal</enum> </property> <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set> </property> </widget> </item> @@ -220,10 +395,21 @@ <tabstop>iconButton</tabstop> <tabstop>instNameTextBox</tabstop> <tabstop>groupBox</tabstop> + <tabstop>keepPlaytimeCheckbox</tabstop> + <tabstop>copyScreenshotsCheckbox</tabstop> + <tabstop>copySavesCheckbox</tabstop> + <tabstop>copyShaderPacksCheckbox</tabstop> + <tabstop>copyGameOptionsCheckbox</tabstop> + <tabstop>copyServersCheckbox</tabstop> + <tabstop>copyResPacksCheckbox</tabstop> + <tabstop>copyModsCheckbox</tabstop> + <tabstop>symbolicLinksCheckbox</tabstop> + <tabstop>recursiveLinkCheckbox</tabstop> + <tabstop>hardLinksCheckbox</tabstop> + <tabstop>dontLinkSavesCheckbox</tabstop> + <tabstop>useCloneCheckbox</tabstop> </tabstops> - <resources> - <include location="../../graphics.qrc"/> - </resources> + <resources/> <connections> <connection> <sender>buttonBox</sender> @@ -232,8 +418,8 @@ <slot>accept()</slot> <hints> <hint type="sourcelabel"> - <x>254</x> - <y>316</y> + <x>269</x> + <y>692</y> </hint> <hint type="destinationlabel"> <x>157</x> @@ -248,8 +434,8 @@ <slot>reject()</slot> <hints> <hint type="sourcelabel"> - <x>322</x> - <y>316</y> + <x>337</x> + <y>692</y> </hint> <hint type="destinationlabel"> <x>286</x> diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index f13e36e8..07ec3c70 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -45,6 +45,8 @@ #include <QDebug> #include <QSaveFile> #include <QStack> +#include <QFileInfo> + #include "StringUtils.h" #include "SeparatorPrefixTree.h" #include "Application.h" @@ -429,7 +431,8 @@ bool ExportInstanceDialog::doExport() QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; } - if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files)) + + if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true)) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index cc597fe0..eca3e865 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -46,7 +46,6 @@ MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); loadSettings(); updateCheckboxStuff(); } diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 640f436d..103881b5 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>936</width> - <height>1134</height> + <height>541</height> </rect> </property> <property name="sizePolicy"> @@ -39,7 +39,7 @@ </property> <widget class="QWidget" name="minecraftTab"> <attribute name="title"> - <string notr="true">Minecraft</string> + <string notr="true">General</string> </attribute> <layout class="QVBoxLayout" name="verticalLayout_3"> <item> @@ -112,22 +112,29 @@ </widget> </item> <item> - <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox"> + <widget class="QGroupBox" name="gameTimeGroupBox"> <property name="title"> - <string>Native library workarounds</string> + <string>Game time</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> + <layout class="QVBoxLayout" name="verticalLayout_6"> <item> - <widget class="QCheckBox" name="useNativeGLFWCheck"> + <widget class="QCheckBox" name="showGameTime"> <property name="text"> - <string>Use system installation of &GLFW</string> + <string>Show time spent &playing instances</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="useNativeOpenALCheck"> + <widget class="QCheckBox" name="showGlobalGameTime"> <property name="text"> - <string>Use system installation of &OpenAL</string> + <string>Show time spent playing across &all instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="recordGameTime"> + <property name="text"> + <string>&Record time spent playing instances</string> </property> </widget> </item> @@ -135,38 +142,28 @@ </widget> </item> <item> - <widget class="QGroupBox" name="perfomanceGroupBox"> + <widget class="QGroupBox" name="groupBox"> <property name="title"> - <string>Performance</string> + <string>Miscellaneous</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QCheckBox" name="enableFeralGamemodeCheck"> - <property name="toolTip"> - <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> - </property> - <property name="text"> - <string>Enable Feral GameMode</string> - </property> - </widget> - </item> + <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QCheckBox" name="enableMangoHud"> + <widget class="QCheckBox" name="closeAfterLaunchCheck"> <property name="toolTip"> - <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> + <string><html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html></string> </property> <property name="text"> - <string>Enable MangoHud</string> + <string>&Close the launcher after game window opens</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <widget class="QCheckBox" name="quitAfterGameStopCheck"> <property name="toolTip"> - <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> </property> <property name="text"> - <string>Use discrete GPU</string> + <string>&Quit the launcher after game window closes</string> </property> </widget> </item> @@ -174,29 +171,42 @@ </widget> </item> <item> - <widget class="QGroupBox" name="gameTimeGroupBox"> + <spacer name="verticalSpacerMinecraft"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Tweaks</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox"> <property name="title"> - <string>Game time</string> + <string>Native library workarounds</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> + <layout class="QVBoxLayout" name="verticalLayout_11"> <item> - <widget class="QCheckBox" name="showGameTime"> - <property name="text"> - <string>Show time spent &playing instances</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="showGlobalGameTime"> + <widget class="QCheckBox" name="useNativeGLFWCheck"> <property name="text"> - <string>Show time spent playing across &all instances</string> + <string>Use system installation of &GLFW</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="recordGameTime"> + <widget class="QCheckBox" name="useNativeOpenALCheck"> <property name="text"> - <string>&Record time spent playing instances</string> + <string>Use system installation of &OpenAL</string> </property> </widget> </item> @@ -204,28 +214,38 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox"> + <widget class="QGroupBox" name="perfomanceGroupBox"> <property name="title"> - <string>Miscellaneous</string> + <string>Performance</string> </property> - <layout class="QVBoxLayout" name="verticalLayout"> + <layout class="QVBoxLayout" name="verticalLayout_2"> <item> - <widget class="QCheckBox" name="closeAfterLaunchCheck"> + <widget class="QCheckBox" name="enableFeralGamemodeCheck"> <property name="toolTip"> - <string><html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html></string> + <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> </property> <property name="text"> - <string>&Close the launcher after game window opens</string> + <string>Enable Feral GameMode</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="quitAfterGameStopCheck"> + <widget class="QCheckBox" name="enableMangoHud"> <property name="toolTip"> - <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> + <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> </property> <property name="text"> - <string>&Quit the launcher after game window closes</string> + <string>Enable MangoHud</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + </property> + <property name="text"> + <string>Use discrete GPU</string> </property> </widget> </item> @@ -233,14 +253,14 @@ </widget> </item> <item> - <spacer name="verticalSpacerMinecraft"> + <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> <property name="sizeHint" stdset="0"> <size> - <width>0</width> - <height>0</height> + <width>20</width> + <height>40</height> </size> </property> </spacer> @@ -255,11 +275,6 @@ <tabstop>maximizedCheckBox</tabstop> <tabstop>windowWidthSpinBox</tabstop> <tabstop>windowHeightSpinBox</tabstop> - <tabstop>useNativeGLFWCheck</tabstop> - <tabstop>useNativeOpenALCheck</tabstop> - <tabstop>enableFeralGamemodeCheck</tabstop> - <tabstop>enableMangoHud</tabstop> - <tabstop>useDiscreteGpuCheck</tabstop> </tabstops> <resources/> <connections/> diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index d4a395d9..b6ad159e 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -107,6 +107,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl auto head = ui->worldTreeView->header(); head->setSectionResizeMode(0, QHeaderView::Stretch); head->setSectionResizeMode(1, QHeaderView::ResizeToContents); + head->setSectionResizeMode(4, QHeaderView::ResizeToContents); connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); worldChanged(QModelIndex(), QModelIndex()); |