Merge pull request #243 from Scrumplex/migration-dialog
Closes https://github.com/PrismLauncher/PrismLauncher/issues/46 Closes https://github.com/PrismLauncher/PrismLauncher/issues/204
This commit is contained in:
		@@ -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;
 | 
			
		||||
@@ -439,6 +430,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
 | 
			
		||||
        qDebug() << "<> Log initialized.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										96
									
								
								launcher/DataMigrationTask.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								launcher/DataMigrationTask.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -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"));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								launcher/DataMigrationTask.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								launcher/DataMigrationTask.h
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
};
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								launcher/pathmatcher/SimplePrefixMatcher.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								launcher/pathmatcher/SimplePrefixMatcher.h
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
};
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user