aboutsummaryrefslogtreecommitdiff
path: root/launcher/updater
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/updater')
-rw-r--r--launcher/updater/DownloadTask.cpp173
-rw-r--r--launcher/updater/DownloadTask.h96
-rw-r--r--launcher/updater/DownloadTask_test.cpp195
-rw-r--r--launcher/updater/GoUpdate.cpp198
-rw-r--r--launcher/updater/GoUpdate.h125
-rw-r--r--launcher/updater/UpdateChecker.cpp260
-rw-r--r--launcher/updater/UpdateChecker.h119
-rw-r--r--launcher/updater/UpdateChecker_test.cpp147
-rw-r--r--launcher/updater/testdata/1.json43
-rw-r--r--launcher/updater/testdata/2.json31
-rw-r--r--launcher/updater/testdata/channels.json23
-rw-r--r--launcher/updater/testdata/errorChannels.json23
-rw-r--r--launcher/updater/testdata/fileOneA1
-rw-r--r--launcher/updater/testdata/fileOneB3
-rw-r--r--launcher/updater/testdata/fileThree1
-rw-r--r--launcher/updater/testdata/fileTwo1
-rw-r--r--launcher/updater/testdata/garbageChannels.json22
-rw-r--r--launcher/updater/testdata/index.json9
-rw-r--r--launcher/updater/testdata/noChannels.json5
-rw-r--r--launcher/updater/testdata/oneChannel.json11
-rw-r--r--launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml17
21 files changed, 1503 insertions, 0 deletions
diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp
new file mode 100644
index 00000000..20b26ebb
--- /dev/null
+++ b/launcher/updater/DownloadTask.cpp
@@ -0,0 +1,173 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "DownloadTask.h"
+
+#include "updater/UpdateChecker.h"
+#include "GoUpdate.h"
+#include "net/NetJob.h"
+
+#include <QFile>
+#include <QTemporaryDir>
+#include <QCryptographicHash>
+
+namespace GoUpdate
+{
+
+DownloadTask::DownloadTask(Status status, QString target, QObject *parent)
+ : Task(parent), m_updateFilesDir(target)
+{
+ m_status = status;
+
+ m_updateFilesDir.setAutoRemove(false);
+}
+
+void DownloadTask::executeTask()
+{
+ loadVersionInfo();
+}
+
+void DownloadTask::loadVersionInfo()
+{
+ setStatus(tr("Loading version information..."));
+
+ NetJob *netJob = new NetJob("Version Info");
+
+ // Find the index URL.
+ QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json");
+ qDebug() << m_status.newRepoUrl << " turns into " << newIndexUrl;
+
+ netJob->addNetAction(m_newVersionFileListDownload = Net::Download::makeByteArray(newIndexUrl, &newVersionFileListData));
+
+ // If we have a current version URL, get that one too.
+ if (!m_status.currentRepoUrl.isEmpty())
+ {
+ QUrl cIndexUrl = QUrl(m_status.currentRepoUrl).resolved(QString::number(m_status.currentVersionId) + ".json");
+ netJob->addNetAction(m_currentVersionFileListDownload = Net::Download::makeByteArray(cIndexUrl, &currentVersionFileListData));
+ qDebug() << m_status.currentRepoUrl << " turns into " << cIndexUrl;
+ }
+
+ // connect signals and start the job
+ connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo);
+ connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed);
+ m_vinfoNetJob.reset(netJob);
+ netJob->start();
+}
+
+void DownloadTask::vinfoDownloadFailed()
+{
+ // Something failed. We really need the second download (current version info), so parse
+ // downloads anyways as long as the first one succeeded.
+ if (m_newVersionFileListDownload->wasSuccessful())
+ {
+ processDownloadedVersionInfo();
+ return;
+ }
+
+ // TODO: Give a more detailed error message.
+ qCritical() << "Failed to download version info files.";
+ emitFailed(tr("Failed to download version info files."));
+}
+
+void DownloadTask::processDownloadedVersionInfo()
+{
+ VersionFileList m_currentVersionFileList;
+ VersionFileList m_newVersionFileList;
+
+ setStatus(tr("Reading file list for new version..."));
+ qDebug() << "Reading file list for new version...";
+ QString error;
+ if (!parseVersionInfo(newVersionFileListData, m_newVersionFileList, error))
+ {
+ qCritical() << error;
+ emitFailed(error);
+ return;
+ }
+
+ // if we have the current version info, use it.
+ if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->wasSuccessful())
+ {
+ setStatus(tr("Reading file list for current version..."));
+ qDebug() << "Reading file list for current version...";
+ // if this fails, it's not a complete loss.
+ QString error;
+ if(!parseVersionInfo( currentVersionFileListData, m_currentVersionFileList, error))
+ {
+ qDebug() << error << "This is not a fatal error.";
+ }
+ }
+
+ // We don't need this any more.
+ m_currentVersionFileListDownload.reset();
+ m_newVersionFileListDownload.reset();
+ m_vinfoNetJob.reset();
+
+ setStatus(tr("Processing file lists - figuring out how to install the update..."));
+
+ // make a new netjob for the actual update files
+ NetJobPtr netJob (new NetJob("Update Files"));
+
+ // fill netJob and operationList
+ if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations))
+ {
+ emitFailed(tr("Failed to process update lists..."));
+ return;
+ }
+
+ // Now start the download.
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished);
+ QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed);
+
+ if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/MultiMC5/issues/1701
+ {
+ setStatus(tr("Downloading one update file."));
+ }
+ else
+ {
+ setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
+ }
+ qDebug() << "Begin downloading update files to" << m_updateFilesDir.path();
+ m_filesNetJob = netJob;
+ m_filesNetJob->start();
+}
+
+void DownloadTask::fileDownloadFinished()
+{
+ emitSucceeded();
+}
+
+void DownloadTask::fileDownloadFailed(QString reason)
+{
+ qCritical() << "Failed to download update files:" << reason;
+ emitFailed(tr("Failed to download update files: %1").arg(reason));
+}
+
+void DownloadTask::fileDownloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress(current, total);
+}
+
+QString DownloadTask::updateFilesDir()
+{
+ return m_updateFilesDir.path();
+}
+
+OperationList DownloadTask::operations()
+{
+ return m_operations;
+}
+
+} \ No newline at end of file
diff --git a/launcher/updater/DownloadTask.h b/launcher/updater/DownloadTask.h
new file mode 100644
index 00000000..fc5030b4
--- /dev/null
+++ b/launcher/updater/DownloadTask.h
@@ -0,0 +1,96 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "GoUpdate.h"
+
+namespace GoUpdate
+{
+/*!
+ * The DownloadTask is a task that takes a given version ID and repository URL,
+ * downloads that version's files from the repository, and prepares to install them.
+ */
+class DownloadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ /**
+ * Create a download task
+ *
+ * target is a template - XXXXXX at the end will be replaced with a random generated string, ensuring uniqueness
+ */
+ explicit DownloadTask(Status status, QString target, QObject* parent = 0);
+ virtual ~DownloadTask() {};
+
+ /// Get the directory that will contain the update files.
+ QString updateFilesDir();
+
+ /// Get the list of operations that should be done
+ OperationList operations();
+
+ /// set updater download behavior
+ void setUseLocalUpdater(bool useLocal);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ /*!
+ * Downloads the version info files from the repository.
+ * The files for both the current build, and the build that we're updating to need to be downloaded.
+ * If the current version's info file can't be found, MultiMC will not delete files that
+ * were removed between versions. It will still replace files that have changed, however.
+ * Note that although the repository URL for the current version is not given to the update task,
+ * the task will attempt to look it up in the UpdateChecker's channel list.
+ * If an error occurs here, the function will call emitFailed and return false.
+ */
+ void loadVersionInfo();
+
+ NetJobPtr m_vinfoNetJob;
+ QByteArray currentVersionFileListData;
+ QByteArray newVersionFileListData;
+ Net::Download::Ptr m_currentVersionFileListDownload;
+ Net::Download::Ptr m_newVersionFileListDownload;
+
+ NetJobPtr m_filesNetJob;
+
+ Status m_status;
+
+ OperationList m_operations;
+
+ /*!
+ * Temporary directory to store update files in.
+ * This will be set to not auto delete. Task will fail if this fails to be created.
+ */
+ QTemporaryDir m_updateFilesDir;
+
+protected slots:
+ /*!
+ * This function is called when version information is finished downloading
+ * and at least the new file list download succeeded
+ */
+ void processDownloadedVersionInfo();
+ void vinfoDownloadFailed();
+
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+ void fileDownloadProgressChanged(qint64 current, qint64 total);
+};
+
+}
diff --git a/launcher/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp
new file mode 100644
index 00000000..8d5375e8
--- /dev/null
+++ b/launcher/updater/DownloadTask_test.cpp
@@ -0,0 +1,195 @@
+#include <QTest>
+#include <QSignalSpy>
+
+#include "TestUtil.h"
+
+#include "updater/GoUpdate.h"
+#include "updater/DownloadTask.h"
+#include "updater/UpdateChecker.h"
+#include <FileSystem.h>
+
+using namespace GoUpdate;
+
+FileSourceList encodeBaseFile(const char *suffix)
+{
+ auto base = QDir::currentPath();
+ QUrl localFile = QUrl::fromLocalFile(base + suffix);
+ QString localUrlString = localFile.toString(QUrl::FullyEncoded);
+ auto item = FileSource("http", localUrlString);
+ return FileSourceList({item});
+}
+
+Q_DECLARE_METATYPE(VersionFileList)
+Q_DECLARE_METATYPE(Operation)
+
+QDebug operator<<(QDebug dbg, const FileSource &f)
+{
+ dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url
+ << " comp=" << f.compressionType << ")";
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const VersionFileEntry &v)
+{
+ dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode
+ << " md5=" << v.md5 << " sources=" << v.sources << ")";
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const Operation::Type &t)
+{
+ switch (t)
+ {
+ case Operation::OP_REPLACE:
+ dbg << "OP_COPY";
+ break;
+ case Operation::OP_DELETE:
+ dbg << "OP_DELETE";
+ break;
+ }
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const Operation &u)
+{
+ dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source
+ << " dest=" << u.destination << " mode=" << u.destinationMode << ")";
+ return dbg.maybeSpace();
+}
+
+class DownloadTaskTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+ }
+ void cleanupTestCase()
+ {
+ }
+
+ void test_parseVersionInfo_data()
+ {
+ QTest::addColumn<QByteArray>("data");
+ QTest::addColumn<VersionFileList>("list");
+ QTest::addColumn<QString>("error");
+ QTest::addColumn<bool>("ret");
+
+ QTest::newRow("one")
+ << MULTIMC_GET_TEST_FILE("data/1.json")
+ << (VersionFileList()
+ << VersionFileEntry{"fileOne",
+ 493,
+ encodeBaseFile("/data/fileOneA"),
+ "9eb84090956c484e32cb6c08455a667b"}
+ << VersionFileEntry{"fileTwo",
+ 644,
+ encodeBaseFile("/data/fileTwo"),
+ "38f94f54fa3eb72b0ea836538c10b043"}
+ << VersionFileEntry{"fileThree",
+ 750,
+ encodeBaseFile("/data/fileThree"),
+ "f12df554b21e320be6471d7154130e70"})
+ << QString() << true;
+ QTest::newRow("two")
+ << MULTIMC_GET_TEST_FILE("data/2.json")
+ << (VersionFileList()
+ << VersionFileEntry{"fileOne",
+ 493,
+ encodeBaseFile("/data/fileOneB"),
+ "42915a71277c9016668cce7b82c6b577"}
+ << VersionFileEntry{"fileTwo",
+ 644,
+ encodeBaseFile("/data/fileTwo"),
+ "38f94f54fa3eb72b0ea836538c10b043"})
+ << QString() << true;
+ }
+ void test_parseVersionInfo()
+ {
+ QFETCH(QByteArray, data);
+ QFETCH(VersionFileList, list);
+ QFETCH(QString, error);
+ QFETCH(bool, ret);
+
+ VersionFileList outList;
+ QString outError;
+ bool outRet = parseVersionInfo(data, outList, outError);
+ QCOMPARE(outRet, ret);
+ QCOMPARE(outList, list);
+ QCOMPARE(outError, error);
+ }
+
+ void test_processFileLists_data()
+ {
+ QTest::addColumn<QString>("tempFolder");
+ QTest::addColumn<VersionFileList>("currentVersion");
+ QTest::addColumn<VersionFileList>("newVersion");
+ QTest::addColumn<OperationList>("expectedOperations");
+
+ QTemporaryDir tempFolderObj;
+ QString tempFolder = tempFolderObj.path();
+ // update fileOne, keep fileTwo, remove fileThree
+ QTest::newRow("test 1")
+ << tempFolder << (VersionFileList()
+ << VersionFileEntry{
+ "data/fileOne", 493,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileOne-1"),
+ "9eb84090956c484e32cb6c08455a667b"}
+ << VersionFileEntry{
+ "data/fileTwo", 644,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileTwo-1"),
+ "38f94f54fa3eb72b0ea836538c10b043"}
+ << VersionFileEntry{
+ "data/fileThree", 420,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileThree-1"),
+ "f12df554b21e320be6471d7154130e70"})
+ << (VersionFileList()
+ << VersionFileEntry{
+ "data/fileOne", 493,
+ FileSourceList()
+ << FileSource("http",
+ "http://host/path/fileOne-2"),
+ "42915a71277c9016668cce7b82c6b577"}
+ << VersionFileEntry{
+ "data/fileTwo", 644,
+ FileSourceList()
+ << FileSource("http",
+ "http://host/path/fileTwo-2"),
+ "38f94f54fa3eb72b0ea836538c10b043"})
+ << (OperationList()
+ << Operation::DeleteOp("data/fileThree")
+ << Operation::CopyOp(
+ FS::PathCombine(tempFolder,
+ QString("data/fileOne").replace("/", "_")),
+ "data/fileOne", 493));
+ }
+ void test_processFileLists()
+ {
+ QFETCH(QString, tempFolder);
+ QFETCH(VersionFileList, currentVersion);
+ QFETCH(VersionFileList, newVersion);
+ QFETCH(OperationList, expectedOperations);
+
+ OperationList operations;
+
+ processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy"), operations);
+ qDebug() << (operations == expectedOperations);
+ qDebug() << operations;
+ qDebug() << expectedOperations;
+ QCOMPARE(operations, expectedOperations);
+ }
+};
+
+extern "C"
+{
+ QTEST_GUILESS_MAIN(DownloadTaskTest)
+}
+
+#include "DownloadTask_test.moc"
diff --git a/launcher/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp
new file mode 100644
index 00000000..6167418e
--- /dev/null
+++ b/launcher/updater/GoUpdate.cpp
@@ -0,0 +1,198 @@
+#include "GoUpdate.h"
+#include <QDebug>
+#include <QDomDocument>
+#include <QFile>
+#include <FileSystem.h>
+
+#include "net/Download.h"
+#include "net/ChecksumValidator.h"
+
+namespace GoUpdate
+{
+
+bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ error = QString("Failed to parse version info JSON: %1 at %2")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset);
+ qCritical() << error;
+ return false;
+ }
+
+ QJsonObject json = jsonDoc.object();
+
+ qDebug() << data;
+ qDebug() << "Loading version info from JSON.";
+ QJsonArray filesArray = json.value("Files").toArray();
+ for (QJsonValue fileValue : filesArray)
+ {
+ QJsonObject fileObj = fileValue.toObject();
+
+ QString file_path = fileObj.value("Path").toString();
+
+ VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
+ FileSourceList(), fileObj.value("MD5").toString(), };
+ qDebug() << "File" << file.path << "with perms" << file.mode;
+
+ QJsonArray sourceArray = fileObj.value("Sources").toArray();
+ for (QJsonValue val : sourceArray)
+ {
+ QJsonObject sourceObj = val.toObject();
+
+ QString type = sourceObj.value("SourceType").toString();
+ if (type == "http")
+ {
+ file.sources.append(FileSource("http", sourceObj.value("Url").toString()));
+ }
+ else
+ {
+ qWarning() << "Unknown source type" << type << "ignored.";
+ }
+ }
+
+ qDebug() << "Loaded info for" << file.path;
+
+ list.append(file);
+ }
+
+ return true;
+}
+
+bool processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+)
+{
+ // First, if we've loaded the current version's file list, we need to iterate through it and
+ // delete anything in the current one version's list that isn't in the new version's list.
+ for (VersionFileEntry entry : currentVersion)
+ {
+ QFileInfo toDelete(FS::PathCombine(rootPath, entry.path));
+ if (!toDelete.exists())
+ {
+ qCritical() << "Expected file " << toDelete.absoluteFilePath()
+ << " doesn't exist!";
+ }
+ bool keep = false;
+
+ //
+ for (VersionFileEntry newEntry : newVersion)
+ {
+ if (newEntry.path == entry.path)
+ {
+ qDebug() << "Not deleting" << entry.path
+ << "because it is still present in the new version.";
+ keep = true;
+ break;
+ }
+ }
+
+ // If the loop reaches the end and we didn't find a match, delete the file.
+ if (!keep)
+ {
+ if (toDelete.exists())
+ ops.append(Operation::DeleteOp(entry.path));
+ }
+ }
+
+ // Next, check each file in MultiMC's folder and see if we need to update them.
+ for (VersionFileEntry entry : newVersion)
+ {
+ // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a
+ // way to do this in the background.
+ QString fileMD5;
+ QString realEntryPath = FS::PathCombine(rootPath, entry.path);
+ QFile entryFile(realEntryPath);
+ QFileInfo entryInfo(realEntryPath);
+
+ bool needs_upgrade = false;
+ if (!entryFile.exists())
+ {
+ needs_upgrade = true;
+ }
+ else
+ {
+ bool pass = true;
+ if (!entryInfo.isReadable())
+ {
+ qCritical() << "File " << realEntryPath << " is not readable.";
+ pass = false;
+ }
+ if (!entryInfo.isWritable())
+ {
+ qCritical() << "File " << realEntryPath << " is not writable.";
+ pass = false;
+ }
+ if (!entryFile.open(QFile::ReadOnly))
+ {
+ qCritical() << "File " << realEntryPath << " cannot be opened for reading.";
+ pass = false;
+ }
+ if (!pass)
+ {
+ ops.clear();
+ return false;
+ }
+ }
+
+ if(!needs_upgrade)
+ {
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ auto foo = entryFile.readAll();
+
+ hash.addData(foo);
+ fileMD5 = hash.result().toHex();
+ if ((fileMD5 != entry.md5))
+ {
+ qDebug() << "MD5Sum does not match!";
+ qDebug() << "Expected:'" << entry.md5 << "'";
+ qDebug() << "Got: '" << fileMD5 << "'";
+ needs_upgrade = true;
+ }
+ }
+
+ // skip file. it doesn't need an upgrade.
+ if (!needs_upgrade)
+ {
+ qDebug() << "File" << realEntryPath << " does not need updating.";
+ continue;
+ }
+
+ // yep. this file actually needs an upgrade. PROCEED.
+ qDebug() << "Found file" << realEntryPath << " that needs updating.";
+
+ // Go through the sources list and find one to use.
+ // TODO: Make a NetAction that takes a source list and tries each of them until one
+ // works. For now, we'll just use the first http one.
+ for (FileSource source : entry.sources)
+ {
+ if (source.type != "http")
+ continue;
+
+ qDebug() << "Will download" << entry.path << "from" << source.url;
+
+ // Download it to updatedir/<filepath>-<md5> where filepath is the file's
+ // path with slashes replaced by underscores.
+ QString dlPath = FS::PathCombine(tempPath, QString(entry.path).replace("/", "_"));
+
+ // We need to download the file to the updatefiles folder and add a task
+ // to copy it to its install path.
+ auto download = Net::Download::makeFile(source.url, dlPath);
+ auto rawMd5 = QByteArray::fromHex(entry.md5.toLatin1());
+ download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ job->addNetAction(download);
+ ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ return true;
+}
+}
diff --git a/launcher/updater/GoUpdate.h b/launcher/updater/GoUpdate.h
new file mode 100644
index 00000000..8058e543
--- /dev/null
+++ b/launcher/updater/GoUpdate.h
@@ -0,0 +1,125 @@
+#pragma once
+#include <QByteArray>
+#include <net/NetJob.h>
+
+namespace GoUpdate
+{
+
+/**
+ * A temporary object exchanged between updated checker and the actual update task
+ */
+struct Status
+{
+ bool updateAvailable = false;
+
+ int newVersionId = -1;
+ QString newRepoUrl;
+
+ int currentVersionId = -1;
+ QString currentRepoUrl;
+
+ // path to the root of the application
+ QString rootPath;
+};
+
+/**
+ * Struct that describes an entry in a VersionFileEntry's `Sources` list.
+ */
+struct FileSource
+{
+ FileSource(QString type, QString url, QString compression="")
+ {
+ this->type = type;
+ this->url = url;
+ this->compressionType = compression;
+ }
+
+ bool operator==(const FileSource &f2) const
+ {
+ return type == f2.type && url == f2.url && compressionType == f2.compressionType;
+ }
+
+ QString type;
+ QString url;
+ QString compressionType;
+};
+typedef QList<FileSource> FileSourceList;
+
+/**
+ * Structure that describes an entry in a GoUpdate version's `Files` list.
+ */
+struct VersionFileEntry
+{
+ QString path;
+ int mode;
+ FileSourceList sources;
+ QString md5;
+ bool operator==(const VersionFileEntry &v2) const
+ {
+ return path == v2.path && mode == v2.mode && sources == v2.sources && md5 == v2.md5;
+ }
+};
+typedef QList<VersionFileEntry> VersionFileList;
+
+/**
+ * Structure that describes an operation to perform when installing updates.
+ */
+struct Operation
+{
+ static Operation CopyOp(QString from, QString to, int fmode=0644)
+ {
+ return Operation{OP_REPLACE, from, to, fmode};
+ }
+ static Operation DeleteOp(QString file)
+ {
+ return Operation{OP_DELETE, QString(), file, 0644};
+ }
+
+ // FIXME: for some types, some of the other fields are irrelevant!
+ bool operator==(const Operation &u2) const
+ {
+ return type == u2.type &&
+ source == u2.source &&
+ destination == u2.destination &&
+ destinationMode == u2.destinationMode;
+ }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_REPLACE,
+ OP_DELETE,
+ } type;
+
+ //! The source file, if any
+ QString source;
+
+ //! The destination file.
+ QString destination;
+
+ //! The mode to change the destination file to.
+ int destinationMode;
+};
+typedef QList<Operation> OperationList;
+
+/**
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+bool parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error);
+
+/*!
+ * Takes a list of file entries for the current version's files and the new version's files
+ * and populates the downloadList and operationList with information about how to download and install the update.
+ */
+bool processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+);
+
+}
+Q_DECLARE_METATYPE(GoUpdate::Status)
diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..be33c73c
--- /dev/null
+++ b/launcher/updater/UpdateChecker.cpp
@@ -0,0 +1,260 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "UpdateChecker.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QDebug>
+
+#define API_VERSION 0
+#define CHANLIST_FORMAT 0
+
+UpdateChecker::UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild)
+{
+ m_channelListUrl = channelListUrl;
+ m_currentChannel = currentChannel;
+ m_currentBuild = currentBuild;
+}
+
+QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const
+{
+ return m_channels;
+}
+
+bool UpdateChecker::hasChannels() const
+{
+ return !m_channels.isEmpty();
+}
+
+void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate)
+{
+ qDebug() << "Checking for updates.";
+
+ // If the channel list hasn't loaded yet, load it and defer checking for updates until
+ // later.
+ if (!m_chanListLoaded)
+ {
+ qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring "
+ "update check.";
+ m_checkUpdateWaiting = true;
+ m_deferredUpdateChannel = updateChannel;
+ updateChanList(notifyNoUpdate);
+ return;
+ }
+
+ if (m_updateChecking)
+ {
+ qDebug() << "Ignoring update check request. Already checking for updates.";
+ return;
+ }
+
+ m_updateChecking = true;
+
+ // Find the desired channel within the channel list and get its repo URL. If if cannot be
+ // found, error.
+ m_newRepoUrl = "";
+ for (ChannelListEntry entry : m_channels)
+ {
+ if (entry.id == updateChannel)
+ m_newRepoUrl = entry.url;
+ if (entry.id == m_currentChannel)
+ m_currentRepoUrl = entry.url;
+ }
+
+ qDebug() << "m_repoUrl = " << m_newRepoUrl;