Rework curseforge download (#611)
* Use the bulk endpoint on mod resolution for faster download * Search on modrinth for api blocked mods * Display a dialog for manually downloading blocked mods
This commit is contained in:
		@@ -128,6 +128,8 @@ set(NET_SOURCES
 | 
				
			|||||||
    net/PasteUpload.h
 | 
					    net/PasteUpload.h
 | 
				
			||||||
    net/Sink.h
 | 
					    net/Sink.h
 | 
				
			||||||
    net/Validator.h
 | 
					    net/Validator.h
 | 
				
			||||||
 | 
					    net/Upload.cpp
 | 
				
			||||||
 | 
					    net/Upload.h
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Game launch logic
 | 
					# Game launch logic
 | 
				
			||||||
@@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES
 | 
				
			|||||||
    ui/dialogs/SkinUploadDialog.h
 | 
					    ui/dialogs/SkinUploadDialog.h
 | 
				
			||||||
    ui/dialogs/ModDownloadDialog.cpp
 | 
					    ui/dialogs/ModDownloadDialog.cpp
 | 
				
			||||||
    ui/dialogs/ModDownloadDialog.h
 | 
					    ui/dialogs/ModDownloadDialog.h
 | 
				
			||||||
 | 
					    ui/dialogs/ScrollMessageBox.cpp
 | 
				
			||||||
 | 
					    ui/dialogs/ScrollMessageBox.h
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # GUI - widgets
 | 
					    # GUI - widgets
 | 
				
			||||||
    ui/widgets/Common.cpp
 | 
					    ui/widgets/Common.cpp
 | 
				
			||||||
@@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI
 | 
				
			|||||||
    ui/dialogs/LoginDialog.ui
 | 
					    ui/dialogs/LoginDialog.ui
 | 
				
			||||||
    ui/dialogs/EditAccountDialog.ui
 | 
					    ui/dialogs/EditAccountDialog.ui
 | 
				
			||||||
    ui/dialogs/ReviewMessageBox.ui
 | 
					    ui/dialogs/ReviewMessageBox.ui
 | 
				
			||||||
 | 
					    ui/dialogs/ScrollMessageBox.ui
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
qt5_add_resources(LAUNCHER_RESOURCES
 | 
					qt5_add_resources(LAUNCHER_RESOURCES
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,9 +60,9 @@
 | 
				
			|||||||
#include "net/ChecksumValidator.h"
 | 
					#include "net/ChecksumValidator.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "ui/dialogs/CustomMessageBox.h"
 | 
					#include "ui/dialogs/CustomMessageBox.h"
 | 
				
			||||||
 | 
					#include "ui/dialogs/ScrollMessageBox.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <algorithm>
 | 
					#include <algorithm>
 | 
				
			||||||
#include <iterator>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
 | 
					InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -394,35 +394,58 @@ void InstanceImportTask::processFlame()
 | 
				
			|||||||
    connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
 | 
					    connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        auto results = m_modIdResolver->getResults();
 | 
					        auto results = m_modIdResolver->getResults();
 | 
				
			||||||
 | 
					        //first check for blocked mods
 | 
				
			||||||
 | 
					        QString text;
 | 
				
			||||||
 | 
					        auto anyBlocked = false;
 | 
				
			||||||
 | 
					        for(const auto& result: results.files.values()) {
 | 
				
			||||||
 | 
					            if (!result.resolved || result.url.isEmpty()) {
 | 
				
			||||||
 | 
					                text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
 | 
				
			||||||
 | 
					                anyBlocked = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(anyBlocked) {
 | 
				
			||||||
 | 
					            qWarning() << "Blocked mods found, displaying mod list";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            auto message_dialog = new ScrollMessageBox(m_parent,
 | 
				
			||||||
 | 
					                                                       tr("Blocked mods found"),
 | 
				
			||||||
 | 
					                                                       tr("The following mods were blocked on third party launchers.<br/>"
 | 
				
			||||||
 | 
					                                                          "You will need to manually download them and add them to the modpack"),
 | 
				
			||||||
 | 
					                                                       text);
 | 
				
			||||||
 | 
					            message_dialog->setModal(true);
 | 
				
			||||||
 | 
					            message_dialog->show();
 | 
				
			||||||
 | 
					            connect(message_dialog, &QDialog::rejected, [&]() {
 | 
				
			||||||
 | 
					                m_modIdResolver.reset();
 | 
				
			||||||
 | 
					                emitFailed("Canceled");
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            connect(message_dialog, &QDialog::accepted, [&]() {
 | 
				
			||||||
                m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
 | 
					                m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
 | 
				
			||||||
        for(const auto& result: results.files)
 | 
					                for (const auto &result: m_modIdResolver->getResults().files) {
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                    QString filename = result.fileName;
 | 
					                    QString filename = result.fileName;
 | 
				
			||||||
            if(!result.required)
 | 
					                    if (!result.required) {
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                        filename += ".disabled";
 | 
					                        filename += ".disabled";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
 | 
					                    auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
 | 
				
			||||||
                    auto path = FS::PathCombine(m_stagingPath, relpath);
 | 
					                    auto path = FS::PathCombine(m_stagingPath, relpath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            switch(result.type)
 | 
					                    switch (result.type) {
 | 
				
			||||||
            {
 | 
					                        case Flame::File::Type::Folder: {
 | 
				
			||||||
                case Flame::File::Type::Folder:
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                            logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
 | 
					                            logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
 | 
				
			||||||
                            // fall-through intentional, we treat these as plain old mods and dump them wherever.
 | 
					                            // fall-through intentional, we treat these as plain old mods and dump them wherever.
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        case Flame::File::Type::SingleFile:
 | 
					                        case Flame::File::Type::SingleFile:
 | 
				
			||||||
                case Flame::File::Type::Mod:
 | 
					                        case Flame::File::Type::Mod: {
 | 
				
			||||||
                {
 | 
					                            if (!result.url.isEmpty()) {
 | 
				
			||||||
                                qDebug() << "Will download" << result.url << "to" << path;
 | 
					                                qDebug() << "Will download" << result.url << "to" << path;
 | 
				
			||||||
                                auto dl = Net::Download::makeFile(result.url, path);
 | 
					                                auto dl = Net::Download::makeFile(result.url, path);
 | 
				
			||||||
                                m_filesNetJob->addNetAction(dl);
 | 
					                                m_filesNetJob->addNetAction(dl);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                            break;
 | 
					                            break;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        case Flame::File::Type::Modpack:
 | 
					                        case Flame::File::Type::Modpack:
 | 
				
			||||||
                    logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
 | 
					                            logWarning(
 | 
				
			||||||
 | 
					                                    tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
 | 
				
			||||||
 | 
					                                            relpath));
 | 
				
			||||||
                            break;
 | 
					                            break;
 | 
				
			||||||
                        case Flame::File::Type::Cmod2:
 | 
					                        case Flame::File::Type::Cmod2:
 | 
				
			||||||
                        case Flame::File::Type::Ctoc:
 | 
					                        case Flame::File::Type::Ctoc:
 | 
				
			||||||
@@ -432,23 +455,75 @@ void InstanceImportTask::processFlame()
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                m_modIdResolver.reset();
 | 
					                m_modIdResolver.reset();
 | 
				
			||||||
        connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
 | 
					                connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                            m_filesNetJob.reset();
 | 
					                            m_filesNetJob.reset();
 | 
				
			||||||
                            emitSucceeded();
 | 
					                            emitSucceeded();
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
        connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
 | 
					                connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                    m_filesNetJob.reset();
 | 
					                    m_filesNetJob.reset();
 | 
				
			||||||
                    emitFailed(reason);
 | 
					                    emitFailed(reason);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
        connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
 | 
					                connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                    setProgress(current, total);
 | 
					                    setProgress(current, total);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                setStatus(tr("Downloading mods..."));
 | 
					                setStatus(tr("Downloading mods..."));
 | 
				
			||||||
                m_filesNetJob->start();
 | 
					                m_filesNetJob->start();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }else{
 | 
				
			||||||
 | 
					            //TODO extract to function ?
 | 
				
			||||||
 | 
					            m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
 | 
				
			||||||
 | 
					            for (const auto &result: m_modIdResolver->getResults().files) {
 | 
				
			||||||
 | 
					                QString filename = result.fileName;
 | 
				
			||||||
 | 
					                if (!result.required) {
 | 
				
			||||||
 | 
					                    filename += ".disabled";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
 | 
				
			||||||
 | 
					                auto path = FS::PathCombine(m_stagingPath, relpath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                switch (result.type) {
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Folder: {
 | 
				
			||||||
 | 
					                        logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
 | 
				
			||||||
 | 
					                        // fall-through intentional, we treat these as plain old mods and dump them wherever.
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    case Flame::File::Type::SingleFile:
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Mod: {
 | 
				
			||||||
 | 
					                        if (!result.url.isEmpty()) {
 | 
				
			||||||
 | 
					                            qDebug() << "Will download" << result.url << "to" << path;
 | 
				
			||||||
 | 
					                            auto dl = Net::Download::makeFile(result.url, path);
 | 
				
			||||||
 | 
					                            m_filesNetJob->addNetAction(dl);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Modpack:
 | 
				
			||||||
 | 
					                        logWarning(
 | 
				
			||||||
 | 
					                                tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
 | 
				
			||||||
 | 
					                                        relpath));
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Cmod2:
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Ctoc:
 | 
				
			||||||
 | 
					                    case Flame::File::Type::Unknown:
 | 
				
			||||||
 | 
					                        logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            m_modIdResolver.reset();
 | 
				
			||||||
 | 
					            connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
 | 
				
			||||||
 | 
					                        m_filesNetJob.reset();
 | 
				
			||||||
 | 
					                        emitSucceeded();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
 | 
				
			||||||
 | 
					                m_filesNetJob.reset();
 | 
				
			||||||
 | 
					                emitFailed(reason);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
 | 
				
			||||||
 | 
					                setProgress(current, total);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            setStatus(tr("Downloading mods..."));
 | 
				
			||||||
 | 
					            m_filesNetJob->start();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
 | 
					    connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
 | 
				
			||||||
@@ -524,11 +599,11 @@ void InstanceImportTask::processModrinth()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
 | 
					            auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
 | 
				
			||||||
            bool had_optional = false;
 | 
					            bool had_optional = false;
 | 
				
			||||||
            for (auto& obj : jsonFiles) {
 | 
					            for (auto& modInfo : jsonFiles) {
 | 
				
			||||||
                Modrinth::File file;
 | 
					                Modrinth::File file;
 | 
				
			||||||
                file.path = Json::requireString(obj, "path");
 | 
					                file.path = Json::requireString(modInfo, "path");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                auto env = Json::ensureObject(obj, "env");
 | 
					                auto env = Json::ensureObject(modInfo, "env");
 | 
				
			||||||
                QString support = Json::ensureString(env, "client", "unsupported");
 | 
					                QString support = Json::ensureString(env, "client", "unsupported");
 | 
				
			||||||
                if (support == "unsupported") {
 | 
					                if (support == "unsupported") {
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
@@ -546,7 +621,7 @@ void InstanceImportTask::processModrinth()
 | 
				
			|||||||
                        file.path += ".disabled";
 | 
					                        file.path += ".disabled";
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                QJsonObject hashes = Json::requireObject(obj, "hashes");
 | 
					                QJsonObject hashes = Json::requireObject(modInfo, "hashes");
 | 
				
			||||||
                QString hash;
 | 
					                QString hash;
 | 
				
			||||||
                QCryptographicHash::Algorithm hashAlgorithm;
 | 
					                QCryptographicHash::Algorithm hashAlgorithm;
 | 
				
			||||||
                hash = Json::ensureString(hashes, "sha1");
 | 
					                hash = Json::ensureString(hashes, "sha1");
 | 
				
			||||||
@@ -566,7 +641,7 @@ void InstanceImportTask::processModrinth()
 | 
				
			|||||||
                file.hashAlgorithm = hashAlgorithm;
 | 
					                file.hashAlgorithm = hashAlgorithm;
 | 
				
			||||||
                // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
 | 
					                // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
 | 
				
			||||||
                // (as Modrinth seems to incorrectly handle spaces)
 | 
					                // (as Modrinth seems to incorrectly handle spaces)
 | 
				
			||||||
                file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path);
 | 
					                file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path);
 | 
				
			||||||
                if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
 | 
					                if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
 | 
				
			||||||
                    throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
 | 
					                    throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,6 +42,7 @@
 | 
				
			|||||||
#include <QFutureWatcher>
 | 
					#include <QFutureWatcher>
 | 
				
			||||||
#include "settings/SettingsObject.h"
 | 
					#include "settings/SettingsObject.h"
 | 
				
			||||||
#include "QObjectPtr.h"
 | 
					#include "QObjectPtr.h"
 | 
				
			||||||
 | 
					#include "modplatform/flame/PackManifest.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <nonstd/optional>
 | 
					#include <nonstd/optional>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,6 +60,10 @@ public:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    bool canAbort() const override { return true; }
 | 
					    bool canAbort() const override { return true; }
 | 
				
			||||||
    bool abort() override;
 | 
					    bool abort() override;
 | 
				
			||||||
 | 
					    const QVector<Flame::File> &getBlockedFiles() const
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return m_blockedMods;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
protected:
 | 
					protected:
 | 
				
			||||||
    //! Entry point for tasks.
 | 
					    //! Entry point for tasks.
 | 
				
			||||||
@@ -87,6 +92,7 @@ private: /* data */
 | 
				
			|||||||
    std::unique_ptr<QuaZip> m_packZip;
 | 
					    std::unique_ptr<QuaZip> m_packZip;
 | 
				
			||||||
    QFuture<nonstd::optional<QStringList>> m_extractFuture;
 | 
					    QFuture<nonstd::optional<QStringList>> m_extractFuture;
 | 
				
			||||||
    QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
 | 
					    QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
 | 
				
			||||||
 | 
					    QVector<Flame::File> m_blockedMods;
 | 
				
			||||||
    enum class ModpackType{
 | 
					    enum class ModpackType{
 | 
				
			||||||
        Unknown,
 | 
					        Unknown,
 | 
				
			||||||
        MultiMC,
 | 
					        MultiMC,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
#include "FileResolvingTask.h"
 | 
					#include "FileResolvingTask.h"
 | 
				
			||||||
#include "Json.h"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
 | 
					#include "Json.h"
 | 
				
			||||||
 | 
					#include "net/Upload.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
 | 
				
			||||||
    : m_network(network), m_toProcess(toProcess)
 | 
					    : m_network(network), m_toProcess(toProcess)
 | 
				
			||||||
{}
 | 
					{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
 | 
				
			|||||||
    setStatus(tr("Resolving mod IDs..."));
 | 
					    setStatus(tr("Resolving mod IDs..."));
 | 
				
			||||||
    setProgress(0, m_toProcess.files.size());
 | 
					    setProgress(0, m_toProcess.files.size());
 | 
				
			||||||
    m_dljob = new NetJob("Mod id resolver", m_network);
 | 
					    m_dljob = new NetJob("Mod id resolver", m_network);
 | 
				
			||||||
    results.resize(m_toProcess.files.size());
 | 
					    result.reset(new QByteArray());
 | 
				
			||||||
    int index = 0;
 | 
					    //build json data to send
 | 
				
			||||||
    for (auto& file : m_toProcess.files) {
 | 
					    QJsonObject object;
 | 
				
			||||||
        auto projectIdStr = QString::number(file.projectId);
 | 
					
 | 
				
			||||||
        auto fileIdStr = QString::number(file.fileId);
 | 
					    object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
 | 
				
			||||||
        QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
 | 
					        l.push_back(s.fileId);
 | 
				
			||||||
        auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
 | 
					        return l;
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					    QByteArray data = Json::toText(object);
 | 
				
			||||||
 | 
					    auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
 | 
				
			||||||
    m_dljob->addNetAction(dl);
 | 
					    m_dljob->addNetAction(dl);
 | 
				
			||||||
        index++;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
 | 
					    connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
 | 
				
			||||||
    m_dljob->start();
 | 
					    m_dljob->start();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Flame::FileResolvingTask::netJobFinished()
 | 
					void Flame::FileResolvingTask::netJobFinished()
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    bool failed = false;
 | 
					 | 
				
			||||||
    int index = 0;
 | 
					    int index = 0;
 | 
				
			||||||
    for (auto& bytes : results) {
 | 
					    // job to check modrinth for blocked projects
 | 
				
			||||||
        auto& out = m_toProcess.files[index];
 | 
					    auto job = new NetJob("Modrinth check", m_network);
 | 
				
			||||||
 | 
					    blockedProjects = QMap<File *,QByteArray *>();
 | 
				
			||||||
 | 
					    auto doc = Json::requireDocument(*result);
 | 
				
			||||||
 | 
					    auto array = Json::requireArray(doc.object()["data"]);
 | 
				
			||||||
 | 
					    for (QJsonValueRef file : array) {
 | 
				
			||||||
 | 
					        auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
 | 
				
			||||||
 | 
					        auto& out = m_toProcess.files[fileid];
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            failed &= (!out.parseFromBytes(bytes));
 | 
					           out.parseFromObject(Json::requireObject(file));
 | 
				
			||||||
        } catch (const JSONValidationError& e) {
 | 
					        } catch (const JSONValidationError& e) {
 | 
				
			||||||
            qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
 | 
					            qDebug() << "Blocked mod on curseforge" << out.fileName;
 | 
				
			||||||
            qCritical() << e.cause();
 | 
					            auto hash = out.hash;
 | 
				
			||||||
            qCritical() << "JSON:";
 | 
					            if(!hash.isEmpty()) {
 | 
				
			||||||
            qCritical() << bytes;
 | 
					                auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
 | 
				
			||||||
            failed = true;
 | 
					                auto output = new QByteArray();
 | 
				
			||||||
 | 
					                auto dl = Net::Download::makeByteArray(QUrl(url), output);
 | 
				
			||||||
 | 
					                QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
 | 
				
			||||||
 | 
					                    out.resolved = true;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                job->addNetAction(dl);
 | 
				
			||||||
 | 
					                blockedProjects.insert(&out, output);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        index++;
 | 
					        index++;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!failed) {
 | 
					    connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    job->start();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Flame::FileResolvingTask::modrinthCheckFinished() {
 | 
				
			||||||
 | 
					    qDebug() << "Finished with blocked mods : " << blockedProjects.size();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
 | 
				
			||||||
 | 
					        auto &out = *it;
 | 
				
			||||||
 | 
					        auto bytes = blockedProjects[out];
 | 
				
			||||||
 | 
					        if (!out->resolved) {
 | 
				
			||||||
 | 
					            delete bytes;
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        QJsonDocument doc = QJsonDocument::fromJson(*bytes);
 | 
				
			||||||
 | 
					        auto obj = doc.object();
 | 
				
			||||||
 | 
					        auto array = Json::requireArray(obj,"files");
 | 
				
			||||||
 | 
					        for (auto file: array) {
 | 
				
			||||||
 | 
					            auto fileObj = Json::requireObject(file);
 | 
				
			||||||
 | 
					            auto primary = Json::requireBoolean(fileObj,"primary");
 | 
				
			||||||
 | 
					            if (primary) {
 | 
				
			||||||
 | 
					                out->url = Json::requireUrl(fileObj,"url");
 | 
				
			||||||
 | 
					                qDebug() << "Found alternative on modrinth " << out->fileName;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        delete bytes;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    //copy to an output list and filter out projects found on modrinth
 | 
				
			||||||
 | 
					    auto block = new QList<File *>();
 | 
				
			||||||
 | 
					    auto it = blockedProjects.keys();
 | 
				
			||||||
 | 
					    std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
 | 
				
			||||||
 | 
					        return !f->resolved;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    //Display not found mods early
 | 
				
			||||||
 | 
					    if (!block->empty()) {
 | 
				
			||||||
 | 
					        //blocked mods found, we need the slug for displaying.... we need another job :D !
 | 
				
			||||||
 | 
					        auto slugJob = new NetJob("Slug Job", m_network);
 | 
				
			||||||
 | 
					        auto slugs = QVector<QByteArray>(block->size());
 | 
				
			||||||
 | 
					        auto index = 0;
 | 
				
			||||||
 | 
					        for (auto fileInfo: *block) {
 | 
				
			||||||
 | 
					            auto projectId = fileInfo->projectId;
 | 
				
			||||||
 | 
					            slugs[index] = QByteArray();
 | 
				
			||||||
 | 
					            auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
 | 
				
			||||||
 | 
					            auto dl = Net::Download::makeByteArray(url, &slugs[index]);
 | 
				
			||||||
 | 
					            slugJob->addNetAction(dl);
 | 
				
			||||||
 | 
					            index++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
 | 
				
			||||||
 | 
					            slugJob->deleteLater();
 | 
				
			||||||
 | 
					            auto index = 0;
 | 
				
			||||||
 | 
					            for (const auto &slugResult: slugs) {
 | 
				
			||||||
 | 
					                auto json = QJsonDocument::fromJson(slugResult);
 | 
				
			||||||
 | 
					                auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
 | 
				
			||||||
 | 
					                        "websiteUrl");
 | 
				
			||||||
 | 
					                auto mod = block->at(index);
 | 
				
			||||||
 | 
					                auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
 | 
				
			||||||
 | 
					                mod->websiteUrl = link;
 | 
				
			||||||
 | 
					                index++;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            emitSucceeded();
 | 
					            emitSucceeded();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        slugJob->start();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        emitFailed(tr("Some mod ID resolving tasks failed."));
 | 
					        emitSucceeded();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ class FileResolvingTask : public Task
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    Q_OBJECT
 | 
					    Q_OBJECT
 | 
				
			||||||
public:
 | 
					public:
 | 
				
			||||||
    explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
 | 
					    explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
 | 
				
			||||||
    virtual ~FileResolvingTask() {};
 | 
					    virtual ~FileResolvingTask() {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const Flame::Manifest &getResults() const
 | 
					    const Flame::Manifest &getResults() const
 | 
				
			||||||
@@ -27,7 +27,11 @@ protected slots:
 | 
				
			|||||||
private: /* data */
 | 
					private: /* data */
 | 
				
			||||||
    shared_qobject_ptr<QNetworkAccessManager> m_network;
 | 
					    shared_qobject_ptr<QNetworkAccessManager> m_network;
 | 
				
			||||||
    Flame::Manifest m_toProcess;
 | 
					    Flame::Manifest m_toProcess;
 | 
				
			||||||
    QVector<QByteArray> results;
 | 
					    std::shared_ptr<QByteArray> result;
 | 
				
			||||||
    NetJob::Ptr m_dljob;
 | 
					    NetJob::Ptr m_dljob;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void modrinthCheckFinished();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    QMap<File *, QByteArray *> blockedProjects;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
 | 
				
			|||||||
        auto obj = Json::requireObject(item);
 | 
					        auto obj = Json::requireObject(item);
 | 
				
			||||||
        Flame::File file;
 | 
					        Flame::File file;
 | 
				
			||||||
        loadFileV1(file, obj);
 | 
					        loadFileV1(file, obj);
 | 
				
			||||||
        m.files.append(file);
 | 
					        m.files.insert(file.fileId,file);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    m.overrides = Json::ensureString(manifest, "overrides", "overrides");
 | 
					    m.overrides = Json::ensureString(manifest, "overrides", "overrides");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
 | 
				
			|||||||
    loadManifestV1(m, obj);
 | 
					    loadManifestV1(m, obj);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool Flame::File::parseFromBytes(const QByteArray& bytes)
 | 
					bool Flame::File::parseFromObject(const QJsonObject& obj)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    auto doc = Json::requireDocument(bytes);
 | 
					 | 
				
			||||||
    if (!doc.isObject()) {
 | 
					 | 
				
			||||||
        throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    auto obj = Json::ensureObject(doc.object(), "data");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fileName = Json::requireString(obj, "fileName");
 | 
					    fileName = Json::requireString(obj, "fileName");
 | 
				
			||||||
 | 
					 | 
				
			||||||
    QString rawUrl = Json::requireString(obj, "downloadUrl");
 | 
					 | 
				
			||||||
    url = QUrl(rawUrl, QUrl::TolerantMode);
 | 
					 | 
				
			||||||
    if (!url.isValid()) {
 | 
					 | 
				
			||||||
        throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
 | 
					    // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
 | 
				
			||||||
    // It is also optional
 | 
					    // It is also optional
 | 
				
			||||||
    type = File::Type::SingleFile;
 | 
					    type = File::Type::SingleFile;
 | 
				
			||||||
@@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes)
 | 
				
			|||||||
        // this is probably a mod, dunno what else could modpacks download
 | 
					        // this is probably a mod, dunno what else could modpacks download
 | 
				
			||||||
        targetFolder = "mods";
 | 
					        targetFolder = "mods";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // get the hash
 | 
				
			||||||
 | 
					    hash = QString();
 | 
				
			||||||
 | 
					    auto hashes = Json::ensureArray(obj, "hashes");
 | 
				
			||||||
 | 
					    for(QJsonValueRef item : hashes) {
 | 
				
			||||||
 | 
					        auto hobj = Json::requireObject(item);
 | 
				
			||||||
 | 
					        auto algo = Json::requireInteger(hobj, "algo");
 | 
				
			||||||
 | 
					        auto value = Json::requireString(hobj, "value");
 | 
				
			||||||
 | 
					        if (algo == 1) {
 | 
				
			||||||
 | 
					            hash = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // may throw, if the project is blocked
 | 
				
			||||||
 | 
					    QString rawUrl = Json::ensureString(obj, "downloadUrl");
 | 
				
			||||||
 | 
					    url = QUrl(rawUrl, QUrl::TolerantMode);
 | 
				
			||||||
 | 
					    if (!url.isValid()) {
 | 
				
			||||||
 | 
					        throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    resolved = true;
 | 
					    resolved = true;
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,19 +2,24 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include <QString>
 | 
					#include <QString>
 | 
				
			||||||
#include <QVector>
 | 
					#include <QVector>
 | 
				
			||||||
 | 
					#include <QMap>
 | 
				
			||||||
#include <QUrl>
 | 
					#include <QUrl>
 | 
				
			||||||
 | 
					#include <QJsonObject>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Flame
 | 
					namespace Flame
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
struct File
 | 
					struct File
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    // NOTE: throws JSONValidationError
 | 
					    // NOTE: throws JSONValidationError
 | 
				
			||||||
    bool parseFromBytes(const QByteArray &bytes);
 | 
					    bool parseFromObject(const QJsonObject& object);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    int projectId = 0;
 | 
					    int projectId = 0;
 | 
				
			||||||
    int fileId = 0;
 | 
					    int fileId = 0;
 | 
				
			||||||
    // NOTE: the opposite to 'optional'. This is at the time of writing unused.
 | 
					    // NOTE: the opposite to 'optional'. This is at the time of writing unused.
 | 
				
			||||||
    bool required = true;
 | 
					    bool required = true;
 | 
				
			||||||
 | 
					    QString hash;
 | 
				
			||||||
 | 
					    // NOTE: only set on blocked files ! Empty otherwise.
 | 
				
			||||||
 | 
					    QString websiteUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // our
 | 
					    // our
 | 
				
			||||||
    bool resolved = false;
 | 
					    bool resolved = false;
 | 
				
			||||||
@@ -54,7 +59,8 @@ struct Manifest
 | 
				
			|||||||
    QString name;
 | 
					    QString name;
 | 
				
			||||||
    QString version;
 | 
					    QString version;
 | 
				
			||||||
    QString author;
 | 
					    QString author;
 | 
				
			||||||
    QVector<Flame::File> files;
 | 
					    //File id -> File
 | 
				
			||||||
 | 
					    QMap<int,Flame::File> files;
 | 
				
			||||||
    QString overrides;
 | 
					    QString overrides;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										199
									
								
								launcher/net/Upload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								launcher/net/Upload.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by timoreo on 20/05/22.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "Upload.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <utility>
 | 
				
			||||||
 | 
					#include "ByteArraySink.h"
 | 
				
			||||||
 | 
					#include "BuildConfig.h"
 | 
				
			||||||
 | 
					#include "Application.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Net {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
 | 
				
			||||||
 | 
					        setProgress(bytesReceived, bytesTotal);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::downloadError(QNetworkReply::NetworkError error) {
 | 
				
			||||||
 | 
					        if (error == QNetworkReply::OperationCanceledError) {
 | 
				
			||||||
 | 
					            qCritical() << "Aborted " << m_url.toString();
 | 
				
			||||||
 | 
					            m_state = State::AbortedByUser;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // error happened during download.
 | 
				
			||||||
 | 
					            qCritical() << "Failed " << m_url.toString() << " with reason " << error;
 | 
				
			||||||
 | 
					            m_state = State::Failed;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::sslErrors(const QList<QSslError> &errors) {
 | 
				
			||||||
 | 
					        int i = 1;
 | 
				
			||||||
 | 
					        for (const auto& error : errors) {
 | 
				
			||||||
 | 
					            qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
 | 
				
			||||||
 | 
					            auto cert = error.certificate();
 | 
				
			||||||
 | 
					            qCritical() << "Certificate in question:\n" << cert.toText();
 | 
				
			||||||
 | 
					            i++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool Upload::handleRedirect()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
 | 
				
			||||||
 | 
					        if (!redirect.isValid()) {
 | 
				
			||||||
 | 
					            if (!m_reply->hasRawHeader("Location")) {
 | 
				
			||||||
 | 
					                // no redirect -> it's fine to continue
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // there is a Location header, but it's not correct. we need to apply some workarounds...
 | 
				
			||||||
 | 
					            QByteArray redirectBA = m_reply->rawHeader("Location");
 | 
				
			||||||
 | 
					            if (redirectBA.size() == 0) {
 | 
				
			||||||
 | 
					                // empty, yet present redirect header? WTF?
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            QString redirectStr = QString::fromUtf8(redirectBA);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (redirectStr.startsWith("//")) {
 | 
				
			||||||
 | 
					                /*
 | 
				
			||||||
 | 
					                 * IF the URL begins with //, we need to insert the URL scheme.
 | 
				
			||||||
 | 
					                 * See: https://bugreports.qt.io/browse/QTBUG-41061
 | 
				
			||||||
 | 
					                 * See: http://tools.ietf.org/html/rfc3986#section-4.2
 | 
				
			||||||
 | 
					                 */
 | 
				
			||||||
 | 
					                redirectStr = m_reply->url().scheme() + ":" + redirectStr;
 | 
				
			||||||
 | 
					            } else if (redirectStr.startsWith("/")) {
 | 
				
			||||||
 | 
					                /*
 | 
				
			||||||
 | 
					                 * IF the URL begins with /, we need to process it as a relative URL
 | 
				
			||||||
 | 
					                 */
 | 
				
			||||||
 | 
					                auto url = m_reply->url();
 | 
				
			||||||
 | 
					                url.setPath(redirectStr, QUrl::TolerantMode);
 | 
				
			||||||
 | 
					                redirectStr = url.toString();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /*
 | 
				
			||||||
 | 
					             * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
 | 
				
			||||||
 | 
					             * FIXME: report Qt bug for this
 | 
				
			||||||
 | 
					             */
 | 
				
			||||||
 | 
					            redirect = QUrl(redirectStr, QUrl::TolerantMode);
 | 
				
			||||||
 | 
					            if (!redirect.isValid()) {
 | 
				
			||||||
 | 
					                qWarning() << "Failed to parse redirect URL:" << redirectStr;
 | 
				
			||||||
 | 
					                downloadError(QNetworkReply::ProtocolFailure);
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            qDebug() << "Fixed location header:" << redirect;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            qDebug() << "Location header:" << redirect;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        m_url = QUrl(redirect.toString());
 | 
				
			||||||
 | 
					        qDebug() << "Following redirect to " << m_url.toString();
 | 
				
			||||||
 | 
					        startAction(m_network);
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::downloadFinished() {
 | 
				
			||||||
 | 
					        // handle HTTP redirection first
 | 
				
			||||||
 | 
					        // very unlikely for post requests, still can happen
 | 
				
			||||||
 | 
					        if (handleRedirect()) {
 | 
				
			||||||
 | 
					            qDebug() << "Upload redirected:" << m_url.toString();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // if the download failed before this point ...
 | 
				
			||||||
 | 
					        if (m_state == State::Succeeded) {
 | 
				
			||||||
 | 
					            qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString();
 | 
				
			||||||
 | 
					            m_sink->abort();
 | 
				
			||||||
 | 
					            m_reply.reset();
 | 
				
			||||||
 | 
					            emit succeeded();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        } else if (m_state == State::Failed) {
 | 
				
			||||||
 | 
					            qDebug() << "Upload failed in previous step:" << m_url.toString();
 | 
				
			||||||
 | 
					            m_sink->abort();
 | 
				
			||||||
 | 
					            m_reply.reset();
 | 
				
			||||||
 | 
					            emit failed("");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        } else if (m_state == State::AbortedByUser) {
 | 
				
			||||||
 | 
					            qDebug() << "Upload aborted in previous step:" << m_url.toString();
 | 
				
			||||||
 | 
					            m_sink->abort();
 | 
				
			||||||
 | 
					            m_reply.reset();
 | 
				
			||||||
 | 
					            emit aborted();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // make sure we got all the remaining data, if any
 | 
				
			||||||
 | 
					        auto data = m_reply->readAll();
 | 
				
			||||||
 | 
					        if (data.size()) {
 | 
				
			||||||
 | 
					            qDebug() << "Writing extra" << data.size() << "bytes";
 | 
				
			||||||
 | 
					            m_state = m_sink->write(data);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // otherwise, finalize the whole graph
 | 
				
			||||||
 | 
					        m_state = m_sink->finalize(*m_reply.get());
 | 
				
			||||||
 | 
					        if (m_state != State::Succeeded) {
 | 
				
			||||||
 | 
					            qDebug() << "Upload failed to finalize:" << m_url.toString();
 | 
				
			||||||
 | 
					            m_sink->abort();
 | 
				
			||||||
 | 
					            m_reply.reset();
 | 
				
			||||||
 | 
					            emit failed("");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        m_reply.reset();
 | 
				
			||||||
 | 
					        qDebug() << "Upload succeeded:" << m_url.toString();
 | 
				
			||||||
 | 
					        emit succeeded();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::downloadReadyRead() {
 | 
				
			||||||
 | 
					        if (m_state == State::Running) {
 | 
				
			||||||
 | 
					            auto data = m_reply->readAll();
 | 
				
			||||||
 | 
					            m_state = m_sink->write(data);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void Upload::executeTask() {
 | 
				
			||||||
 | 
					        setStatus(tr("Uploading %1").arg(m_url.toString()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (m_state == State::AbortedByUser) {
 | 
				
			||||||
 | 
					            qWarning() << "Attempt to start an aborted Upload:" << m_url.toString();
 | 
				
			||||||
 | 
					            emit aborted();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        QNetworkRequest request(m_url);
 | 
				
			||||||
 | 
					        m_state = m_sink->init(request);
 | 
				
			||||||
 | 
					        switch (m_state) {
 | 
				
			||||||
 | 
					            case State::Succeeded:
 | 
				
			||||||
 | 
					                emitSucceeded();
 | 
				
			||||||
 | 
					                qDebug() << "Upload cache hit " << m_url.toString();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            case State::Running:
 | 
				
			||||||
 | 
					                qDebug() << "Uploading " << m_url.toString();
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case State::Inactive:
 | 
				
			||||||
 | 
					            case State::Failed:
 | 
				
			||||||
 | 
					                emitFailed("");
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            case State::AbortedByUser:
 | 
				
			||||||
 | 
					                emitAborted();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
 | 
				
			||||||
 | 
					        if (request.url().host().contains("api.curseforge.com")) {
 | 
				
			||||||
 | 
					            request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        //TODO other types of post requests ?
 | 
				
			||||||
 | 
					        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
 | 
				
			||||||
 | 
					        QNetworkReply* rep = m_network->post(request, m_post_data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        m_reply.reset(rep);
 | 
				
			||||||
 | 
					        connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
 | 
				
			||||||
 | 
					        connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
 | 
				
			||||||
 | 
					        connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
 | 
				
			||||||
 | 
					        connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors);
 | 
				
			||||||
 | 
					        connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
 | 
				
			||||||
 | 
					        auto* up = new Upload();
 | 
				
			||||||
 | 
					        up->m_url = std::move(url);
 | 
				
			||||||
 | 
					        up->m_sink.reset(new ByteArraySink(output));
 | 
				
			||||||
 | 
					        up->m_post_data = std::move(m_post_data);
 | 
				
			||||||
 | 
					        return up;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					} // Net
 | 
				
			||||||
							
								
								
									
										31
									
								
								launcher/net/Upload.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								launcher/net/Upload.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "NetAction.h"
 | 
				
			||||||
 | 
					#include "Sink.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Net {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Upload : public NetAction {
 | 
				
			||||||
 | 
					        Q_OBJECT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public:
 | 
				
			||||||
 | 
					        static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected slots:
 | 
				
			||||||
 | 
					        void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
 | 
				
			||||||
 | 
					        void downloadError(QNetworkReply::NetworkError error) override;
 | 
				
			||||||
 | 
					        void sslErrors(const QList<QSslError> & errors);
 | 
				
			||||||
 | 
					        void downloadFinished() override;
 | 
				
			||||||
 | 
					        void downloadReadyRead() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public slots:
 | 
				
			||||||
 | 
					        void executeTask() override;
 | 
				
			||||||
 | 
					    private:
 | 
				
			||||||
 | 
					        std::unique_ptr<Sink> m_sink;
 | 
				
			||||||
 | 
					        QByteArray m_post_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bool handleRedirect();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // Net
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								launcher/ui/dialogs/ScrollMessageBox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								launcher/ui/dialogs/ScrollMessageBox.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					#include "ScrollMessageBox.h"
 | 
				
			||||||
 | 
					#include "ui_ScrollMessageBox.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ScrollMessageBox::ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body) :
 | 
				
			||||||
 | 
					        QDialog(parent), ui(new Ui::ScrollMessageBox) {
 | 
				
			||||||
 | 
					    ui->setupUi(this);
 | 
				
			||||||
 | 
					    this->setWindowTitle(title);
 | 
				
			||||||
 | 
					    ui->label->setText(text);
 | 
				
			||||||
 | 
					    ui->textBrowser->setText(body);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ScrollMessageBox::~ScrollMessageBox() {
 | 
				
			||||||
 | 
					    delete ui;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								launcher/ui/dialogs/ScrollMessageBox.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								launcher/ui/dialogs/ScrollMessageBox.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <QDialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					QT_BEGIN_NAMESPACE
 | 
				
			||||||
 | 
					namespace Ui { class ScrollMessageBox; }
 | 
				
			||||||
 | 
					QT_END_NAMESPACE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScrollMessageBox : public QDialog {
 | 
				
			||||||
 | 
					Q_OBJECT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
					    ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ~ScrollMessageBox() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
					    Ui::ScrollMessageBox *ui;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										84
									
								
								launcher/ui/dialogs/ScrollMessageBox.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								launcher/ui/dialogs/ScrollMessageBox.ui
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<ui version="4.0">
 | 
				
			||||||
 | 
					 <class>ScrollMessageBox</class>
 | 
				
			||||||
 | 
					 <widget class="QDialog" name="ScrollMessageBox">
 | 
				
			||||||
 | 
					  <property name="geometry">
 | 
				
			||||||
 | 
					   <rect>
 | 
				
			||||||
 | 
					    <x>0</x>
 | 
				
			||||||
 | 
					    <y>0</y>
 | 
				
			||||||
 | 
					    <width>400</width>
 | 
				
			||||||
 | 
					    <height>455</height>
 | 
				
			||||||
 | 
					   </rect>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <property name="windowTitle">
 | 
				
			||||||
 | 
					   <string>ScrollMessageBox</string>
 | 
				
			||||||
 | 
					  </property>
 | 
				
			||||||
 | 
					  <layout class="QGridLayout" name="gridLayout">
 | 
				
			||||||
 | 
					   <item row="0" column="0">
 | 
				
			||||||
 | 
					    <widget class="QLabel" name="label">
 | 
				
			||||||
 | 
					     <property name="text">
 | 
				
			||||||
 | 
					      <string notr="true"/>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="textFormat">
 | 
				
			||||||
 | 
					      <enum>Qt::RichText</enum>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item row="2" column="0">
 | 
				
			||||||
 | 
					    <widget class="QDialogButtonBox" name="buttonBox">
 | 
				
			||||||
 | 
					     <property name="orientation">
 | 
				
			||||||
 | 
					      <enum>Qt::Horizontal</enum>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="standardButtons">
 | 
				
			||||||
 | 
					      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					   <item row="1" column="0">
 | 
				
			||||||
 | 
					    <widget class="QTextBrowser" name="textBrowser">
 | 
				
			||||||
 | 
					     <property name="acceptRichText">
 | 
				
			||||||
 | 
					      <bool>true</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					     <property name="openExternalLinks">
 | 
				
			||||||
 | 
					      <bool>true</bool>
 | 
				
			||||||
 | 
					     </property>
 | 
				
			||||||
 | 
					    </widget>
 | 
				
			||||||
 | 
					   </item>
 | 
				
			||||||
 | 
					  </layout>
 | 
				
			||||||
 | 
					 </widget>
 | 
				
			||||||
 | 
					 <resources/>
 | 
				
			||||||
 | 
					 <connections>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>accepted()</signal>
 | 
				
			||||||
 | 
					   <receiver>ScrollMessageBox</receiver>
 | 
				
			||||||
 | 
					   <slot>accept()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>199</x>
 | 
				
			||||||
 | 
					     <y>425</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>199</x>
 | 
				
			||||||
 | 
					     <y>227</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					  <connection>
 | 
				
			||||||
 | 
					   <sender>buttonBox</sender>
 | 
				
			||||||
 | 
					   <signal>rejected()</signal>
 | 
				
			||||||
 | 
					   <receiver>ScrollMessageBox</receiver>
 | 
				
			||||||
 | 
					   <slot>reject()</slot>
 | 
				
			||||||
 | 
					   <hints>
 | 
				
			||||||
 | 
					    <hint type="sourcelabel">
 | 
				
			||||||
 | 
					     <x>199</x>
 | 
				
			||||||
 | 
					     <y>425</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					    <hint type="destinationlabel">
 | 
				
			||||||
 | 
					     <x>199</x>
 | 
				
			||||||
 | 
					     <y>227</y>
 | 
				
			||||||
 | 
					    </hint>
 | 
				
			||||||
 | 
					   </hints>
 | 
				
			||||||
 | 
					  </connection>
 | 
				
			||||||
 | 
					 </connections>
 | 
				
			||||||
 | 
					</ui>
 | 
				
			||||||
@@ -117,7 +117,7 @@ void ImportPage::updateState()
 | 
				
			|||||||
            if(fi.exists() && (zip || fi.suffix() == "mrpack"))
 | 
					            if(fi.exists() && (zip || fi.suffix() == "mrpack"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                QFileInfo fi(url.fileName());
 | 
					                QFileInfo fi(url.fileName());
 | 
				
			||||||
                dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
 | 
					                dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
 | 
				
			||||||
                dialog->setSuggestedIcon("default");
 | 
					                dialog->setSuggestedIcon("default");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -130,7 +130,7 @@ void ImportPage::updateState()
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            // hook, line and sinker.
 | 
					            // hook, line and sinker.
 | 
				
			||||||
            QFileInfo fi(url.fileName());
 | 
					            QFileInfo fi(url.fileName());
 | 
				
			||||||
            dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
 | 
					            dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
 | 
				
			||||||
            dialog->setSuggestedIcon("default");
 | 
					            dialog->setSuggestedIcon("default");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -201,7 +201,7 @@ void FlamePage::suggestCurrent()
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
 | 
					    dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
 | 
				
			||||||
    QString editedLogoName;
 | 
					    QString editedLogoName;
 | 
				
			||||||
    editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
 | 
					    editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
 | 
				
			||||||
    listModel->getLogo(current.logoName, current.logoUrl,
 | 
					    listModel->getLogo(current.logoName, current.logoUrl,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user