import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { basename, dirname, 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 { configPath, secretsPath: process.env.ICESHRIMP_SECRETS, url: config.url, version: JSON.parse(readFileSync("packages/meta.json", "utf8")).version, 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 timestamp() { const now = new Date(); const pad = (value) => value.toString().padStart(2, "0"); return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad( now.getDate(), )}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; } 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}`)); }); }); } const config = loadConfig(); const outFile = resolve( process.argv[2] ?? `backups/iceshrimp-full-${timestamp()}.tar.gz`, ); const workDir = mkdtempSync(resolve(tmpdir(), "iceshrimp-backup-")); mkdirSync(dirname(outFile), { recursive: true }); try { 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; await run( "pg_dump", [ "--format=custom", "--blobs", "--no-owner", "--file", resolve(workDir, "database.dump"), config.db.database, ], env, ); const configDir = resolve(workDir, "config"); mkdirSync(configDir, { recursive: true }); for (const path of [config.configPath, config.secretsPath].filter(Boolean)) { if (existsSync(path)) cpSync(path, resolve(configDir, basename(path))); } const mediaDir = resolve(config.mediaDir); const outDir = dirname(outFile); let includedMedia = false; if ( existsSync(mediaDir) && outDir !== mediaDir && !outDir.startsWith(`${mediaDir}/`) ) { cpSync(mediaDir, resolve(workDir, "files"), { recursive: true, dereference: false, }); includedMedia = true; } writeFileSync( resolve(workDir, "manifest.json"), `${JSON.stringify( { type: "iceshrimp-full-backup", version: config.version, createdAt: new Date().toISOString(), url: config.url, database: config.db.database, included: { database: true, config: true, media: includedMedia, }, restore: "yarn full:restore ", }, null, 2, )}\n`, "utf8", ); await run("tar", ["-czf", outFile, "-C", workDir, "."], process.env); console.log( `Full backup written to ${outFile} (${statSync(outFile).size} bytes)`, ); } finally { rmSync(workDir, { recursive: true, force: true }); }