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>
This commit is contained in:
commit
dd69c17c3b
77 changed files with 7007 additions and 0 deletions
160
lib/snapshots.ts
Normal file
160
lib/snapshots.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue