diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/updater/Installer.cpp | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-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.cpp | 373 |
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()); +} |
