Restructure updating of shared directories

Signed-off-by: Naomi <103967@gmail.com>
This commit is contained in:
Naomi 2025-06-22 16:31:59 +02:00
parent 62e4845dca
commit db4e7d8820
7 changed files with 134 additions and 214 deletions

View File

@ -315,8 +315,6 @@ set(MINECRAFT_SOURCES
minecraft/Library.cpp minecraft/Library.cpp
minecraft/Library.h minecraft/Library.h
minecraft/MojangDownloadInfo.h minecraft/MojangDownloadInfo.h
minecraft/UpdateSharedDirectoriesTask.cpp
minecraft/UpdateSharedDirectoriesTask.h
minecraft/VanillaInstanceCreationTask.cpp minecraft/VanillaInstanceCreationTask.cpp
minecraft/VanillaInstanceCreationTask.h minecraft/VanillaInstanceCreationTask.h
minecraft/VersionFile.cpp minecraft/VersionFile.cpp

View File

@ -55,6 +55,8 @@
#include "DesktopServices.h" #include "DesktopServices.h"
#include "PSaveFile.h" #include "PSaveFile.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/FileConflictDialog.h"
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#define NOMINMAX #define NOMINMAX
@ -674,6 +676,44 @@ bool move(const QString& source, const QString& dest)
return true; return true;
} }
bool interactiveMove(const QString& source, const QString& destination, bool recursive /* = false */, QWidget* parent /* = nullptr */)
{
const QFileInfo sourceInfo(source);
// Make sure the source exists.
if (!sourceInfo.exists())
return false;
// Recursive doesn't make sense if the source isn't a directory.
if (recursive && sourceInfo.isDir()) {
QDirIterator sourceIt(source, QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden | QDir::Filter::NoDotAndDotDot);
while (sourceIt.hasNext()) {
if (!interactiveMove(sourceIt.next(), FS::PathCombine(destination, sourceIt.fileName()), false))
return false;
}
return true;
}
if (QFile(destination).exists()) {
FileConflictDialog dialog(source, destination, true, parent);
FileConflictDialog::Result result = dialog.execWithResult();
switch(result) {
case FileConflictDialog::Cancel:
return false;
case FileConflictDialog::ChooseDestination:
return FS::deletePath(source);
case FileConflictDialog::ChooseSource:
FS::deletePath(destination);
break;
}
}
return FS::move(source, destination);
}
bool deletePath(QString path) bool deletePath(QString path)
{ {
std::error_code err; std::error_code err;
@ -1711,4 +1751,51 @@ QString getSymLinkTarget(const QString& path)
return QFileInfo(path).symLinkTarget(); return QFileInfo(path).symLinkTarget();
} }
bool tryCreateSymlink(const QString& source, const QString& destination, const QString& symlinkName /* = "symbolic link" */) {
// Make sure that symbolic links are supported.
if (!FS::canLink(source, destination)) {
CustomMessageBox::selectable(nullptr, QObject::tr("Failed"), QObject::tr("Failed to create %1.\nSymbolic links are not supported on the filesystem").arg(symlinkName), QMessageBox::Warning, QMessageBox::Ok)->exec();
return false;
}
// Check if the destination already exists.
// If it's already a symlink, it might already be correct.
if (FS::isSymLink(destination)) {
// If the target of the symlink is already the source, there's nothing to do.
if (FS::getSymLinkTarget(destination) == source) {
return true;
}
FS::deletePath(destination);
} else if (QFileInfo::exists(destination)) {
if (!FS::checkFolderPathEmpty(destination)) {
if (!interactiveMove(destination, source, true)) {
CustomMessageBox::selectable(nullptr, QObject::tr("Failed"), QObject::tr("Failed to create %1.\nEnsure that \"%2\" is empty.").arg(symlinkName).arg(destination), QMessageBox::Warning, QMessageBox::Ok)->exec();
return false;
}
}
FS::deletePath(destination);
}
// Make sure the source folder exists
if (!FS::ensureFolderPathExists(source)) {
CustomMessageBox::selectable(nullptr, QObject::tr("Failed"), QObject::tr("Failed to create %1.\nEnsure that \"%2\" exists.").arg(symlinkName).arg(source), QMessageBox::Warning, QMessageBox::Ok)->exec();
return false;
}
FS::create_link folderLink(source, destination);
folderLink.linkRecursively(false);
if (!folderLink()) {
CustomMessageBox::selectable(nullptr, QObject::tr("Failed"), QObject::tr("Failed to create %1. Error %2: %3")
.arg(symlinkName)
.arg(folderLink.getOSError().value())
.arg(folderLink.getOSError().message().c_str()),
QMessageBox::Warning, QMessageBox::Ok)->exec();
}
return true;
}
} // namespace FS } // namespace FS

View File

