213 lines
7.3 KiB
JavaScript
213 lines
7.3 KiB
JavaScript
'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 };
|