import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, } from "node:fs"; import { extname, resolve } from "node:path"; import { tmpdir } from "node:os"; import { spawn } from "node:child_process"; import process from "node:process"; import yaml from "js-yaml"; function loadConfig() { const configPath = process.env.ICESHRIMP_CONFIG ?? ".config/default.yml"; const config = existsSync(configPath) ? yaml.load(readFileSync(configPath, "utf8")) ?? {} : {}; const db = config.db ?? {}; return { mediaDir: process.env.ICESHRIMP_MEDIA_DIR ?? "files", db: { host: process.env.PGHOST ?? db.host ?? "localhost", port: String(process.env.PGPORT ?? db.port ?? 5432), database: process.env.PGDATABASE ?? db.db ?? db.database ?? "iceshrimp", user: process.env.PGUSER ?? db.user ?? "postgres", password: process.env.PGPASSWORD ?? db.pass ?? db.password, }, }; } function run(command, args, env, options = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { stdio: "inherit", env, ...options, }); child.on("error", reject); child.on("exit", (code) => { if (code === 0) resolvePromise(); else reject(new Error(`${command} exited with code ${code}`)); }); }); } async function restoreDatabase(backupFile, db, env) { if (extname(backupFile) === ".sql") { await run("psql", ["--dbname", db.database, "--file", backupFile], env); } else { await run( "pg_restore", [ "--clean", "--if-exists", "--no-owner", "--dbname", db.database, backupFile, ], env, ); } } const input = process.argv[2]; if (!input) { console.error("Usage: yarn full:restore "); process.exit(1); } const backupFile = resolve(input); if (!existsSync(backupFile)) { console.error(`Backup file not found: ${backupFile}`); process.exit(1); } const config = loadConfig(); const env = { ...process.env, PGHOST: config.db.host, PGPORT: config.db.port, PGDATABASE: config.db.database, PGUSER: config.db.user, }; if (config.db.password != null) env.PGPASSWORD = config.db.password; if (backupFile.endsWith(".tar.gz") || backupFile.endsWith(".tgz")) { const workDir = mkdtempSync(resolve(tmpdir(), "iceshrimp-restore-")); try { await run("tar", ["-xzf", backupFile, "-C", workDir], process.env); const manifestPath = resolve(workDir, "manifest.json"); if (existsSync(manifestPath)) { const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); if (manifest.type !== "iceshrimp-full-backup") { throw new Error("Archive is not an Iceshrimp full backup"); } } const dumpPath = resolve(workDir, "database.dump"); if (!existsSync(dumpPath)) { throw new Error("Archive does not contain database.dump"); } await restoreDatabase(dumpPath, config.db, env); const filesPath = resolve(workDir, "files"); if (existsSync(filesPath)) { const mediaDir = resolve(config.mediaDir); mkdirSync(mediaDir, { recursive: true }); cpSync(filesPath, mediaDir, { recursive: true, dereference: false, }); console.log(`Media files restored to ${mediaDir}`); } const configPath = resolve(workDir, "config"); if (existsSync(configPath)) { const restoredConfigDir = resolve(".config/restored-backup"); mkdirSync(restoredConfigDir, { recursive: true }); cpSync(configPath, restoredConfigDir, { recursive: true, dereference: false, }); console.log(`Backup config files copied to ${restoredConfigDir}`); } } finally { rmSync(workDir, { recursive: true, force: true }); } } else { await restoreDatabase(backupFile, config.db, env); } console.log(`Backup restored from ${backupFile}`);