@ -293,6 +293,17 @@ class create_link : public QObject {
*/ */
bool move(const QString& source, const QString& dest); bool move(const QString& source, const QString& dest);
/**
* @brief Move a file or folder, and ask the user what to do in case of a conflict.
* @param source What to move.
* @param destination Where to move it to.
* @param recursive If true, all direct children will be moved 1 by 1.
* If false, the source will be directly moved to the destination.
* @param parent The parent of the dialog.
* @return True if everything could be moved.
*/
bool interactiveMove(const QString& source, const QString& destination, bool recursive = false, QWidget* parent = nullptr);
/** /**
* Delete a folder recursively * Delete a folder recursively
*/ */
@ -587,4 +598,6 @@ bool isSymLink(const QString& path);
*/ */
QString getSymLinkTarget(const QString& path); QString getSymLinkTarget(const QString& path);
bool tryCreateSymlink(const QString& source, const QString& destination, const QString& symlinkName = "symbolic link");
} // namespace FS } // namespace FS

View File

@ -85,7 +85,6 @@
#include "AssetsUtils.h" #include "AssetsUtils.h"
#include "MinecraftLoadAndCheck.h" #include "MinecraftLoadAndCheck.h"
#include "PackProfile.h" #include "PackProfile.h"
#include "minecraft/UpdateSharedDirectoriesTask.h"
#include "minecraft/gameoptions/GameOptions.h" #include "minecraft/gameoptions/GameOptions.h"
#include "minecraft/update/FoldersTask.h" #include "minecraft/update/FoldersTask.h"
@ -1317,8 +1316,38 @@ QList<Mod*> MinecraftInstance::getJarMods() const
void MinecraftInstance::applySettings() void MinecraftInstance::applySettings()
{ {
// Shared directories // Shared directories
m_update_shared_directories_task = std::make_shared<UpdateSharedDirectoriesTask>(this); updateSharedDirectories();
m_update_shared_directories_task->start(); }
bool MinecraftInstance::updateSharedDirectories()
{
const std::vector<std::tuple<QString, QString, QString>> sharedDirectories = {
{ "UseSharedScreenshotsFolder", m_settings->get("SharedScreenshotsPath").toString(), screenshotsDir() },
{ "UseSharedSavesFolder", m_settings->get("SharedSavesPath").toString(), worldDir() },
{ "UseSharedResourcePacksFolder", m_settings->get("SharedResourcePacksPath").toString(), resourcePacksDir() },
{ "UseSharedResourcePacksFolder", m_settings->get("SharedResourcePacksPath").toString(), texturePacksDir() }
};
bool success = true;
for (const auto& [useSetting, source, destination] : sharedDirectories) {
// Create symlink if useSetting is true, remove symlink if false.
if (m_settings->get(useSetting).toBool())
{
// Try to create symlink, set setting to false if failed.
if(!FS::tryCreateSymlink(source, destination, tr("shared folder"))) {
m_settings->set(useSetting, false);
success = false;
}
}
else {
// Safety check
if (FS::isSymLink(destination)) {
success = FS::deletePath(destination) && success;
}
}
}
return success;
} }
#include "MinecraftInstance.moc" #include "MinecraftInstance.moc"

View File

