summaryrefslogtreecommitdiff
path: root/meshmc/launcher/java/download/JavaDownloadTask.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/java/download/JavaDownloadTask.cpp')
-rw-r--r--meshmc/launcher/java/download/JavaDownloadTask.cpp344
1 files changed, 344 insertions, 0 deletions
diff --git a/meshmc/launcher/java/download/JavaDownloadTask.cpp b/meshmc/launcher/java/download/JavaDownloadTask.cpp
new file mode 100644
index 0000000000..eb73c4d6d5
--- /dev/null
+++ b/meshmc/launcher/java/download/JavaDownloadTask.cpp
@@ -0,0 +1,344 @@
+/* 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 "JavaDownloadTask.h"
+
+#include <QDir>
+#include <QProcess>
+#include <QDebug>
+#include <QDirIterator>
+#include <QFileInfo>
+
+#include "Application.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "net/Download.h"
+#include "net/ChecksumValidator.h"
+#include "MMCZip.h"
+
+JavaDownloadTask::JavaDownloadTask(const JavaDownload::RuntimeEntry& runtime,
+ const QString& targetDir, QObject* parent)
+ : Task(parent), m_runtime(runtime), m_targetDir(targetDir)
+{
+}
+
+void JavaDownloadTask::executeTask()
+{
+ if (m_runtime.downloadType == "manifest") {
+ downloadManifest();
+ } else {
+ downloadArchive();
+ }
+}
+
+void JavaDownloadTask::downloadArchive()
+{
+ setStatus(tr("Downloading %1...").arg(m_runtime.name));
+
+ // Determine archive extension and path
+ QUrl url(m_runtime.url);
+ QString filename = url.fileName();
+ m_archivePath = FS::PathCombine(m_targetDir, filename);
+
+ // Create target directory
+ if (!FS::ensureFolderPathExists(m_targetDir)) {
+ emitFailed(
+ tr("Failed to create target directory: %1").arg(m_targetDir));
+ return;
+ }
+
+ m_downloadJob = new NetJob(tr("Java download"), APPLICATION->network());
+
+ auto dl = Net::Download::makeFile(m_runtime.url, m_archivePath);
+
+ // Add checksum validation
+ if (!m_runtime.checksumHash.isEmpty()) {
+ QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256;
+ if (m_runtime.checksumType == "sha1")
+ algo = QCryptographicHash::Sha1;
+ auto validator = new Net::ChecksumValidator(
+ algo, QByteArray::fromHex(m_runtime.checksumHash.toLatin1()));
+ dl->addValidator(validator);
+ }
+
+ m_downloadJob->addNetAction(dl);
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::downloadFinished);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+ connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::downloadFinished()
+{
+ m_downloadJob.reset();
+ extractArchive();
+}
+
+void JavaDownloadTask::downloadFailed(QString reason)
+{
+ m_downloadJob.reset();
+ // Clean up partial download
+ if (!m_archivePath.isEmpty() && !QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove partial download:" << m_archivePath;
+ emitFailed(tr("Failed to download Java: %1").arg(reason));
+}
+
+void JavaDownloadTask::extractArchive()
+{
+ setStatus(tr("Extracting %1...").arg(m_runtime.name));
+
+ bool success = false;
+
+ if (m_archivePath.endsWith(".zip")) {
+ // Use QuaZip for zip files
+ auto result = MMCZip::extractDir(m_archivePath, m_targetDir);
+ success = result.has_value();
+ } else if (m_archivePath.endsWith(".tar.gz") ||
+ m_archivePath.endsWith(".tgz")) {
+ // Use system tar for tar.gz files
+ QProcess tarProcess;
+ tarProcess.setWorkingDirectory(m_targetDir);
+ tarProcess.start("tar", QStringList() << "xzf" << m_archivePath);
+ tarProcess.waitForFinished(300000); // 5 minute timeout
+ success = (tarProcess.exitCode() == 0 &&
+ tarProcess.exitStatus() == QProcess::NormalExit);
+ if (!success) {
+ qWarning() << "tar extraction failed:"
+ << tarProcess.readAllStandardError();
+ }
+ } else {
+ if (!QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove archive:" << m_archivePath;
+ emitFailed(tr("Unsupported archive format: %1").arg(m_archivePath));
+ return;
+ }
+
+ // Clean up archive file
+ if (!QFile::remove(m_archivePath))
+ qWarning() << "Failed to remove archive:" << m_archivePath;
+
+ if (!success) {
+ emitFailed(tr("Failed to extract Java archive."));
+ return;
+ }
+
+ // Fix permissions on extracted files
+ QDirIterator it(m_targetDir, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ if (file.isDir()) {
+ permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser |
+ QFileDevice::ExeUser;
+ } else {
+ permissions |= QFileDevice::ReadUser | QFileDevice::WriteUser;
+ }
+ QFile::setPermissions(filepath, permissions);
+ }
+
+ // Find the java binary
+ m_installedJavaPath = findJavaBinary(m_targetDir);
+ if (m_installedJavaPath.isEmpty()) {
+ emitFailed(tr("Could not find java binary in extracted archive."));
+ return;
+ }
+
+ // Make java binary executable
+ auto perms = QFile::permissions(m_installedJavaPath) |
+ QFileDevice::ExeUser | QFileDevice::ExeGroup |
+ QFileDevice::ExeOther;
+ QFile::setPermissions(m_installedJavaPath, perms);
+
+ qDebug() << "Java installed successfully at:" << m_installedJavaPath;
+ emitSucceeded();
+}
+
+void JavaDownloadTask::downloadManifest()
+{
+ setStatus(tr("Downloading manifest for %1...").arg(m_runtime.name));
+
+ if (!FS::ensureFolderPathExists(m_targetDir)) {
+ emitFailed(
+ tr("Failed to create target directory: %1").arg(m_targetDir));
+ return;
+ }
+
+ m_downloadJob =
+ new NetJob(tr("Java manifest download"), APPLICATION->network());
+ auto dl =
+ Net::Download::makeByteArray(QUrl(m_runtime.url), &m_manifestData);
+
+ if (m_runtime.checksumType == "sha1" && !m_runtime.checksumHash.isEmpty()) {
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Sha1,
+ QByteArray::fromHex(m_runtime.checksumHash.toLatin1())));
+ }
+
+ m_downloadJob->addNetAction(dl);
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::manifestDownloaded);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::manifestDownloaded()
+{
+ m_downloadJob.reset();
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_manifestData);
+ } catch (const Exception& e) {
+ m_manifestData.clear();
+ emitFailed(tr("Failed to parse Java manifest: %1").arg(e.cause()));
+ return;
+ }
+ m_manifestData.clear();
+
+ if (!doc.isObject()) {
+ emitFailed(tr("Failed to parse Java manifest."));
+ return;
+ }
+
+ auto files = doc.object()["files"].toObject();
+ m_executableFiles.clear();
+ m_linkEntries.clear();
+
+ // Create directories first
+ for (auto it = files.begin(); it != files.end(); ++it) {
+ auto entry = it.value().toObject();
+ if (entry["type"].toString() == "directory") {
+ QDir().mkpath(FS::PathCombine(m_targetDir, it.key()));
+ }
+ }
+
+ // Queue file downloads
+ setStatus(tr("Downloading %1 files...").arg(m_runtime.name));
+ m_downloadJob =
+ new NetJob(tr("Java runtime files"), APPLICATION->network());
+
+ for (auto it = files.begin(); it != files.end(); ++it) {
+ auto entry = it.value().toObject();
+
+ if (entry["type"].toString() == "file") {
+ auto downloads = entry["downloads"].toObject();
+ auto raw = downloads["raw"].toObject();
+
+ QString url = raw["url"].toString();
+ QString sha1 = raw["sha1"].toString();
+ QString filePath = FS::PathCombine(m_targetDir, it.key());
+
+ // Ensure parent directory exists
+ QFileInfo fi(filePath);
+ QDir().mkpath(fi.absolutePath());
+
+ auto dl = Net::Download::makeFile(QUrl(url), filePath);
+ if (!sha1.isEmpty()) {
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Sha1,
+ QByteArray::fromHex(sha1.toLatin1())));
+ }
+ m_downloadJob->addNetAction(dl);
+
+ if (entry["executable"].toBool()) {
+ m_executableFiles.append(filePath);
+ }
+ } else if (entry["type"].toString() == "link") {
+ m_linkEntries.append({it.key(), entry["target"].toString()});
+ }
+ }
+
+ connect(m_downloadJob.get(), &NetJob::succeeded, this,
+ &JavaDownloadTask::manifestFilesDownloaded);
+ connect(m_downloadJob.get(), &NetJob::failed, this,
+ &JavaDownloadTask::downloadFailed);
+ connect(m_downloadJob.get(), &NetJob::progress, this, &Task::setProgress);
+
+ m_downloadJob->start();
+}
+
+void JavaDownloadTask::manifestFilesDownloaded()
+{
+ m_downloadJob.reset();
+
+ // Create symlinks
+ for (const auto& link : m_linkEntries) {
+ QString linkPath = FS::PathCombine(m_targetDir, link.first);
+ QFileInfo fi(linkPath);
+ QDir().mkpath(fi.absolutePath());
+ QFile::link(link.second, linkPath);
+ }
+
+ // Set executable permissions
+ for (const auto& path : m_executableFiles) {
+ QFile::setPermissions(
+ path, QFile::permissions(path) | QFileDevice::ExeUser |
+ QFileDevice::ExeGroup | QFileDevice::ExeOther);
+ }
+
+ // Find java binary
+ m_installedJavaPath = findJavaBinary(m_targetDir);
+ if (m_installedJavaPath.isEmpty()) {
+ emitFailed(tr("Could not find java binary in downloaded runtime."));
+ return;
+ }
+
+ qDebug() << "Java installed successfully at:" << m_installedJavaPath;
+ emitSucceeded();
+}
+
+QString JavaDownloadTask::findJavaBinary(const QString& dir) const
+{
+#if defined(Q_OS_WIN)
+ QString binaryName = "javaw.exe";
+#else
+ QString binaryName = "java";
+#endif
+
+ // Search for java binary in bin/ subdirectories
+ QDirIterator it(dir, QStringList() << binaryName, QDir::Files,
+ QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ QString path = it.filePath();
+ if (path.contains("/bin/")) {
+ return path;
+ }
+ }
+
+ // Fallback: any match
+ QDirIterator it2(dir, QStringList() << binaryName, QDir::Files,
+ QDirIterator::Subdirectories);
+ if (it2.hasNext()) {
+ it2.next();
+ return it2.filePath();
+ }
+
+ return QString();
+}