'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 /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 /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 };