mc-dashboard/lib/snapshots.ts
hurkicorgi dd69c17c3b Initial commit: Minecraft dashboard
Next.js 16 + Tailwind v4 + shadcn v4 dashboard for managing a modded
Forge 1.20.1 server. Includes server controls, player management, mod
manager with Modrinth search and dependency resolution, world backups,
snapshots, analytics, logs, and chat bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:46:58 -06:00

160 lines
4.2 KiB
TypeScript

import {
existsSync,
mkdirSync,
readdirSync,
copyFileSync,
rmSync,
writeFileSync,
readFileSync,
unlinkSync,
} from "fs";
import { join } from "path";
import { MODS_DIR, CLIENT_MODS_DIR, MOD_METADATA_FILE } from "./constants";
const SNAPSHOTS_DIR = "/home/minecraft/server/snapshots";
const MAX_SNAPSHOTS = 10;
export type SnapshotMeta = {
name: string;
createdAt: string;
modCount: number;
mods: string[];
};
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;
return { ...meta, dirName };
})
.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);
}
}
}