144 lines
3.5 KiB
JavaScript
144 lines
3.5 KiB
JavaScript
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 <this archive>",
|
|
},
|
|
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 });
|
|
}
|