summaryrefslogtreecommitdiffstats
path: root/api/logic/updater
diff options
context:
space:
mode:
Diffstat (limited to 'api/logic/updater')
-rw-r--r--api/logic/updater/DownloadTask.cpp169
-rw-r--r--api/logic/updater/DownloadTask.h95
-rw-r--r--api/logic/updater/GoUpdate.cpp215
-rw-r--r--api/logic/updater/GoUpdate.h133
-rw-r--r--api/logic/updater/UpdateChecker.cpp269
-rw-r--r--api/logic/updater/UpdateChecker.h121
6 files changed, 1002 insertions, 0 deletions
diff --git a/api/logic/updater/DownloadTask.cpp b/api/logic/updater/DownloadTask.cpp
new file mode 100644
index 00000000..6947e8bf
--- /dev/null
+++ b/api/logic/updater/DownloadTask.cpp
@@ -0,0 +1,169 @@
+/* Copyright 2013-2014 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..."));
+
+ m_currentVersionFileListDownload.reset();
+ m_newVersionFileListDownload.reset();
+
+ 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 = ByteArrayDownload::make(newIndexUrl));
+
+ // 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 = ByteArrayDownload::make(cIndexUrl));
+ 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->m_status != Job_Failed)
+ {
+ 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(m_newVersionFileListDownload->m_data, m_newVersionFileList, error))
+ {
+ qCritical() << error;
+ emitFailed(error);
+ return;
+ }
+
+ // if we have the current version info, use it.
+ if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->m_status != Job_Failed)
+ {
+ 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( m_currentVersionFileListDownload->m_data, 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);
+
+ 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/api/logic/updater/DownloadTask.h b/api/logic/updater/DownloadTask.h
new file mode 100644
index 00000000..83b4a142
--- /dev/null
+++ b/api/logic/updater/DownloadTask.h
@@ -0,0 +1,95 @@
+/* Copyright 2013-2014 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"
+
+#include "multimc_logic_export.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 MULTIMC_LOGIC_EXPORT 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);
+
+ /// 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;
+ ByteArrayDownloadPtr m_currentVersionFileListDownload;
+ ByteArrayDownloadPtr 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);
+};
+
+} \ No newline at end of file
diff --git a/api/logic/updater/GoUpdate.cpp b/api/logic/updater/GoUpdate.cpp
new file mode 100644
index 00000000..4e465d5c
--- /dev/null
+++ b/api/logic/updater/GoUpdate.cpp
@@ -0,0 +1,215 @@
+#include "GoUpdate.h"
+#include <QDebug>
+#include <QDomDocument>
+#include <QFile>
+#include <FileSystem.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();
+#ifdef Q_OS_MAC
+ // On OSX, the paths for the updater need to be fixed.
+ // basically, anything that isn't in the .app folder is ignored.
+ // everything else is changed so the code that processes the files actually finds
+ // them and puts the replacements in the right spots.
+ fixPathForOSX(file_path);
+#endif
+ 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 = MD5EtagDownload::make(source.url, dlPath);
+ download->m_expected_md5 = entry.md5;
+ job->addNetAction(download);
+ ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ return true;
+}
+
+bool fixPathForOSX(QString &path)
+{
+ if (path.startsWith("MultiMC.app/"))
+ {
+ // remove the prefix and add a new, more appropriate one.
+ path.remove(0, 12);
+ return true;
+ }
+ else
+ {
+ qCritical() << "Update path not within .app: " << path;
+ return false;
+ }
+}
+} \ No newline at end of file
diff --git a/api/logic/updater/GoUpdate.h b/api/logic/updater/GoUpdate.h
new file mode 100644
index 00000000..b8a534de
--- /dev/null
+++ b/api/logic/updater/GoUpdate.h
@@ -0,0 +1,133 @@
+#pragma once
+#include <QByteArray>
+#include <net/NetJob.h>
+
+#include "multimc_logic_export.h"
+
+namespace GoUpdate
+{
+
+/**
+ * A temporary object exchanged between updated checker and the actual update task
+ */
+struct MULTIMC_LOGIC_EXPORT 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 MULTIMC_LOGIC_EXPORT 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 MULTIMC_LOGIC_EXPORT 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 MULTIMC_LOGIC_EXPORT Operation
+{
+ static Operation CopyOp(QString fsource, QString fdest, int fmode=0644)
+ {
+ return Operation{OP_REPLACE, fsource, fdest, fmode};
+ }
+ static Operation DeleteOp(QString file)
+ {
+ return Operation{OP_DELETE, file, "", 0644};
+ }
+
+ // FIXME: for some types, some of the other fields are irrelevant!
+ bool operator==(const Operation &u2) const
+ {
+ return type == u2.type && file == u2.file && dest == u2.dest && mode == u2.mode;
+ }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_REPLACE,
+ OP_DELETE,
+ } type;
+
+ //! The file to operate on.
+ QString file;
+
+ //! The destination file.
+ QString dest;
+
+ //! The mode to change the source file to.
+ int mode;
+};
+typedef QList<Operation> OperationList;
+
+/**
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+bool MULTIMC_LOGIC_EXPORT 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 MULTIMC_LOGIC_EXPORT processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+);
+
+/*!
+ * This fixes destination paths for OSX - removes 'MultiMC.app' prefix
+ * The updater runs in MultiMC.app/Contents/MacOs by default
+ * The destination paths are such as this: MultiMC.app/blah/blah
+ *
+ * @return false if the path couldn't be fixed (is invalid)
+ */
+bool MULTIMC_LOGIC_EXPORT fixPathForOSX(QString &path);
+
+}
+Q_DECLARE_METATYPE(GoUpdate::Status); \ No newline at end of file
diff --git a/api/logic/updater/UpdateChecker.cpp b/api/logic/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..1cdac916
--- /dev/null
+++ b/api/logic/updater/UpdateChecker.cpp
@@ -0,0 +1,269 @@
+/* Copyright 2013-2015 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;
+
+ m_updateChecking = false;
+ m_chanListLoading = false;
+ m_checkUpdateWaiting = false;
+ m_chanListLoaded = false;
+}
+
+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;
+
+ // If we didn't find our channel, error.
+ if (m_newRepoUrl.isEmpty())
+ {
+ qCritical() << "m_repoUrl is empty!";
+ emit updateCheckFailed();
+ return;
+ }
+
+ QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json"));
+
+ auto job = new NetJob("GoUpdate Repository Index");
+ job->addNetAction(ByteArrayDownload::make(indexUrl));
+ connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
+ { updateCheckFinished(notifyNoUpdate); });
+ connect(job, &NetJob::failed, this, &UpdateChecker::updateCheckFailed);
+ indexJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::updateCheckFinished(bool notifyNoUpdate)
+{
+ qDebug() << "Finished downloading repo index. Checking for new versions.";
+
+ QJsonParseError jsonError;
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl =
+ std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first());
+ data = dl->m_data;
+ indexJob.reset();
+ }
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
+ {
+ qCritical() << "Failed to parse GoUpdate repository index. JSON error"
+ << jsonError.errorString() << "at offset" << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int apiVersion = object.value("ApiVersion").toVariant().toInt(&success);
+ if (apiVersion != API_VERSION || !success)
+ {
+ qCritical() << "Failed to check for updates. API version mismatch. We're using"
+ << API_VERSION << "server has" << apiVersion;
+ return;
+ }
+
+ qDebug() << "Processing repository version list.";
+ QJsonObject newestVersion;
+ QJsonArray versions = object.value("Versions").toArray();
+ for (QJsonValue versionVal : versions)
+ {
+ QJsonObject version = versionVal.toObject();
+ if (newestVersion.value("Id").toVariant().toInt() <
+ version.value("Id").toVariant().toInt())
+ {
+ newestVersion = version;
+ }
+ }
+
+ // We've got the version with the greatest ID number. Now compare it to our current build
+ // number and update if they're different.
+ int newBuildNumber = newestVersion.value("Id").toVariant().toInt();
+ if (newBuildNumber != m_currentBuild)
+ {
+ qDebug() << "Found newer version with ID" << newBuildNumber;
+ // Update!
+ GoUpdate::Status updateStatus;
+ updateStatus.updateAvailable = true;
+ updateStatus.currentVersionId = m_currentBuild;
+ updateStatus.currentRepoUrl = m_currentRepoUrl;
+ updateStatus.newVersionId = newBuildNumber;
+ updateStatus.newRepoUrl = m_newRepoUrl;
+ emit updateAvailable(updateStatus);
+ }
+ else if (notifyNoUpdate)
+ {
+ emit noUpdateFound();
+ }
+
+ m_updateChecking = false;
+}
+
+void UpdateChecker::updateCheckFailed()
+{
+ qCritical() << "Update check failed for reasons unknown.";
+}
+
+void UpdateChecker::updateChanList(bool notifyNoUpdate)
+{
+ qDebug() << "Loading the channel list.";
+
+ if (m_channelListUrl.isEmpty())
+ {
+ qCritical() << "Failed to update channel list. No channel list URL set."
+ << "If you'd like to use MultiMC's update system, please pass the channel "
+ "list URL to CMake at compile time.";
+ return;
+ }
+
+ m_chanListLoading = true;
+ NetJob *job = new NetJob("Update System Channel List");
+ job->addNetAction(ByteArrayDownload::make(QUrl(m_channelListUrl)));
+ connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
+ { chanListDownloadFinished(notifyNoUpdate); });
+ QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
+ chanListJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate)
+{
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl =
+ std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first());
+ data = dl->m_data;
+ chanListJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ // TODO: Report errors to the user.
+ qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at"
+ << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int formatVersion = object.value("format_version").toVariant().toInt(&success);
+ if (formatVersion != CHANLIST_FORMAT || !success)
+ {
+ qCritical()
+ << "Failed to check for updates. Channel list format version mismatch. We're using"
+ << CHANLIST_FORMAT << "server has" << formatVersion;
+ return;
+ }
+
+ // Load channels into a temporary array.
+ QList<ChannelListEntry> loadedChannels;
+ QJsonArray channelArray = object.value("channels").toArray();
+ for (QJsonValue chanVal : channelArray)
+ {
+ QJsonObject channelObj = chanVal.toObject();
+ ChannelListEntry entry{channelObj.value("id").toVariant().toString(),
+ channelObj.value("name").toVariant().toString(),
+ channelObj.value("description").toVariant().toString(),
+ channelObj.value("url").toVariant().toString()};
+ if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty())
+ {
+ qCritical() << "Channel list entry with empty ID, name, or URL. Skipping.";
+ continue;
+ }
+ loadedChannels.append(entry);
+ }
+
+ // Swap the channel list we just loaded into the object's channel list.
+ m_channels.swap(loadedChannels);
+
+ m_chanListLoading = false;
+ m_chanListLoaded = true;
+ qDebug() << "Successfully loaded UpdateChecker channel list.";
+
+ // If we're waiting to check for updates, do that now.
+ if (m_checkUpdateWaiting)
+ checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate);
+
+ emit channelListLoaded();
+}
+
+void UpdateChecker::chanListDownloadFailed(QString reason)
+{
+ m_chanListLoading = false;
+ qCritical() << QString("Failed to download channel list: %1").arg(reason);
+ emit channelListLoaded();
+}
+
diff --git a/api/logic/updater/UpdateChecker.h b/api/logic/updater/UpdateChecker.h
new file mode 100644
index 00000000..c7fad10e
--- /dev/null
+++ b/api/logic/updater/UpdateChecker.h
@@ -0,0 +1,121 @@
+/* Copyright 2013-2015 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 "net/NetJob.h"
+#include "GoUpdate.h"
+
+#include <QUrl>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT UpdateChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild);
+ void checkForUpdate(QString updateChannel, bool notifyNoUpdate);
+
+ /*!
+ * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
+ * If this isn't called before checkForUpdate(), it will automatically be called.
+ */
+ void updateChanList(bool notifyNoUpdate);
+
+ /*!
+ * An entry in the channel list.
+ */
+ struct ChannelListEntry
+ {
+ QString id;
+ QString name;
+ QString description;
+ QString url;
+ };
+
+ /*!
+ * Returns a the current channel list.
+ * If the channel list hasn't been loaded, this list will be empty.
+ */
+ QList<ChannelListEntry> getChannelList() const;
+
+ /*!
+ * Returns false if the channel list is empty.
+ */
+ bool hasChannels() const;
+
+signals:
+ //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version.
+ void updateAvailable(GoUpdate::Status status);
+
+ //! Signal emitted when the channel list finishes loading or fails to load.
+ void channelListLoaded();
+
+ void noUpdateFound();
+
+private slots:
+ void updateCheckFinished(bool notifyNoUpdate);
+ void updateCheckFailed();
+
+ void chanListDownloadFinished(bool notifyNoUpdate);
+ void chanListDownloadFailed(QString reason);
+
+private:
+ friend class UpdateCheckerTest;
+
+ NetJobPtr indexJob;
+ NetJobPtr chanListJob;
+
+ QString m_channelListUrl;
+
+ QList<ChannelListEntry> m_channels;
+
+ /*!
+ * True while the system is checking for updates.
+ * If checkForUpdate is called while this is true, it will be ignored.
+ */
+ bool m_updateChecking;
+
+ /*!
+ * True if the channel list has loaded.
+ * If this is false, trying to check for updates will call updateChanList first.
+ */
+ bool m_chanListLoaded;
+
+ /*!
+ * Set to true while the channel list is currently loading.
+ */
+ bool m_chanListLoading;
+
+ /*!
+ * Set to true when checkForUpdate is called while the channel list isn't loaded.
+ * When the channel list finishes loading, if this is true, the update checker will check for updates.
+ */
+ bool m_checkUpdateWaiting;
+
+ /*!
+ * if m_checkUpdateWaiting, this is the last used update channel
+ */
+ QString m_deferredUpdateChannel;
+
+ int m_currentBuild = -1;
+ QString m_currentChannel;
+ QString m_currentRepoUrl;
+
+ QString m_newRepoUrl;
+};
+