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>
160 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|