import { NextRequest } from "next/server"; import { existsSync, readFileSync, unlinkSync } from "fs"; import { join } from "path"; import { exec } from "child_process"; import { auth } from "@/lib/auth"; import { downloadMod, getLatestVersion } from "@/lib/modrinth"; import type { ModSide } from "@/lib/modrinth"; import { createSnapshot, restoreSnapshot, listSnapshots } from "@/lib/snapshots"; import { waitForServerAdaptive, rebuildModpack, addModMetadata, removeModMetadata, invalidateModsCache, } from "@/lib/mods"; import { invalidate as invalidateCache } from "@/lib/cache"; import { MODS_DIR, CLIENT_MODS_DIR, MOD_METADATA_FILE } from "@/lib/constants"; export const dynamic = "force-dynamic"; type ModMetadataEntry = { projectId: string; side: ModSide }; function restartServer(): Promise { return new Promise((resolve, reject) => { exec("sudo systemctl restart minecraft.service", (err) => { if (err) reject(err); else resolve(); }); }); } function locateMod(filename: string): { dir: string; side: ModSide } | null { if (existsSync(join(MODS_DIR, filename))) { return { dir: MODS_DIR, side: "both" }; } if (existsSync(join(CLIENT_MODS_DIR, filename))) { return { dir: CLIENT_MODS_DIR, side: "client" }; } return null; } export async function POST(req: NextRequest) { const session = await auth(); if (!session) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 403, headers: { "Content-Type": "application/json" }, }); } const { filenames } = (await req.json()) as { filenames: string[] }; if (!Array.isArray(filenames) || filenames.length === 0) { return new Response(JSON.stringify({ error: "No mods to update" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Validate filenames for (const f of filenames) { if (!f || !f.endsWith(".jar") || f.includes("/") || f.includes("\\")) { return new Response(JSON.stringify({ error: `Invalid filename: ${f}` }), { status: 400, headers: { "Content-Type": "application/json" }, }); } } let metadata: Record; try { metadata = JSON.parse(readFileSync(MOD_METADATA_FILE, "utf8")); } catch { return new Response( JSON.stringify({ error: "Mod metadata unavailable" }), { status: 500, headers: { "Content-Type": "application/json" } } ); } const stream = new TransformStream(); const writer = stream.writable.getWriter(); const encoder = new TextEncoder(); async function send(event: string, data: object) { try { await writer.write( encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) ); } catch {} } (async () => { const snapName = `before-update-${Date.now()}`; try { // 1. Resolve targets await send("step", { id: "resolve", status: "active", message: `Resolving latest versions for ${filenames.length} mod(s)...`, }); type Target = { oldFilename: string; dir: string; side: ModSide; projectId: string; newFilename: string; url: string; }; const targets: Target[] = []; for (const f of filenames) { const entry = metadata[f]; if (!entry?.projectId) { await send("step", { id: "resolve", status: "error", message: `${f}: unknown origin (not installed via dashboard)`, }); await send("done", { success: false, message: `Cannot update ${f} — no Modrinth project linked.`, }); return; } const loc = locateMod(f); if (!loc) { await send("step", { id: "resolve", status: "error", message: `${f}: file missing on disk`, }); await send("done", { success: false, message: `Mod file missing: ${f}` }); return; } const side: ModSide = entry.side || loc.side; const latest = await getLatestVersion(entry.projectId); if (!latest || !latest.files?.[0]) { await send("step", { id: "resolve", status: "error", message: `${f}: no compatible Modrinth version`, }); await send("done", { success: false, message: `No update available for ${f}.`, }); return; } const file = latest.files[0]; if (file.filename === f) { // Already up to date continue; } targets.push({ oldFilename: f, dir: side === "client" ? CLIENT_MODS_DIR : MODS_DIR, side, projectId: entry.projectId, newFilename: file.filename, url: file.url, }); } if (targets.length === 0) { await send("step", { id: "resolve", status: "done", message: "All selected mods already current", }); await send("done", { success: true, message: "No updates needed — everything is up to date.", installed: [], }); return; } await send("step", { id: "resolve", status: "done", message: `${targets.length} update(s) to apply`, }); // 2. Snapshot await send("step", { id: "snapshot", status: "active", message: "Creating safety snapshot...", }); try { createSnapshot(snapName); } catch (e) { await send("step", { id: "snapshot", status: "error", message: (e as Error).message, }); await send("done", { success: false, message: `Snapshot failed: ${(e as Error).message}`, }); return; } await send("step", { id: "snapshot", status: "done", message: "Snapshot created" }); // 3. Download + swap await send("step", { id: "download", status: "active", message: `Downloading ${targets.length} mod(s)...`, }); try { for (const t of targets) { await downloadMod(t.url, t.newFilename, t.side); const oldPath = join(t.dir, t.oldFilename); if (existsSync(oldPath) && t.oldFilename !== t.newFilename) { unlinkSync(oldPath); } removeModMetadata(t.oldFilename); addModMetadata(t.newFilename, { projectId: t.projectId, side: t.side }); } invalidateModsCache(); invalidateCache("mods:updates"); } catch (e) { await send("step", { id: "download", status: "error", message: (e as Error).message, }); await send("step", { id: "rollback", status: "active", message: "Rolling back...", }); try { const snaps = listSnapshots(); if (snaps.length > 0) restoreSnapshot(snaps[0].dirName); } catch {} await send("step", { id: "rollback", status: "done", message: "Rolled back to snapshot", }); await send("done", { success: false, message: `Download failed: ${(e as Error).message}`, rolledBack: true, }); return; } await send("step", { id: "download", status: "done", message: "All mods downloaded", }); // 4. Restart if any server-side updates const hasServerMods = targets.some((t) => t.side !== "client"); if (!hasServerMods) { await send("step", { id: "modpack", status: "active", message: "Rebuilding modpack...", }); try { rebuildModpack(); } catch {} await send("step", { id: "modpack", status: "done", message: "Modpack updated", }); await send("done", { success: true, message: `Updated ${targets.length} client-only mod(s).`, installed: targets.map((t) => t.newFilename), }); return; } await send("step", { id: "restart", status: "active", message: "Restarting server...", }); try { await restartServer(); } catch (e) { await send("step", { id: "restart", status: "error", message: (e as Error).message, }); await send("step", { id: "rollback", status: "active", message: "Rolling back..." }); try { const snaps = listSnapshots(); if (snaps.length > 0) restoreSnapshot(snaps[0].dirName); } catch {} await send("step", { id: "rollback", status: "done", message: "Rolled back to snapshot", }); await send("done", { success: false, message: `Restart failed: ${(e as Error).message}`, rolledBack: true, }); return; } await send("step", { id: "restart", status: "done", message: "Restart command sent", }); // 5. Verify await send("step", { id: "health", status: "active", message: "Waiting for server...", }); const online = await waitForServerAdaptive(async (p) => { await send("step", { id: "health", status: "active", message: p.message }); }); if (online) { await send("step", { id: "health", status: "done", message: "Server is online", }); await send("step", { id: "modpack", status: "active", message: "Rebuilding modpack...", }); try { rebuildModpack(); } catch {} await send("step", { id: "modpack", status: "done", message: "Modpack updated", }); await send("done", { success: true, message: `Updated ${targets.length} mod(s). Server is online.`, installed: targets.map((t) => t.newFilename), }); } else { await send("step", { id: "health", status: "error", message: "Server failed to start", }); await send("step", { id: "rollback", status: "active", message: "Rolling back to snapshot...", }); try { const snaps = listSnapshots(); if (snaps.length > 0) { restoreSnapshot(snaps[0].dirName); await restartServer(); await waitForServerAdaptive(async (p) => { await send("step", { id: "rollback", status: "active", message: `Rollback: ${p.message}`, }); }); try { rebuildModpack(); } catch {} } } catch {} await send("step", { id: "rollback", status: "done", message: "Rolled back and server restarted", }); await send("done", { success: false, message: `Server failed after updating ${targets.length} mod(s). Rolled back.`, rolledBack: true, }); } } catch (e) { await send("done", { success: false, message: (e as Error).message }); } finally { try { writer.close(); } catch {} } })(); return new Response(stream.readable, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "X-Accel-Buffering": "no", }, }); }