aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
authorRachel Powers <508861+Ryex@users.noreply.github.com>2023-05-07 13:21:21 -0700
committerRachel Powers <508861+Ryex@users.noreply.github.com>2023-05-07 13:21:21 -0700
commit884ac7307817e10f04512a29213a017ca344c16d (patch)
tree37bcb97dfec8516edba3501a5475b1daf314d2b7 /launcher
parent718abaae0ef465050c81c0dfba63ce9f0fff17fc (diff)
parentce5bb29c442cee3654c5f4287a999d5d6593032f (diff)
downloadPrismLauncher-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')
-rw-r--r--launcher/BaseInstance.cpp36
-rw-r--r--launcher/BaseInstance.h6
-rw-r--r--launcher/CMakeLists.txt48
-rw-r--r--launcher/DesktopServices.cpp1
-rw-r--r--launcher/FileSystem.cpp967
-rw-r--r--launcher/FileSystem.h347
-rw-r--r--launcher/InstanceCopyPrefs.cpp59
-rw-r--r--launcher/InstanceCopyPrefs.h16
-rw-r--r--launcher/InstanceCopyTask.cpp103
-rw-r--r--launcher/InstanceCopyTask.h5
-rw-r--r--launcher/InstanceList.cpp12
-rw-r--r--launcher/InstanceList.h2
-rw-r--r--launcher/MMCZip.cpp16
-rw-r--r--launcher/MMCZip.h6
-rw-r--r--launcher/QVariantUtils.h70
-rw-r--r--launcher/StringUtils.cpp58
-rw-r--r--launcher/StringUtils.h38
-rw-r--r--launcher/filelink/FileLink.cpp277
-rw-r--r--launcher/filelink/FileLink.h67
-rw-r--r--launcher/filelink/filelink.exe.manifest28
-rw-r--r--launcher/filelink/main.cpp30
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp14
-rw-r--r--launcher/minecraft/World.cpp24
-rw-r--r--launcher/minecraft/World.h15
-rw-r--r--launcher/minecraft/WorldList.cpp34
-rw-r--r--launcher/minecraft/WorldList.h9
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp24
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h2
-rw-r--r--launcher/minecraft/mod/Resource.cpp20
-rw-r--r--launcher/minecraft/mod/Resource.h13
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp32
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h7
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp24
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h2
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h4
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp4
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h2
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp6
-rw-r--r--launcher/settings/INIFile.cpp137
-rw-r--r--launcher/settings/INIFile.h47
-rw-r--r--launcher/settings/SettingsObject.h2
-rw-r--r--launcher/ui/MainWindow.cpp14
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp39
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.cpp125
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.h33
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.ui358
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp5
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp1
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui133
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp1
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 &quot;copy saves&quot; 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 &amp;GLFW</string>
+ <string>Show time spent &amp;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 &amp;OpenAL</string>
+ <string>Show time spent playing across &amp;all instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>&amp;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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically reopen when the game crashes or exits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>Enable MangoHud</string>
+ <string>&amp;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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>Use discrete GPU</string>
+ <string>&amp;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 &amp;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 &amp;all instances</string>
+ <string>Use system installation of &amp;GLFW</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="recordGameTime">
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
<property name="text">
- <string>&amp;Record time spent playing instances</string>
+ <string>Use system installation of &amp;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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically reopen when the game crashes or exits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>&amp;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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
- <string>&amp;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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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());