@ -52,7 +52,6 @@ class WorldList;
class GameOptions; class GameOptions;
class LaunchStep; class LaunchStep;
class PackProfile; class PackProfile;
class UpdateSharedDirectoriesTask;
class MinecraftInstance : public BaseInstance { class MinecraftInstance : public BaseInstance {
Q_OBJECT Q_OBJECT
@ -165,9 +164,10 @@ class MinecraftInstance : public BaseInstance {
protected: protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
bool updateSharedDirectories();
protected: // data protected: // data
std::shared_ptr<PackProfile> m_components; std::shared_ptr<PackProfile> m_components;
std::shared_ptr<UpdateSharedDirectoriesTask> m_update_shared_directories_task;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list; mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_nil_mod_list; mutable std::shared_ptr<ModFolderModel> m_nil_mod_list;

View File

@ -1,186 +0,0 @@
#include "UpdateSharedDirectoriesTask.h"
#include <QDirIterator>
#include "Application.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/FileConflictDialog.h"
/**
* @brief Move a file or folder, and ask the user what to do in case of a conflict.
* @param source What to move.
* @param destination Where to move it to.
* @param recursive If true, all direct children will be moved 1 by 1.
* If false, the source will be directly moved to the destination.
* @param parent The parent of the dialog.
* @return True if everything could be moved.
*/
bool interactiveMove(const QString& source, const QString& destination, bool recursive = false, QWidget* parent = nullptr)
{
const QFileInfo sourceInfo(source);
// Make sure the source exists.
if (!sourceInfo.exists())
return false;
// Recursive doesn't make sense if the source isn't a directory.
if (recursive && sourceInfo.isDir()) {
QDirIterator sourceIt(source, QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden | QDir::Filter::NoDotAndDotDot);
while (sourceIt.hasNext()) {
if (!interactiveMove(sourceIt.next(), FS::PathCombine(destination, sourceIt.fileName()), false))
return false;
}
return true;
}
if (QFile(destination).exists()) {
FileConflictDialog dialog(source, destination, true, parent);
FileConflictDialog::Result result = dialog.execWithResult();
switch(result) {
case FileConflictDialog::Cancel:
return false;
case FileConflictDialog::ChooseDestination:
return FS::deletePath(source);
case FileConflictDialog::ChooseSource:
FS::deletePath(destination);
break;
}
}
return FS::move(source, destination);
}
class TryCreateSymlinkTask : public Task {
public:
explicit TryCreateSymlinkTask(const QString& source,
const QString& destination,
MinecraftInstance* instance,
const QString& setting)
: m_source(source), m_destination(destination), m_inst(instance), m_setting(setting)
{
setObjectName("TryCreateSymlinkTask");
}
virtual ~TryCreateSymlinkTask() {}
protected:
void executeTask()
{
bool create = m_inst->settings()->get(m_setting).toBool();
// Check if we have to delete an existing symlink
if (!create) {
// Safety check
if (FS::isSymLink(m_destination)) {
FS::deletePath(m_destination);
}
emitSucceeded();
return;
}
// Make sure that symbolic links are supported.
if (!FS::canLink(m_source, m_destination)) {
fail(tr("Failed to create shared folder.\nSymbolic links are not supported on the filesystem"));
return;
}
// Check if the destination already exists.
// If it's already a symlink, it might already be correct.
if (FS::isSymLink(m_destination)) {
// If the target of the symlink is already the source, there's nothing to do.
if (FS::getSymLinkTarget(m_destination) == m_source) {
emitSucceeded();
return;
}
FS::deletePath(m_destination);
} else if (QFileInfo::exists(m_destination)) {
if (!FS::checkFolderPathEmpty(m_destination)) {
if (!interactiveMove(m_destination, m_source, true)) {
fail(tr("Failed to create shared folder.\nEnsure that \"%1\" is empty.").arg(m_destination));
return;
}
}
FS::deletePath(m_destination);
}
// Make sure the source folder exists
if (!FS::ensureFolderPathExists(m_source)) {
fail(tr("Failed to create shared folder.\nEnsure that \"%1\" exists.").arg(m_source));
return;
}
FS::create_link folderLink(m_source, m_destination);
folderLink.linkRecursively(false);
if (folderLink()) {
emitSucceeded();
} else {
fail(tr("Failed to create shared folder. Error %1: %2")
.arg(folderLink.getOSError().value())
.arg(folderLink.getOSError().message().c_str()));
}
return;
}
void fail(const QString& reason)
{
m_inst->settings()->set(m_setting, false);
emitFailed(reason);
}
private:
QString m_source;
QString m_destination;
MinecraftInstance* m_inst;
QString m_setting;
};
UpdateSharedDirectoriesTask::UpdateSharedDirectoriesTask(MinecraftInstance* inst)
: Task(), m_inst(inst)
{}
void UpdateSharedDirectoriesTask::executeTask()
{
auto tasks = makeShared<ConcurrentTask>("UpdateSharedDirectoriesTask");
auto screenshotsTask = makeShared<TryCreateSymlinkTask>(m_inst->settings()->get("SharedScreenshotsPath").toString(),
m_inst->screenshotsDir(), m_inst, "UseSharedScreenshotsFolder");
connect(screenshotsTask.get(), &Task::failed, this, &UpdateSharedDirectoriesTask::notifyFailed);
tasks->addTask(screenshotsTask);
auto savesTask = makeShared<TryCreateSymlinkTask>(m_inst->settings()->get("SharedSavesPath").toString(), m_inst->worldDir(), m_inst,
"UseSharedSavesFolder");
connect(savesTask.get(), &Task::failed, this, &UpdateSharedDirectoriesTask::notifyFailed);
tasks->addTask(savesTask);
auto resoucePacksTask = makeShared<TryCreateSymlinkTask>(m_inst->settings()->get("SharedResourcePacksPath").toString(),
m_inst->resourcePacksDir(), m_inst, "UseSharedResourcePacksFolder");
connect(resoucePacksTask.get(), &Task::failed, this, &UpdateSharedDirectoriesTask::notifyFailed);
tasks->addTask(resoucePacksTask);
auto texturePacksTask = makeShared<TryCreateSymlinkTask>(m_inst->settings()->get("SharedResourcePacksPath").toString(),
m_inst->texturePacksDir(), m_inst, "UseSharedResourcePacksFolder");
connect(texturePacksTask.get(), &Task::failed, this, &UpdateSharedDirectoriesTask::notifyFailed);
tasks->addTask(texturePacksTask);
m_tasks = tasks;
connect(m_tasks.get(), &Task::succeeded, this, &UpdateSharedDirectoriesTask::emitSucceeded);
m_tasks->start();
}
void UpdateSharedDirectoriesTask::notifyFailed(QString reason)
{
CustomMessageBox::selectable(nullptr, tr("Failed"), reason, QMessageBox::Warning, QMessageBox::Ok)->exec();
emit failed(reason);
}

View File

@ -1,21 +0,0 @@
#pragma once
#include "tasks/Task.h"
class MinecraftInstance;
class UpdateSharedDirectoriesTask : public Task {
public:
explicit UpdateSharedDirectoriesTask(MinecraftInstance* inst);
virtual ~UpdateSharedDirectoriesTask = default;
protected:
virtual void executeTask() override;
protected slots:
void notifyFailed(QString reason);
private:
MinecraftInstance* m_inst;
Task::Ptr m_tasks;
};