aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--launcher/Application.cpp119
-rw-r--r--launcher/Application.h1
-rw-r--r--launcher/CMakeLists.txt3
-rw-r--r--launcher/DataMigrationTask.cpp96
-rw-r--r--launcher/DataMigrationTask.h42
-rw-r--r--launcher/FileSystem.cpp12
-rw-r--r--launcher/FileSystem.h30
-rw-r--r--launcher/InstanceCopyTask.cpp6
-rw-r--r--launcher/pathmatcher/SimplePrefixMatcher.h25
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp3
-rw-r--r--tests/FileSystem_test.cpp37
11 files changed, 341 insertions, 33 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 883f8968..ea8d2326 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -1,4 +1,7 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -38,10 +41,14 @@
#include "Application.h"
#include "BuildConfig.h"
+#include "DataMigrationTask.h"
#include "net/PasteUpload.h"
+#include "pathmatcher/MultiMatcher.h"
+#include "pathmatcher/SimplePrefixMatcher.h"
#include "ui/MainWindow.h"
#include "ui/InstanceWindow.h"
+#include "ui/dialogs/ProgressDialog.h"
#include "ui/instanceview/AccessibleInstanceView.h"
#include "ui/pages/BasePageProvider.h"
@@ -301,22 +308,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
dataPath = foo.absolutePath();
adjustedBy = "Persistent data path";
- QDir polymcData(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "PolyMC"));
- if (polymcData.exists()) {
- dataPath = polymcData.absolutePath();
- adjustedBy = "PolyMC data path";
- }
-
-#ifdef Q_OS_LINUX
- // TODO: this should be removed in a future version
- // TODO: provide a migration path similar to macOS migration
- QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc"));
- if (bar.exists()) {
- dataPath = bar.absolutePath();
- adjustedBy = "Legacy data path";
- }
-#endif
-
#ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
@@ -440,6 +431,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
{
+ bool migrated = false;
+
+ if (!migrated)
+ migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg");
+ if (!migrated)
+ migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg");
+ }
+
+ {
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
qDebug() << "Version : " << BuildConfig.printableVersionString();
@@ -1605,3 +1605,88 @@ int Application::suitableMaxMem()
return maxMemoryAlloc;
}
+
+bool Application::handleDataMigration(const QString& currentData,
+ const QString& oldData,
+ const QString& name,
+ const QString& configFile) const
+{
+ QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt");
+ QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) };
+
+ QLocale locale;
+
+ // Is there a valid config at the old location?
+ bool configExists = false;
+ for (QString configPath : configPaths) {
+ configExists |= QFileInfo::exists(configPath);
+ }
+
+ if (!configExists || QFileInfo::exists(nomigratePath)) {
+ qDebug() << "<> No migration needed from" << name;
+ return false;
+ }
+
+ QString message;
+ bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE));
+
+ if (currentExists) {
+ message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do "
+ "you want to be reminded of the pending data migration next time you start %2?")
+ .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
+ } else {
+ message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?")
+ .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
+
+ QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log"));
+ if (logInfo.exists()) {
+ QString lastModified = logInfo.lastModified().toString(locale.dateFormat());
+ message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?")
+ .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME);
+ }
+ }
+
+ QMessageBox::StandardButton askMoveDialogue =
+ QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ auto setDoNotMigrate = [&nomigratePath] {
+ QFile file(nomigratePath);
+ file.open(QIODevice::WriteOnly);
+ };
+
+ // create no-migrate file if user doesn't want to migrate
+ if (askMoveDialogue != QMessageBox::Yes) {
+ qDebug() << "<> Migration declined for" << name;
+ setDoNotMigrate();
+ return currentExists; // cancel further migrations, if we already have a data directory
+ }
+
+ if (!currentExists) {
+ // Migrate!
+ auto matcher = std::make_shared<MultiMatcher>();
+ matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
+ matcher->add(std::make_shared<SimplePrefixMatcher>(
+ BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
+ matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("icons/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("instances/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("libraries/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("mods/"));
+ matcher->add(std::make_shared<SimplePrefixMatcher>("themes/"));
+
+ ProgressDialog diag;
+ DataMigrationTask task(nullptr, oldData, currentData, matcher);
+ if (diag.execWithTask(&task)) {
+ qDebug() << "<> Migration succeeded";
+ setDoNotMigrate();
+ } else {
+ QString reason = task.failReason();
+ QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason));
+ }
+ } else {
+ qWarning() << "<> Migration was skipped, due to existing data";
+ }
+ return true;
+}
diff --git a/launcher/Application.h b/launcher/Application.h
index 4c2f62d4..7884227a 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -231,6 +231,7 @@ private slots:
void setupWizardFinished(int status);
private:
+ bool handleDataMigration(const QString & currentData, const QString & oldData, const QString & name, const QString & configFile) const;
bool createSetupWizard();
void performMainStartupAction();
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 8db93429..7a577935 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -97,6 +97,7 @@ set(PATHMATCHER_SOURCES
pathmatcher/IPathMatcher.h
pathmatcher/MultiMatcher.h
pathmatcher/RegexpMatcher.h
+ pathmatcher/SimplePrefixMatcher.h
)
set(NET_SOURCES
@@ -575,6 +576,8 @@ SET(LAUNCHER_SOURCES
# Application base
Application.h
Application.cpp
+ DataMigrationTask.h
+ DataMigrationTask.cpp
UpdateController.cpp
UpdateController.h
ApplicationMessage.h
diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp
new file mode 100644
index 00000000..27ce5f01
--- /dev/null
+++ b/launcher/DataMigrationTask.cpp
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "DataMigrationTask.h"
+
+#include "FileSystem.h"
+
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QMap>
+
+#include <QtConcurrent>
+
+DataMigrationTask::DataMigrationTask(QObject* parent,
+ const QString& sourcePath,
+ const QString& targetPath,
+ const IPathMatcher::Ptr pathMatcher)
+ : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
+{
+ m_copy.matcher(m_pathMatcher.get()).whitelist(true);
+}
+
+void DataMigrationTask::executeTask()
+{
+ setStatus(tr("Scanning files..."));
+
+ // 1. Scan
+ // Check how many files we gotta copy
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
+ return m_copy(true); // dry run to collect amount of files
+ });
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+}
+
+void DataMigrationTask::dryRunFinished()
+{
+ disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
+ disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
+#else
+ if (!m_copyFuture.result()) {
+#endif
+ emitFailed(tr("Failed to scan source path."));
+ return;
+ }
+
+ // 2. Copy
+ // Actually copy all files now.
+ m_toCopy = m_copy.totalCopied();
+ connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) {
+ QString shortenedName = relativeName;
+ // shorten the filename to hopefully fit into one line
+ if (shortenedName.length() > 50)
+ shortenedName = relativeName.left(20) + "…" + relativeName.right(29);
+ setProgress(m_copy.totalCopied(), m_toCopy);
+ setStatus(tr("Copying %1…").arg(shortenedName));
+ });
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
+ return m_copy(false); // actually copy now
+ });
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+}
+
+void DataMigrationTask::dryRunAborted()
+{
+ emitFailed(tr("Aborted"));
+}
+
+void DataMigrationTask::copyFinished()
+{
+ disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
+ disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
+#else
+ if (!m_copyFuture.result()) {
+#endif
+ emitFailed(tr("Some paths could not be copied!"));
+ return;
+ }
+
+ emitSucceeded();
+}
+
+void DataMigrationTask::copyAborted()
+{
+ emitFailed(tr("Aborted"));
+}
diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h
new file mode 100644
index 00000000..6cc23b1a
--- /dev/null
+++ b/launcher/DataMigrationTask.h
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "FileSystem.h"
+#include "pathmatcher/IPathMatcher.h"
+#include "tasks/Task.h"
+
+#include <QFuture>
+#include <QFutureWatcher>
+
+/*
+ * Migrate existing data from other MMC-like launchers.
+ */
+
+class DataMigrationTask : public Task {
+ Q_OBJECT
+ public:
+ explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher);
+ ~DataMigrationTask() override = default;
+
+ protected:
+ virtual void executeTask() override;
+
+ protected slots:
+ void dryRunFinished();
+ void dryRunAborted();
+ void copyFinished();
+ void copyAborted();
+
+ private:
+ const QString& m_sourcePath;
+ const QString& m_targetPath;
+ const IPathMatcher::Ptr m_pathMatcher;
+
+ FS::copy m_copy;
+ int m_toCopy = 0;
+ QFuture<bool> m_copyFuture;
+ QFutureWatcher<bool> m_copyFutureWatcher;
+};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 4a8f4bd3..0c6527b1 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -152,9 +152,10 @@ bool ensureFolderPathExists(QString foldernamepath)
/// @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 copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
+ m_copied = 0; // reset counter
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
@@ -174,13 +175,14 @@ bool copy::operator()(const QString& offset)
// Function that'll do the actual copying
auto copy_file = [&](QString src_path, QString relative_dst_path) {
- if (m_blacklist && m_blacklist->matches(relative_dst_path))
+ if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
return;
auto dst_path = PathCombine(dst, relative_dst_path);
- ensureFilePathExists(dst_path);
-
- fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
+ if (!dryRun) {
+ ensureFilePathExists(dst_path);
+ fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
+ }
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index b7e175fd..a9a81123 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -40,6 +40,7 @@
#include <QDir>
#include <QFlags>
+#include <QObject>
namespace FS {
@@ -76,9 +77,10 @@ bool ensureFilePathExists(QString filenamepath);
bool ensureFolderPathExists(QString filenamepath);
/// @brief Copies a directory and it's contents from src to dest
-class copy {
+class copy : public QObject {
+ Q_OBJECT
public:
- copy(const QString& src, const QString& dst)
+ copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
{
m_src.setPath(src);
m_dst.setPath(dst);
@@ -88,21 +90,35 @@ class copy {
m_followSymlinks = follow;
return *this;
}
- copy& blacklist(const IPathMatcher* filter)
+ copy& matcher(const IPathMatcher* filter)
{
- m_blacklist = filter;
+ m_matcher = filter;
return *this;
}
- bool operator()() { return operator()(QString()); }
+ copy& whitelist(bool whitelist)
+ {
+ m_whitelist = whitelist;
+ return *this;
+ }
+
+ bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
+
+ int totalCopied() { return m_copied; }
+
+ signals:
+ void fileCopied(const QString& relativeName);
+ // TODO: maybe add a "shouldCopy" signal in the future?
private:
- bool operator()(const QString& offset);
+ bool operator()(const QString& offset, bool dryRun = false);
private:
bool m_followSymlinks = true;
- const IPathMatcher* m_blacklist = nullptr;
+ const IPathMatcher* m_matcher = nullptr;
+ bool m_whitelist = false;
QDir m_src;
QDir m_dst;
+ int m_copied;
};
/**
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index a4ea947d..0a83ed9c 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -26,9 +26,11 @@ void InstanceCopyTask::executeTask()
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
- folderCopy.followSymlinks(false).blacklist(m_matcher.get());
+ folderCopy.followSymlinks(false).matcher(m_matcher.get());
- m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&folderCopy]{
+ return folderCopy();
+ });
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h
new file mode 100644
index 00000000..fc1f5ced
--- /dev/null
+++ b/launcher/pathmatcher/SimplePrefixMatcher.h
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include <QRegularExpression>
+#include "IPathMatcher.h"
+
+class SimplePrefixMatcher : public IPathMatcher {
+ public:
+ virtual ~SimplePrefixMatcher(){};
+ SimplePrefixMatcher(const QString& prefix)
+ {
+ m_prefix = prefix;
+ m_isPrefix = prefix.endsWith('/');
+ }
+
+ virtual bool matches(const QString& string) const override
+ {
+ if (m_isPrefix)
+ return string.startsWith(m_prefix);
+ return string == m_prefix;
+ }
+ QString m_prefix;
+ bool m_isPrefix = false;
+};
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index 05269f62..da73a449 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -44,7 +44,8 @@ void ProgressDialog::setSkipButton(bool present, QString label)
void ProgressDialog::on_skipButton_clicked(bool checked)
{
Q_UNUSED(checked);
- task->abort();
+ if (ui->skipButton->isEnabled()) // prevent other triggers from aborting
+ task->abort();
}
ProgressDialog::~ProgressDialog()
diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp
index 21270f6f..3a5c38d0 100644
--- a/tests/FileSystem_test.cpp
+++ b/tests/FileSystem_test.cpp
@@ -126,7 +126,7 @@ slots:
qDebug() << tempDir.path();
qDebug() << target_dir.path();
FS::copy c(folder, target_dir.path());
- c.blacklist(new RegexpMatcher("[.]?mcmeta"));
+ c.matcher(new RegexpMatcher("[.]?mcmeta"));
c();
for(auto entry: target_dir.entryList())
@@ -147,6 +147,41 @@ slots:
f();
}
+ void test_copy_with_whitelist()
+ {
+ QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder");
+ auto f = [&folder]()
+ {
+ QTemporaryDir tempDir;
+ tempDir.setAutoRemove(true);
+ qDebug() << "From:" << folder << "To:" << tempDir.path();
+
+ QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder"));
+ qDebug() << tempDir.path();
+ qDebug() << target_dir.path();
+ FS::copy c(folder, target_dir.path());
+ c.matcher(new RegexpMatcher("[.]?mcmeta"));
+ c.whitelist(true);
+ c();
+
+ for(auto entry: target_dir.entryList())
+ {
+ qDebug() << entry;
+ }
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(!target_dir.entryList().contains("assets"));
+ };
+
+ // first try variant without trailing /
+ QVERIFY(!folder.endsWith('/'));
+ f();
+
+ // then variant with trailing /
+ folder.append('/');
+ QVERIFY(folder.endsWith('/'));
+ f();
+ }
+
void test_copy_with_dot_hidden()
{
QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder");