import { existsSync, mkdirSync, readdirSync, copyFileSync, rmSync, statSync, writeFileSync, readFileSync, unlinkSync, } from "fs"; import { join } from "path"; import { MODS_DIR, CLIENT_MODS_DIR, MOD_METADATA_FILE } from "./constants"; function dirSize(dir: string): number { let total = 0; try { for (const entry of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, entry.name); if (entry.isDirectory()) total += dirSize(p); else if (entry.isFile()) { try { total += statSync(p).size; } catch {} } } } catch {} return total; } const SNAPSHOTS_DIR = "/home/minecraft/server/snapshots"; const MAX_SNAPSHOTS = 10; export type SnapshotMeta = { name: string; createdAt: string; modCount: number; mods: string[]; sizeBytes?: number; }; function ensureDir(dir: string) { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); } export function createSnapshot(name: string): SnapshotMeta { ensureDir(SNAPSHOTS_DIR); // Sanitize name const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60); const dirName = `${safeName}_${Date.now()}`; const snapDir = join(SNAPSHOTS_DIR, dirName); mkdirSync(snapDir); // Back up server mods const mods = readdirSync(MODS_DIR).filter((f) => f.endsWith(".jar")); for (const file of mods) { copyFileSync(join(MODS_DIR, file), join(snapDir, file)); } // Back up client mods if (existsSync(CLIENT_MODS_DIR)) { const clientMods = readdirSync(CLIENT_MODS_DIR).filter((f) => f.endsWith(".jar") ); if (clientMods.length > 0) { const clientSnapDir = join(snapDir, "client-mods"); mkdirSync(clientSnapDir); for (const file of clientMods) { copyFileSync(join(CLIENT_MODS_DIR, file), join(clientSnapDir, file)); } } } // Back up mod metadata if (existsSync(MOD_METADATA_FILE)) { copyFileSync(MOD_METADATA_FILE, join(snapDir, "mod-metadata.json")); } const meta: SnapshotMeta = { name: safeName, createdAt: new Date().toISOString(), modCount: mods.length, mods, }; writeFileSync(join(snapDir, "meta.json"), JSON.stringify(meta, null, 2)); // Enforce max snapshots pruneOldSnapshots(); return meta; } export function restoreSnapshot(dirName: string): void { const snapDir = join(SNAPSHOTS_DIR, dirName); if (!existsSync(snapDir)) { throw new Error(`Snapshot "${dirName}" not found`); } // Clear current server mods const currentMods = readdirSync(MODS_DIR).filter((f) => f.endsWith(".jar")); for (const file of currentMods) { unlinkSync(join(MODS_DIR, file)); } // Copy snapshot server mods back const snapFiles = readdirSync(snapDir).filter((f) => f.endsWith(".jar")); for (const file of snapFiles) { copyFileSync(join(snapDir, file), join(MODS_DIR, file)); } // Restore client mods if (existsSync(CLIENT_MODS_DIR)) { const currentClient = readdirSync(CLIENT_MODS_DIR).filter((f) => f.endsWith(".jar") ); for (const file of currentClient) { unlinkSync(join(CLIENT_MODS_DIR, file)); } } const clientSnapDir = join(snapDir, "client-mods"); if (existsSync(clientSnapDir)) { ensureDir(CLIENT_MODS_DIR); const snapClientFiles = readdirSync(clientSnapDir).filter((f) => f.endsWith(".jar") ); for (const file of snapClientFiles) { copyFileSync(join(clientSnapDir, file), join(CLIENT_MODS_DIR, file)); } } // Restore mod metadata const metaBackup = join(snapDir, "mod-metadata.json"); if (existsSync(metaBackup)) { copyFileSync(metaBackup, MOD_METADATA_FILE); } } export function listSnapshots(): (SnapshotMeta & { dirName: string })[] { ensureDir(SNAPSHOTS_DIR); const dirs = readdirSync(SNAPSHOTS_DIR).filter((d) => { const metaPath = join(SNAPSHOTS_DIR, d, "meta.json"); return existsSync(metaPath); }); return dirs .map((dirName) => { const meta = JSON.parse( readFileSync(join(SNAPSHOTS_DIR, dirName, "meta.json"), "utf8") ) as SnapshotMeta; const sizeBytes = dirSize(join(SNAPSHOTS_DIR, dirName)); return { ...meta, dirName, sizeBytes }; }) .sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); } export function deleteSnapshot(dirName: string): void { const snapDir = join(SNAPSHOTS_DIR, dirName); if (!existsSync(snapDir)) { throw new Error(`Snapshot "${dirName}" not found`); } rmSync(snapDir, { recursive: true }); } function pruneOldSnapshots(): void { const snapshots = listSnapshots(); if (snapshots.length > MAX_SNAPSHOTS) { const toDelete = snapshots.slice(MAX_SNAPSHOTS); for (const snap of toDelete) { deleteSnapshot(snap.dirName); } } }