Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import * as fs from "node:fs";
|
||||
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import archiver from "archiver";
|
||||
import config from "../../../../config/index.js";
|
||||
import define from "../../define.js";
|
||||
import { ApiError } from "../../error.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { createTempDir } from "../../../../misc/create-temp.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
size: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
includedMedia: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
backupFailed: {
|
||||
message: "Failed to create backup.",
|
||||
code: "BACKUP_FAILED",
|
||||
id: "7498ab9f-4e1d-40d0-96d8-d8d1d82bd621"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../../..");
|
||||
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) {
|
||||
return new Promise((resolvePromise, reject)=>{
|
||||
const child = spawn(command, args, {
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"pipe"
|
||||
],
|
||||
env
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk)=>{
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code)=>{
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
} else {
|
||||
reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function archiveDirectory(sourceDir, outFile) {
|
||||
return new Promise((resolvePromise, reject)=>{
|
||||
const output = fs.createWriteStream(outFile);
|
||||
const archive = archiver("tar", {
|
||||
gzip: true,
|
||||
gzipOptions: {
|
||||
level: 6
|
||||
}
|
||||
});
|
||||
output.on("close", ()=>resolvePromise());
|
||||
archive.on("error", reject);
|
||||
archive.pipe(output);
|
||||
archive.directory(sourceDir, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
async function pathExists(path) {
|
||||
return stat(path).then(()=>true).catch(()=>false);
|
||||
}
|
||||
export default define(meta, paramDef, async (_ps, me)=>{
|
||||
const [workDir, cleanup] = await createTempDir();
|
||||
const fileName = `iceshrimp-full-${timestamp()}.tar.gz`;
|
||||
const outDir = resolve(process.env.ICESHRIMP_BACKUP_DIR ?? `${rootDir}/backups`);
|
||||
const outFile = resolve(outDir, fileName);
|
||||
const dbDumpPath = resolve(workDir, "database.dump");
|
||||
const mediaDir = resolve(config.mediaDir);
|
||||
let includedMedia = false;
|
||||
try {
|
||||
await mkdir(outDir, {
|
||||
recursive: true
|
||||
});
|
||||
const env = {
|
||||
...process.env,
|
||||
PGHOST: config.db.host,
|
||||
PGPORT: String(config.db.port),
|
||||
PGDATABASE: config.db.db,
|
||||
PGUSER: config.db.user,
|
||||
PGPASSWORD: config.db.pass
|
||||
};
|
||||
await run("pg_dump", [
|
||||
"--format=custom",
|
||||
"--blobs",
|
||||
"--no-owner",
|
||||
"--file",
|
||||
dbDumpPath,
|
||||
config.db.db
|
||||
], env);
|
||||
const configDir = resolve(workDir, "config");
|
||||
await mkdir(configDir, {
|
||||
recursive: true
|
||||
});
|
||||
const configFiles = [
|
||||
process.env.ICESHRIMP_CONFIG ? resolve(process.env.ICESHRIMP_CONFIG) : resolve(rootDir, ".config/default.yml"),
|
||||
...process.env.ICESHRIMP_SECRETS ? [
|
||||
resolve(process.env.ICESHRIMP_SECRETS)
|
||||
] : []
|
||||
];
|
||||
for (const configFile of configFiles){
|
||||
if (await pathExists(configFile)) {
|
||||
fs.copyFileSync(configFile, resolve(configDir, configFile.split("/").pop()));
|
||||
}
|
||||
}
|
||||
if (await pathExists(mediaDir) && !outDir.startsWith(mediaDir + "/") && outDir !== mediaDir) {
|
||||
fs.cpSync(mediaDir, resolve(workDir, "files"), {
|
||||
recursive: true,
|
||||
dereference: false,
|
||||
errorOnExist: false
|
||||
});
|
||||
includedMedia = true;
|
||||
}
|
||||
await writeFile(resolve(workDir, "manifest.json"), `${JSON.stringify({
|
||||
type: "iceshrimp-full-backup",
|
||||
version: config.version,
|
||||
createdAt: new Date().toISOString(),
|
||||
host: config.host,
|
||||
database: config.db.db,
|
||||
included: {
|
||||
database: true,
|
||||
config: true,
|
||||
media: includedMedia
|
||||
},
|
||||
restore: "yarn full:restore <this archive>"
|
||||
}, null, 2)}\n`, "utf8");
|
||||
await archiveDirectory(workDir, outFile);
|
||||
const outStat = await stat(outFile);
|
||||
await insertModerationLog(me, "createBackup", {
|
||||
path: outFile,
|
||||
size: outStat.size,
|
||||
includedMedia
|
||||
});
|
||||
return {
|
||||
path: outFile,
|
||||
fileName,
|
||||
size: outStat.size,
|
||||
includedMedia
|
||||
};
|
||||
} catch (e) {
|
||||
await rm(outFile, {
|
||||
force: true
|
||||
}).catch(()=>{});
|
||||
throw new ApiError(meta.errors.backupFailed, {
|
||||
message: e instanceof Error ? e.message : String(e)
|
||||
});
|
||||
} finally{
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user