Initial push
This commit is contained in:
141
lib/backup/manager.ts
Normal file
141
lib/backup/manager.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Backup manager: creates zip archives of worlds, plugins, or config.
|
||||
* Uses the `archiver` package.
|
||||
*/
|
||||
|
||||
import archiver from "archiver";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { db } from "@/lib/db";
|
||||
import { backups } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export type BackupType = "worlds" | "plugins" | "config" | "full";
|
||||
|
||||
const BACKUPS_DIR =
|
||||
process.env.BACKUPS_PATH ?? path.join(process.cwd(), "backups");
|
||||
|
||||
/** Ensure the backups directory exists. */
|
||||
function ensureBackupsDir(): void {
|
||||
fs.mkdirSync(BACKUPS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup archive of the specified type.
|
||||
* Returns the backup record ID.
|
||||
*/
|
||||
export async function createBackup(
|
||||
type: BackupType,
|
||||
triggeredBy: string,
|
||||
): Promise<string> {
|
||||
ensureBackupsDir();
|
||||
|
||||
const mcPath = process.env.MC_SERVER_PATH ?? "/opt/minecraft/server";
|
||||
const id = nanoid();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const fileName = `backup-${type}-${timestamp}.zip`;
|
||||
const filePath = path.join(BACKUPS_DIR, fileName);
|
||||
|
||||
// Insert pending record
|
||||
await db.insert(backups).values({
|
||||
id,
|
||||
name: fileName,
|
||||
type,
|
||||
size: 0,
|
||||
path: filePath,
|
||||
createdAt: Date.now(),
|
||||
status: "running",
|
||||
triggeredBy,
|
||||
});
|
||||
|
||||
try {
|
||||
await archiveBackup(type, mcPath, filePath);
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
await db
|
||||
.update(backups)
|
||||
.set({ status: "completed", size: stat.size })
|
||||
.where(eq(backups.id, id));
|
||||
} catch (err) {
|
||||
await db
|
||||
.update(backups)
|
||||
.set({ status: "failed" })
|
||||
.where(eq(backups.id, id));
|
||||
throw err;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function archiveBackup(
|
||||
type: BackupType,
|
||||
mcPath: string,
|
||||
outPath: string,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(outPath);
|
||||
const archive = archiver("zip", { zlib: { level: 6 } });
|
||||
|
||||
output.on("close", resolve);
|
||||
archive.on("error", reject);
|
||||
archive.pipe(output);
|
||||
|
||||
const dirsToArchive: { src: string; name: string }[] = [];
|
||||
|
||||
if (type === "worlds" || type === "full") {
|
||||
// Common world directory names
|
||||
for (const dir of ["world", "world_nether", "world_the_end"]) {
|
||||
const p = path.join(mcPath, dir);
|
||||
if (fs.existsSync(p)) dirsToArchive.push({ src: p, name: dir });
|
||||
}
|
||||
}
|
||||
if (type === "plugins" || type === "full") {
|
||||
const p = path.join(mcPath, "plugins");
|
||||
if (fs.existsSync(p)) dirsToArchive.push({ src: p, name: "plugins" });
|
||||
}
|
||||
if (type === "config" || type === "full") {
|
||||
// Config files at server root
|
||||
for (const file of [
|
||||
"server.properties",
|
||||
"ops.json",
|
||||
"whitelist.json",
|
||||
"banned-players.json",
|
||||
"banned-ips.json",
|
||||
"eula.txt",
|
||||
"spigot.yml",
|
||||
"paper.yml",
|
||||
"bukkit.yml",
|
||||
]) {
|
||||
const p = path.join(mcPath, file);
|
||||
if (fs.existsSync(p)) archive.file(p, { name: `config/${file}` });
|
||||
}
|
||||
}
|
||||
|
||||
for (const { src, name } of dirsToArchive) {
|
||||
archive.directory(src, name);
|
||||
}
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a backup file and its DB record. */
|
||||
export async function deleteBackup(id: string): Promise<void> {
|
||||
const record = await db
|
||||
.select()
|
||||
.from(backups)
|
||||
.where(eq(backups.id, id))
|
||||
.get();
|
||||
if (!record) throw new Error("Backup not found");
|
||||
|
||||
if (record.path && fs.existsSync(record.path)) {
|
||||
fs.unlinkSync(record.path);
|
||||
}
|
||||
await db.delete(backups).where(eq(backups.id, id));
|
||||
}
|
||||
|
||||
/** List all backups from the DB. */
|
||||
export async function listBackups() {
|
||||
return db.select().from(backups).orderBy(backups.createdAt);
|
||||
}
|
||||
Reference in New Issue
Block a user