Files
AeroMc-Launcher/src/javadl.js
2026-08-02 17:01:18 +02:00

213 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/*
* javadl.js Java bei Bedarf selbst beschaffen
* ----------------------------------------------
* services.js sucht nur nach vorhandenen Java-Installationen. Wer keine hat,
* kam bisher nicht ins Spiel. Dieses Modul lädt die passende Laufzeit von
* Adoptium (Eclipse Temurin) und legt sie unter <userData>/java ab.
*
* Ablauf: Release suchen -> ZIP laden -> entpacken -> java.exe zurückgeben.
*/
const fs = require('fs');
const path = require('path');
const unzipper = require('unzipper');
const API = 'https://api.adoptium.net/v3';
const UA = 'AeroMC-Launcher';
let javaRoot = null;
// Ablageort festlegen (üblicherweise <userData>/java)
function setJavaDir(dir) {
javaRoot = dir;
return javaRoot;
}
function getJavaDir() { return javaRoot; }
/*
* Sucht das neueste Release für eine Major-Version.
* Bevorzugt die JRE (rund halb so groß), fällt sonst auf das JDK zurück.
*/
async function findRelease(major) {
for (const imageType of ['jre', 'jdk']) {
const url = new URL(`${API}/assets/latest/${major}/hotspot`);
url.searchParams.set('architecture', 'x64');
url.searchParams.set('image_type', imageType);
url.searchParams.set('os', 'windows');
url.searchParams.set('vendor', 'eclipse');
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
if (!res.ok) continue;
const liste = await res.json();
const treffer = (Array.isArray(liste) ? liste : [])
.find((a) => a && a.binary && a.binary.package && a.binary.package.link);
if (!treffer) continue;
return {
major,
imageType,
releaseName: treffer.release_name || `temurin-${major}`,
url: treffer.binary.package.link,
size: Number(treffer.binary.package.size) || 0,
sha256: treffer.binary.package.checksum || '',
};
}
return null;
}
// Ordnername, unter dem eine Laufzeit abgelegt wird
function zielOrdner(release) {
const safe = String(release.releaseName).replace(/[^\w.+-]/g, '_');
return path.join(javaRoot, `${safe}-${release.imageType}`);
}
// java.exe innerhalb einer entpackten Laufzeit finden (bin/ liegt eine Ebene tiefer)
function findeJavaExe(wurzel) {
const direkt = path.join(wurzel, 'bin', 'java.exe');
if (fs.existsSync(direkt)) return direkt;
let eintraege = [];
try { eintraege = fs.readdirSync(wurzel, { withFileTypes: true }); } catch { return null; }
for (const e of eintraege) {
if (!e.isDirectory()) continue;
const p = path.join(wurzel, e.name, 'bin', 'java.exe');
if (fs.existsSync(p)) return p;
}
return null;
}
/*
* Bereits heruntergeladene Laufzeiten auflisten.
* -> [{ path, dir, major }]
*/
function listManaged() {
if (!javaRoot || !fs.existsSync(javaRoot)) return [];
const out = [];
for (const name of fs.readdirSync(javaRoot)) {
const dir = path.join(javaRoot, name);
let exe = null;
try { if (fs.statSync(dir).isDirectory()) exe = findeJavaExe(dir); } catch { continue; }
if (!exe) continue;
const m = name.match(/(?:jdk-?|temurin-?)(\d+)/i);
out.push({ path: exe, dir, major: m ? Number(m[1]) : null });
}
return out;
}
// schon eine passende Laufzeit da?
function findManaged(major) {
return listManaged().find((j) => j.major === major) || null;
}
async function ladeDatei(url, ziel, onProgress) {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Java-Download fehlgeschlagen: HTTP ' + res.status);
const gesamt = Number(res.headers.get('content-length')) || 0;
fs.mkdirSync(path.dirname(ziel), { recursive: true });
const out = fs.createWriteStream(ziel);
let geladen = 0;
let letzte = 0;
try {
for await (const stueck of res.body) {
const buf = Buffer.from(stueck);
geladen += buf.length;
if (!out.write(buf)) await new Promise((resolve, reject) => {
const onDrain = () => { out.off('error', onError); resolve(); };
const onError = (e) => { out.off('drain', onDrain); reject(e); };
out.once('drain', onDrain);
out.once('error', onError);
});
if (onProgress && Date.now() - letzte >= 250) {
letzte = Date.now();
onProgress({ phase: 'java-download', geladen, gesamt, percent: gesamt ? Math.round((geladen / gesamt) * 100) : 0 });
}
}
await new Promise((resolve, reject) => {
out.once('error', reject);
out.end((err) => (err ? reject(err) : resolve()));
});
} catch (err) {
out.destroy();
fs.rmSync(ziel, { force: true });
throw err;
}
return geladen;
}
// Pfade aus dem Archiv gegen Ausbrüche absichern
function saubererPfad(eintrag) {
const p = String(eintrag || '').replace(/\\/g, '/');
if (!p || p.startsWith('/') || /^[a-z]:/i.test(p)) return null;
if (p.split('/').includes('..')) return null;
return p;
}
async function entpacken(zipPfad, zielDir, onProgress) {
const archiv = await unzipper.Open.file(zipPfad);
const dateien = (archiv.files || []).filter((e) => e.type !== 'Directory');
fs.mkdirSync(zielDir, { recursive: true });
let fertig = 0;
let letzte = 0;
for (const eintrag of archiv.files || []) {
const rel = saubererPfad(eintrag.path);
if (!rel) continue;
const ziel = path.join(zielDir, rel);
if (eintrag.type === 'Directory') { fs.mkdirSync(ziel, { recursive: true }); continue; }
fs.mkdirSync(path.dirname(ziel), { recursive: true });
await new Promise((resolve, reject) => {
eintrag.stream()
.pipe(fs.createWriteStream(ziel))
.on('finish', resolve)
.on('error', reject);
});
fertig++;
if (onProgress && Date.now() - letzte >= 250) {
letzte = Date.now();
onProgress({ phase: 'java-entpacken', fertig, gesamt: dateien.length,
percent: dateien.length ? Math.round((fertig / dateien.length) * 100) : 0 });
}
}
return fertig;
}
/*
* Stellt sicher, dass eine Java-Laufzeit der gewünschten Major-Version da ist.
* -> { path, major, geladen } oder null
*/
async function ensureJava(major, onProgress) {
if (!javaRoot) throw new Error('Java-Ablageort ist nicht gesetzt.');
const vorhanden = findManaged(major);
if (vorhanden) return { path: vorhanden.path, major, geladen: false };
const release = await findRelease(major);
if (!release) throw new Error(`Für Java ${major} wird bei Adoptium keine Windows-Fassung angeboten.`);
const ziel = zielOrdner(release);
const tmpZip = path.join(javaRoot, `.download-${Date.now()}.zip`);
fs.mkdirSync(javaRoot, { recursive: true });
try {
if (onProgress) onProgress({ phase: 'java-download', geladen: 0, gesamt: release.size, percent: 0 });
await ladeDatei(release.url, tmpZip, onProgress);
// in einen Nebenordner entpacken und erst am Ende umbenennen -> kein
// halbfertiges Java, falls unterwegs etwas schiefgeht
const tmpDir = ziel + '.teil';
fs.rmSync(tmpDir, { recursive: true, force: true });
await entpacken(tmpZip, tmpDir, onProgress);
fs.rmSync(ziel, { recursive: true, force: true });
fs.renameSync(tmpDir, ziel);
} finally {
fs.rmSync(tmpZip, { force: true });
}
const exe = findeJavaExe(ziel);
if (!exe) throw new Error('Im heruntergeladenen Archiv wurde keine java.exe gefunden.');
return { path: exe, major, geladen: true, dir: ziel };
}
module.exports = { setJavaDir, getJavaDir, ensureJava, findRelease, listManaged, findManaged, findeJavaExe };