summaryrefslogtreecommitdiff
path: root/meshmc/updater/Installer.cpp
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/updater/Installer.cpp
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/updater/Installer.cpp')
-rw-r--r--meshmc/updater/Installer.cpp373
1 files changed, 373 insertions, 0 deletions
diff --git a/meshmc/updater/Installer.cpp b/meshmc/updater/Installer.cpp
new file mode 100644
index 0000000000..058b261b9a
--- /dev/null
+++ b/meshmc/updater/Installer.cpp
@@ -0,0 +1,373 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "Installer.h"
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QProcess>
+#include <QSaveFile>
+#include <QTemporaryDir>
+#include <QThread>
+
+// LibArchive is used for zip and tar.gz extraction.
+#include <archive.h>
+#include <archive_entry.h>
+
+// ---------------------------------------------------------------------------
+// Construction
+// ---------------------------------------------------------------------------
+
+Installer::Installer(QObject* parent) : QObject(parent)
+{
+ m_nam = new QNetworkAccessManager(this);
+}
+
+// ---------------------------------------------------------------------------
+// Public
+// ---------------------------------------------------------------------------
+
+void Installer::start()
+{
+ emit progressMessage(tr("Downloading update from %1 …").arg(m_url));
+ qDebug() << "Installer: downloading" << m_url;
+
+ // Determine a temp file name from the URL.
+ const QFileInfo urlInfo(QUrl(m_url).path());
+ m_tempFile = QDir::tempPath() + "/meshmc-update-" + urlInfo.fileName();
+
+ QNetworkRequest req{QUrl{m_url}};
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
+ QNetworkRequest::NoLessSafeRedirectPolicy);
+ QNetworkReply* reply = m_nam->get(req);
+ connect(reply, &QNetworkReply::downloadProgress, this,
+ &Installer::onDownloadProgress);
+ connect(reply, &QNetworkReply::finished, this,
+ &Installer::onDownloadFinished);
+}
+
+// ---------------------------------------------------------------------------
+// Private slots
+// ---------------------------------------------------------------------------
+
+void Installer::onDownloadProgress(qint64 received, qint64 total)
+{
+ if (total > 0) {
+ const int pct = static_cast<int>((received * 100) / total);
+ emit progressMessage(tr("Downloading … %1%").arg(pct));
+ }
+}
+
+void Installer::onDownloadFinished()
+{
+ auto* reply = qobject_cast<QNetworkReply*>(sender());
+ if (!reply)
+ return;
+ reply->deleteLater();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ emit finished(false,
+ tr("Download failed: %1").arg(reply->errorString()));
+ return;
+ }
+
+ // Write to temp file.
+ QSaveFile file(m_tempFile);
+ if (!file.open(QIODevice::WriteOnly)) {
+ emit finished(false,
+ tr("Cannot write temp file: %1").arg(file.errorString()));
+ return;
+ }
+ file.write(reply->readAll());
+ if (!file.commit()) {
+ emit finished(
+ false, tr("Cannot commit temp file: %1").arg(file.errorString()));
+ return;
+ }
+
+ emit progressMessage(tr("Download complete. Installing …"));
+ qDebug() << "Installer: saved to" << m_tempFile;
+
+ const bool ok = installArchive(m_tempFile);
+
+ if (!ok) {
+ QFile::remove(m_tempFile);
+ return; // finished() already emitted inside installArchive / installExe
+ }
+
+ QFile::remove(m_tempFile);
+ relaunch();
+ emit finished(true, QString());
+}
+
+// ---------------------------------------------------------------------------
+// Private helpers
+// ---------------------------------------------------------------------------
+
+bool Installer::installArchive(const QString& filePath)
+{
+ const QString lower = filePath.toLower();
+
+ if (lower.endsWith(".exe")) {
+ return installExe(filePath);
+ }
+
+ if (lower.endsWith(".zip") || lower.endsWith(".tar.gz") ||
+ lower.endsWith(".tgz")) {
+ // Extract into a temp directory, then copy over root.
+ QTemporaryDir tempDir;
+ if (!tempDir.isValid()) {
+ emit finished(
+ false, tr("Cannot create temporary directory for extraction."));
+ return false;
+ }
+
+ emit progressMessage(tr("Extracting archive …"));
+
+ bool extractOk = false;
+ if (lower.endsWith(".zip")) {
+ extractOk = extractZip(filePath, tempDir.path());
+ } else {
+ extractOk = extractTarGz(filePath, tempDir.path());
+ }
+
+ if (!extractOk) {
+ return false; // finished() already emitted
+ }
+
+ // Copy all extracted files into the root, skipping the updater binary
+ // itself.
+ emit progressMessage(tr("Installing files …"));
+
+ const QString updaterName =
+#ifdef Q_OS_WIN
+ "meshmc-updater.exe";
+#else
+ "meshmc-updater";
+#endif
+
+ QDir src(tempDir.path());
+ // If the archive has a single top-level directory, descend into it.
+ const QStringList topEntries =
+ src.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ if (topEntries.size() == 1 &&
+ src.entryList(QDir::Files | QDir::NoDotAndDotDot).isEmpty()) {
+ src.cd(topEntries.first());
+ }
+
+ const QDir dest(m_root);
+ QDirIterator it(src.absolutePath(), QDir::Files | QDir::NoDotAndDotDot,
+ QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ const QFileInfo& fi = it.fileInfo();
+
+ const QString relPath = src.relativeFilePath(fi.absoluteFilePath());
+ // Don't replace ourselves while running.
+ if (relPath == updaterName)
+ continue;
+
+ const QString destPath = dest.filePath(relPath);
+ QFileInfo destInfo(destPath);
+ if (!QDir().mkpath(destInfo.absolutePath())) {
+ emit finished(false, tr("Cannot create directory: %1")
+ .arg(destInfo.absolutePath()));
+ return false;
+ }
+ if (destInfo.exists())
+ QFile::remove(destPath);
+
+ if (!QFile::copy(fi.absoluteFilePath(), destPath)) {
+ emit finished(
+ false, tr("Cannot copy %1 to %2.").arg(relPath, destPath));
+ return false;
+ }
+
+ // Preserve executable bit on Unix.
+#ifndef Q_OS_WIN
+ if (fi.isExecutable()) {
+ QFile(destPath).setPermissions(
+ QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
+ QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther |
+ QFile::ExeOther);
+ }
+#endif
+ }
+
+ return true;
+ }
+
+ emit finished(
+ false,
+ tr("Unknown archive format: %1").arg(QFileInfo(filePath).suffix()));
+ return false;
+}
+
+bool Installer::installExe(const QString& filePath)
+{
+ // For Windows NSIS / Inno Setup installers, just run the installer
+ // directly. It handles file replacement and relaunching.
+#ifdef Q_OS_WIN
+ emit progressMessage(tr("Launching installer …"));
+ const bool ok = QProcess::startDetached(filePath, QStringList());
+ if (!ok) {
+ emit finished(false,
+ tr("Failed to launch installer: %1").arg(filePath));
+ return false;
+ }
+ // The installer will take care of everything; exit after launching.
+ QCoreApplication::quit();
+ return true;
+#else
+ Q_UNUSED(filePath)
+ emit finished(false, tr(".exe installers are only supported on Windows."));
+ return false;
+#endif
+}
+
+bool Installer::extractZip(const QString& zipPath, const QString& destDir)
+{
+ archive* a = archive_read_new();
+ archive_read_support_format_zip(a);
+ archive_read_support_filter_all(a);
+
+ if (archive_read_open_filename(a, zipPath.toLocal8Bit().constData(),
+ 10240) != ARCHIVE_OK) {
+ const QString err = QString::fromLocal8Bit(archive_error_string(a));
+ archive_read_free(a);
+ emit finished(false, tr("Cannot open zip archive: %1").arg(err));
+ return false;
+ }
+
+ archive* ext = archive_write_disk_new();
+ archive_write_disk_set_options(
+ ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL |
+ ARCHIVE_EXTRACT_FFLAGS);
+ archive_write_disk_set_standard_lookup(ext);
+
+ archive_entry* entry = nullptr;
+ bool ok = true;
+ while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+ const QString entryPath =
+ destDir + "/" +
+ QString::fromLocal8Bit(archive_entry_pathname(entry));
+ archive_entry_set_pathname(entry, entryPath.toLocal8Bit().constData());
+
+ if (archive_write_header(ext, entry) != ARCHIVE_OK) {
+ ok = false;
+ break;
+ }
+ if (archive_entry_size(entry) > 0) {
+ const void* buf;
+ size_t size;
+ la_int64_t offset;
+ while (archive_read_data_block(a, &buf, &size, &offset) ==
+ ARCHIVE_OK) {
+ archive_write_data_block(ext, buf, size, offset);
+ }
+ }
+ archive_write_finish_entry(ext);
+ }
+
+ archive_read_close(a);
+ archive_read_free(a);
+ archive_write_close(ext);
+ archive_write_free(ext);
+
+ if (!ok) {
+ emit finished(false, tr("Failed to extract zip archive."));
+ return false;
+ }
+ return true;
+}
+
+bool Installer::extractTarGz(const QString& tarPath, const QString& destDir)
+{
+ archive* a = archive_read_new();
+ archive_read_support_format_tar(a);
+ archive_read_support_format_gnutar(a);
+ archive_read_support_filter_gzip(a);
+ archive_read_support_filter_bzip2(a);
+ archive_read_support_filter_xz(a);
+
+ if (archive_read_open_filename(a, tarPath.toLocal8Bit().constData(),
+ 10240) != ARCHIVE_OK) {
+ const QString err = QString::fromLocal8Bit(archive_error_string(a));
+ archive_read_free(a);
+ emit finished(false, tr("Cannot open tar archive: %1").arg(err));
+ return false;
+ }
+
+ archive* ext = archive_write_disk_new();
+ archive_write_disk_set_options(
+ ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL |
+ ARCHIVE_EXTRACT_FFLAGS);
+ archive_write_disk_set_standard_lookup(ext);
+
+ archive_entry* entry = nullptr;
+ bool ok = true;
+ while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+ const QString entryPath =
+ destDir + "/" +
+ QString::fromLocal8Bit(archive_entry_pathname(entry));
+ archive_entry_set_pathname(entry, entryPath.toLocal8Bit().constData());
+
+ if (archive_write_header(ext, entry) != ARCHIVE_OK) {
+ ok = false;
+ break;
+ }
+ if (archive_entry_size(entry) > 0) {
+ const void* buf;
+ size_t size;
+ la_int64_t offset;
+ while (archive_read_data_block(a, &buf, &size, &offset) ==
+ ARCHIVE_OK) {
+ archive_write_data_block(ext, buf, size, offset);
+ }
+ }
+ archive_write_finish_entry(ext);
+ }
+
+ archive_read_close(a);
+ archive_read_free(a);
+ archive_write_close(ext);
+ archive_write_free(ext);
+
+ if (!ok) {
+ emit finished(false, tr("Failed to extract tar archive."));
+ return false;
+ }
+ return true;
+}
+
+void Installer::relaunch()
+{
+ if (m_exec.isEmpty())
+ return;
+
+ qDebug() << "Installer: relaunching" << m_exec;
+ QProcess::startDetached(m_exec, QStringList());
+}