mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: throttle logs during backup
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"dockerode": "^4.0.8",
|
"dockerode": "^4.0.8",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.6",
|
"drizzle-orm": "^0.44.6",
|
||||||
|
"es-toolkit": "^1.41.0",
|
||||||
"hono": "^4.9.2",
|
"hono": "^4.9.2",
|
||||||
"hono-openapi": "^1.1.0",
|
"hono-openapi": "^1.1.0",
|
||||||
"http-errors-enhanced": "^3.0.2",
|
"http-errors-enhanced": "^3.0.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawn } from "node:child_process";
|
import { throttle } from "es-toolkit";
|
||||||
import type { RepositoryConfig } from "@ironmount/schemas/restic";
|
import type { RepositoryConfig } from "@ironmount/schemas/restic";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
@@ -9,6 +9,7 @@ import { REPOSITORY_BASE, RESTIC_PASS_FILE } from "../core/constants";
|
|||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { cryptoUtils } from "./crypto";
|
import { cryptoUtils } from "./crypto";
|
||||||
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||||
|
import { safeSpawn } from "./spawn";
|
||||||
|
|
||||||
const backupOutputSchema = type({
|
const backupOutputSchema = type({
|
||||||
message_type: "'summary'",
|
message_type: "'summary'",
|
||||||
@@ -149,49 +150,29 @@ const backup = async (
|
|||||||
|
|
||||||
args.push("--json");
|
args.push("--json");
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
const logData = throttle((data: string) => {
|
||||||
const child = spawn("restic", args, {
|
logger.info(data.trim());
|
||||||
env: { ...process.env, ...env },
|
}, 5000);
|
||||||
signal: options?.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
await safeSpawn({
|
||||||
stdout = data.toString();
|
command: "restic",
|
||||||
logger.info(data.toString());
|
args,
|
||||||
|
env,
|
||||||
|
signal: options?.signal,
|
||||||
|
onStdout: (data) => {
|
||||||
|
stdout = data;
|
||||||
|
logData(data);
|
||||||
|
},
|
||||||
|
onStderr: (error) => {
|
||||||
|
logger.error(error.trim());
|
||||||
|
},
|
||||||
|
finally: async () => {
|
||||||
|
includeFile && (await fs.unlink(includeFile).catch(() => {}));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
logger.error(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("error", async (error) => {
|
|
||||||
if (includeFile) {
|
|
||||||
await fs.unlink(includeFile).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.name === "AbortError") {
|
|
||||||
logger.info("Restic backup process was aborted");
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
logger.error(`Restic backup process error: ${error.message}`);
|
|
||||||
reject(new Error(`Restic backup process error: ${error.message}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", async (code) => {
|
|
||||||
if (includeFile) {
|
|
||||||
await fs.unlink(includeFile).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
logger.error(`Restic backup failed with exit code ${code}`);
|
|
||||||
reject(new Error(`Restic backup failed`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lastLine = stdout.trim();
|
const lastLine = stdout.trim();
|
||||||
const resSummary = JSON.parse(lastLine ?? "{}");
|
const resSummary = JSON.parse(lastLine ?? "{}");
|
||||||
|
|
||||||
@@ -199,17 +180,11 @@ const backup = async (
|
|||||||
|
|
||||||
if (result instanceof type.errors) {
|
if (result instanceof type.errors) {
|
||||||
logger.error(`Restic backup output validation failed: ${result}`);
|
logger.error(`Restic backup output validation failed: ${result}`);
|
||||||
reject(new Error(`Restic backup output validation failed: ${result}`));
|
|
||||||
return;
|
throw new Error(`Restic backup output validation failed: ${result}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(result);
|
return result;
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to parse restic backup output: ${error}`);
|
|
||||||
reject(new Error(`Failed to parse restic backup output: ${error}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreOutputSchema = type({
|
const restoreOutputSchema = type({
|
||||||
@@ -370,7 +345,6 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
|||||||
args.push("--prune");
|
args.push("--prune");
|
||||||
args.push("--json");
|
args.push("--json");
|
||||||
|
|
||||||
// await $`restic unlock --repo ${repoUrl}`.env(env).nothrow();
|
|
||||||
const res = await $`restic ${args}`.env(env).nothrow();
|
const res = await $`restic ${args}`.env(env).nothrow();
|
||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
@@ -465,7 +439,7 @@ const unlock = async (config: RepositoryConfig) => {
|
|||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
const env = await buildEnv(config);
|
const env = await buildEnv(config);
|
||||||
|
|
||||||
const res = await $`restic unlock --repo ${repoUrl} --json`.env(env).nothrow();
|
const res = await $`restic unlock --repo ${repoUrl} --remove-all --json`.env(env).nothrow();
|
||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic unlock failed: ${res.stderr}`);
|
logger.error(`Restic unlock failed: ${res.stderr}`);
|
||||||
@@ -501,7 +475,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasErrors = stdout.includes("error") || stdout.includes("Fatal");
|
const hasErrors = stdout.includes("Fatal");
|
||||||
|
|
||||||
logger.info(`Restic check completed for repository: ${repoUrl}`);
|
logger.info(`Restic check completed for repository: ${repoUrl}`);
|
||||||
return {
|
return {
|
||||||
|
|||||||
56
apps/server/src/utils/spawn.ts
Normal file
56
apps/server/src/utils/spawn.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
onStdout?: (data: string) => void;
|
||||||
|
onStderr?: (error: string) => void;
|
||||||
|
onError?: (error: Error) => Promise<void> | void;
|
||||||
|
onClose?: (code: number | null) => Promise<void> | void;
|
||||||
|
finally?: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const safeSpawn = (params: Params) => {
|
||||||
|
const { command, args, env = {}, signal, ...callbacks } = params;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
signal: signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
if (callbacks.onStdout) {
|
||||||
|
callbacks.onStdout(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", (data) => {
|
||||||
|
if (callbacks.onStderr) {
|
||||||
|
callbacks.onStderr(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", async (error) => {
|
||||||
|
if (callbacks.onError) {
|
||||||
|
await callbacks.onError(error);
|
||||||
|
}
|
||||||
|
if (callbacks.finally) {
|
||||||
|
await callbacks.finally();
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", async (code) => {
|
||||||
|
if (callbacks.onClose) {
|
||||||
|
await callbacks.onClose(code);
|
||||||
|
}
|
||||||
|
if (callbacks.finally) {
|
||||||
|
await callbacks.finally();
|
||||||
|
}
|
||||||
|
resolve(code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
5
bun.lock
5
bun.lock
@@ -76,6 +76,7 @@
|
|||||||
"dockerode": "^4.0.8",
|
"dockerode": "^4.0.8",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.6",
|
"drizzle-orm": "^0.44.6",
|
||||||
|
"es-toolkit": "^1.41.0",
|
||||||
"hono": "^4.9.2",
|
"hono": "^4.9.2",
|
||||||
"hono-openapi": "^1.1.0",
|
"hono-openapi": "^1.1.0",
|
||||||
"http-errors-enhanced": "^3.0.2",
|
"http-errors-enhanced": "^3.0.2",
|
||||||
@@ -812,7 +813,7 @@
|
|||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
"es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
|
"es-toolkit": ["es-toolkit@1.41.0", "", {}, "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||||
|
|
||||||
@@ -1594,6 +1595,8 @@
|
|||||||
|
|
||||||
"protobufjs/@types/node": ["@types/node@20.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg=="],
|
"protobufjs/@types/node": ["@types/node@20.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg=="],
|
||||||
|
|
||||||
|
"recharts/es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
|
||||||
|
|
||||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user