import { NextRequest } from "next/server"; import { auth } from "@/lib/auth"; import { downloadMod } from "@/lib/modrinth"; import type { ModSide } from "@/lib/modrinth"; import { createSnapshot, restoreSnapshot, listSnapshots } from "@/lib/snapshots"; import { waitForServerAdaptive, rebuildModpack, addModMetadata } from "@/lib/mods"; import { exec } from "child_process"; export const dynamic = "force-dynamic"; type ModToInstall = { projectId: string; versionId: string; filename: string; url: string; title: string; side: ModSide; }; function restartServer(): Promise { return new Promise((resolve, reject) => { exec("sudo systemctl restart minecraft.service", (err) => { if (err) reject(err); else resolve(); }); }); } 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 { mods, snapshotName } = (await req.json()) as { mods: ModToInstall[]; snapshotName: string; }; if (!Array.isArray(mods) || mods.length === 0) { return new Response(JSON.stringify({ error: "No mods to install" }), { status: 400, 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 {} } // Run the pipeline in the background, streaming events as we go (async () => { const name = snapshotName || `before-install-${Date.now()}`; try { // 1. Create snapshot await send("step", { id: "snapshot", status: "active", message: "Creating safety snapshot..." }); try { createSnapshot(name); } catch (e) { await send("step", { id: "snapshot", status: "error", message: (e as Error).message }); await send("done", { success: false, message: `Failed to create snapshot: ${(e as Error).message}` }); return; } await send("step", { id: "snapshot", status: "done", message: "Snapshot created" }); // 2. Download mods await send("step", { id: "download", status: "active", message: `Downloading ${mods.length} mod(s)...` }); try { for (const mod of mods) { await downloadMod(mod.url, mod.filename, mod.side); addModMetadata(mod.filename, { projectId: mod.projectId, side: mod.side }); } } 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" }); // 3. Check if server-side mods need a restart const hasServerMods = mods.some((m) => m.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: `Installed ${mods.length} client-only mod(s). No server restart needed.`, installed: mods.map((m) => m.filename), }); return; } // 4. Restart server 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: `Server restart failed: ${(e as Error).message}`, rolledBack: true }); return; } await send("step", { id: "restart", status: "done", message: "Restart command sent" }); // 5. Wait for server with progress reporting await send("step", { id: "health", status: "active", message: "Waiting for server..." }); const online = await waitForServerAdaptive(async (progress) => { await send("step", { id: "health", status: "active", message: progress.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: `Installed ${mods.length} mod(s). Server is online.`, installed: mods.map((m) => m.filename), }); } 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 (progress) => { await send("step", { id: "rollback", status: "active", message: `Rollback: ${progress.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 to start after installing ${mods.length} mod(s). Rolled back to snapshot.`, 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", }, }); }