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:
parent
fcbe233fdb
commit
699ad316f0
@ -128,6 +128,8 @@ set(NET_SOURCES
|
||||
net/PasteUpload.h
|
||||
net/Sink.h
|
||||
net/Validator.h
|
||||
net/Upload.cpp
|
||||
net/Upload.h
|
||||
)
|
||||
|
||||
# Game launch logic
|
||||
@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/SkinUploadDialog.h
|
||||
ui/dialogs/ModDownloadDialog.cpp
|
||||
ui/dialogs/ModDownloadDialog.h
|
||||
ui/dialogs/ScrollMessageBox.cpp
|
||||
ui/dialogs/ScrollMessageBox.h
|
||||
|
||||
# GUI - widgets
|
||||
ui/widgets/Common.cpp
|
||||
@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI
|
||||
ui/dialogs/LoginDialog.ui
|
||||
ui/dialogs/EditAccountDialog.ui
|
||||
ui/dialogs/ReviewMessageBox.ui
|
||||
ui/dialogs/ScrollMessageBox.ui
|
||||
)
|
||||
|
||||
qt5_add_resources(LAUNCHER_RESOURCES
|
||||
|
@ -60,9 +60,9 @@
|
||||
#include "net/ChecksumValidator.h"
|
||||
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/ScrollMessageBox.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
|
||||
{
|
||||
@ -394,61 +394,136 @@ void InstanceImportTask::processFlame()
|
||||
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
|
||||
{
|
||||
auto results = m_modIdResolver->getResults();
|
||||
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
|
||||
for(const auto& result: results.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:
|
||||
{
|
||||
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;
|
||||
//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;
|
||||
}
|
||||
}
|
||||
m_modIdResolver.reset();
|
||||
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
|
||||
{
|
||||
m_filesNetJob.reset();
|
||||
emitSucceeded();
|
||||
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());
|
||||
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();
|
||||
});
|
||||
}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_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)
|
||||
@ -524,11 +599,11 @@ void InstanceImportTask::processModrinth()
|
||||
|
||||
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
|
||||
bool had_optional = false;
|
||||
for (auto& obj : jsonFiles) {
|
||||
for (auto& modInfo : jsonFiles) {
|
||||
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");
|
||||
if (support == "unsupported") {
|
||||
continue;
|
||||
@ -546,7 +621,7 @@ void InstanceImportTask::processModrinth()
|
||||
file.path += ".disabled";
|
||||
}
|
||||
|
||||
QJsonObject hashes = Json::requireObject(obj, "hashes");
|
||||
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
|
||||
QString hash;
|
||||
QCryptographicHash::Algorithm hashAlgorithm;
|
||||
hash = Json::ensureString(hashes, "sha1");
|
||||
@ -566,7 +641,7 @@ void InstanceImportTask::processModrinth()
|
||||
file.hashAlgorithm = hashAlgorithm;
|
||||
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
|
||||
// (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)) {
|
||||
throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
||||
#include <QFutureWatcher>
|
||||
#include "settings/SettingsObject.h"
|
||||
#include "QObjectPtr.h"
|
||||
#include "modplatform/flame/PackManifest.h"
|
||||
|
||||
#include <nonstd/optional>
|
||||
|
||||
@ -59,6 +60,10 @@ public:
|
||||
|
||||
bool canAbort() const override { return true; }
|
||||
bool abort() override;
|
||||
const QVector<Flame::File> &getBlockedFiles() const
|
||||
{
|
||||
return m_blockedMods;
|
||||
}
|
||||
|
||||
protected:
|
||||
//! Entry point for tasks.
|
||||
@ -87,6 +92,7 @@ private: /* data */
|
||||
std::unique_ptr<QuaZip> m_packZip;
|
||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
||||
QVector<Flame::File> m_blockedMods;
|
||||
enum class ModpackType{
|
||||
Unknown,
|
||||
MultiMC,
|
||||
|
@ -1,7 +1,9 @@
|
||||
#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)
|
||||
{}
|
||||
|
||||
@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
|
||||
setStatus(tr("Resolving mod IDs..."));
|
||||
setProgress(0, m_toProcess.files.size());
|
||||
m_dljob = new NetJob("Mod id resolver", m_network);
|
||||
results.resize(m_toProcess.files.size());
|
||||
int index = 0;
|
||||
for (auto& file : m_toProcess.files) {
|
||||
auto projectIdStr = QString::number(file.projectId);
|
||||
auto fileIdStr = QString::number(file.fileId);
|
||||
QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
|
||||
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
|
||||
m_dljob->addNetAction(dl);
|
||||
index++;
|
||||
}
|
||||
result.reset(new QByteArray());
|
||||
//build json data to send
|
||||
QJsonObject object;
|
||||
|
||||
object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
|
||||
l.push_back(s.fileId);
|
||||
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);
|
||||
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
|
||||
m_dljob->start();
|
||||
}
|
||||
|
||||
void Flame::FileResolvingTask::netJobFinished()
|
||||
{
|
||||
bool failed = false;
|
||||
int index = 0;
|
||||
for (auto& bytes : results) {
|
||||
auto& out = m_toProcess.files[index];
|
||||
// job to check modrinth for blocked projects
|
||||
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 {
|
||||
failed &= (!out.parseFromBytes(bytes));
|
||||
out.parseFromObject(Json::requireObject(file));
|
||||
} catch (const JSONValidationError& e) {
|
||||
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
|
||||
qCritical() << e.cause();
|
||||
qCritical() << "JSON:";
|
||||
qCritical() << bytes;
|
||||
failed = true;
|
||||
qDebug() << "Blocked mod on curseforge" << out.fileName;
|
||||
auto hash = out.hash;
|
||||
if(!hash.isEmpty()) {
|
||||
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
|
||||
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++;
|
||||
}
|
||||
if (!failed) {
|
||||
emitSucceeded();
|
||||
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();
|
||||
});
|
||||
slugJob->start();
|
||||
} else {
|
||||
emitFailed(tr("Some mod ID resolving tasks failed."));
|
||||
emitSucceeded();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ class FileResolvingTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
|
||||
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
|
||||
virtual ~FileResolvingTask() {};
|
||||
|
||||
const Flame::Manifest &getResults() const
|
||||
@ -27,7 +27,11 @@ protected slots:
|
||||
private: /* data */
|
||||
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
||||
Flame::Manifest m_toProcess;
|
||||
QVector<QByteArray> results;
|
||||
std::shared_ptr<QByteArray> result;
|
||||
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);
|
||||
Flame::File file;
|
||||
loadFileV1(file, obj);
|
||||
m.files.append(file);
|
||||
m.files.insert(file.fileId,file);
|
||||
}
|
||||
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
|
||||
}
|
||||
@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
|
||||
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");
|
||||
|
||||
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
|
||||
// It is also optional
|
||||
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
|
||||
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;
|
||||
return true;
|
||||
|
@ -2,19 +2,24 @@
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace Flame
|
||||
{
|
||||
struct File
|
||||
{
|
||||
// NOTE: throws JSONValidationError
|
||||
bool parseFromBytes(const QByteArray &bytes);
|
||||
bool parseFromObject(const QJsonObject& object);
|
||||
|
||||
int projectId = 0;
|
||||
int fileId = 0;
|
||||
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
|
||||
bool required = true;
|
||||
QString hash;
|
||||
// NOTE: only set on blocked files ! Empty otherwise.
|
||||
QString websiteUrl;
|
||||
|
||||
// our
|
||||
bool resolved = false;
|
||||
@ -54,7 +59,8 @@ struct Manifest
|
||||
QString name;
|
||||
QString version;
|
||||
QString author;
|
||||
QVector<Flame::File> files;
|
||||
//File id -> File
|
||||
QMap<int,Flame::File> files;
|
||||
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"))
|
||||
{
|
||||
QFileInfo fi(url.fileName());
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
|
||||
dialog->setSuggestedIcon("default");
|
||||
}
|
||||
}
|
||||
@ -130,7 +130,7 @@ void ImportPage::updateState()
|
||||
}
|
||||
// hook, line and sinker.
|
||||
QFileInfo fi(url.fileName());
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
|
||||
dialog->setSuggestedIcon("default");
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ void FlamePage::suggestCurrent()
|
||||
return;
|
||||
}
|
||||
|
||||
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
|
||||
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
|
||||
QString editedLogoName;
|
||||
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
|
||||
listModel->getLogo(current.logoName, current.logoUrl,
|
||||
|
Loading…
Reference in New Issue
Block a user