mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: partial success warning status (#74)
* feat: report partial backups with warnings * chore: rebase * chore: remove un-used size prop
This commit is contained in:
@@ -17,3 +17,25 @@ export const toMessage = (err: unknown): string => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return sanitizeSensitiveData(message);
|
||||
};
|
||||
|
||||
const resticErrorCodes: Record<number, string> = {
|
||||
1: "Command failed: An error occurred while executing the command.",
|
||||
2: "Go runtime error: A runtime error occurred in the Go program.",
|
||||
3: "Backup could not read all files: Some files could not be read during backup.",
|
||||
10: "Repository not found: The specified repository could not be found.",
|
||||
11: "Failed to lock repository: Unable to acquire a lock on the repository. Try to run doctor on the repository.",
|
||||
12: "Wrong repository password: The provided password for the repository is incorrect.",
|
||||
130: "Backup interrupted: The backup process was interrupted.",
|
||||
};
|
||||
|
||||
export class ResticError extends Error {
|
||||
code: number;
|
||||
|
||||
constructor(code: number, stderr: string) {
|
||||
const message = resticErrorCodes[code] || `Unknown restic error with code ${code}`;
|
||||
super(`${message}\n${stderr}`);
|
||||
|
||||
this.code = code;
|
||||
this.name = "ResticError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { cryptoUtils } from "./crypto";
|
||||
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||
import { safeSpawn } from "./spawn";
|
||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||
import { ResticError } from "./errors";
|
||||
|
||||
const backupOutputSchema = type({
|
||||
message_type: "'summary'",
|
||||
@@ -313,34 +314,41 @@ const backup = async (
|
||||
streamProgress(data);
|
||||
}
|
||||
},
|
||||
onStderr: (error) => {
|
||||
logger.error(error.trim());
|
||||
},
|
||||
finally: async () => {
|
||||
includeFile && (await fs.unlink(includeFile).catch(() => {}));
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
},
|
||||
});
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic backup failed: ${res.stderr}`);
|
||||
if (res.exitCode === 3) {
|
||||
logger.error(`Restic backup encountered read errors: ${res.stderr.toString()}`);
|
||||
}
|
||||
|
||||
if (res.exitCode !== 0 && res.exitCode !== 3) {
|
||||
logger.error(`Restic backup failed: ${res.stderr.toString()}`);
|
||||
logger.error(`Command executed: restic ${args.join(" ")}`);
|
||||
|
||||
throw new Error(`Restic backup failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
const lastLine = stdout.trim();
|
||||
const resSummary = JSON.parse(lastLine ?? "{}");
|
||||
let summaryLine = "";
|
||||
try {
|
||||
const resSummary = JSON.parse(lastLine ?? "{}");
|
||||
summaryLine = resSummary;
|
||||
} catch (_) {
|
||||
logger.warn("Failed to parse restic backup output JSON summary.", lastLine);
|
||||
summaryLine = "{}";
|
||||
}
|
||||
|
||||
const result = backupOutputSchema(resSummary);
|
||||
const result = backupOutputSchema(summaryLine);
|
||||
|
||||
if (result instanceof type.errors) {
|
||||
logger.error(`Restic backup output validation failed: ${result}`);
|
||||
|
||||
throw new Error(`Restic backup output validation failed: ${result}`);
|
||||
return { result: null, exitCode: res.exitCode };
|
||||
}
|
||||
|
||||
return result;
|
||||
return { result, exitCode: res.exitCode };
|
||||
};
|
||||
|
||||
const restoreOutputSchema = type({
|
||||
@@ -404,7 +412,7 @@ const restore = async (
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic restore failed: ${res.stderr}`);
|
||||
throw new Error(`Restic restore failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
const stdout = res.text();
|
||||
@@ -517,7 +525,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic forget failed: ${res.stderr}`);
|
||||
throw new Error(`Restic forget failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -535,7 +543,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic snapshot deletion failed: ${res.stderr}`);
|
||||
throw new Error(`Failed to delete snapshot: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -585,7 +593,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic ls failed: ${res.stderr}`);
|
||||
throw new Error(`Restic ls failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
// The output is a stream of JSON objects, first is snapshot info, rest are file/dir nodes
|
||||
@@ -636,7 +644,7 @@ const unlock = async (config: RepositoryConfig) => {
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic unlock failed: ${res.stderr}`);
|
||||
throw new Error(`Restic unlock failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
logger.info(`Restic unlock succeeded for repository: ${repoUrl}`);
|
||||
@@ -697,7 +705,7 @@ const repairIndex = async (config: RepositoryConfig) => {
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic repair index failed: ${stderr}`);
|
||||
throw new Error(`Restic repair index failed: ${stderr}`);
|
||||
throw new ResticError(res.exitCode, stderr);
|
||||
}
|
||||
|
||||
logger.info(`Restic repair index completed for repository: ${repoUrl}`);
|
||||
|
||||
@@ -41,9 +41,8 @@ export const safeSpawn = (params: Params) => {
|
||||
child.stderr.on("data", (data) => {
|
||||
if (callbacks.onStderr) {
|
||||
callbacks.onStderr(data.toString());
|
||||
} else {
|
||||
stderrData += data.toString();
|
||||
}
|
||||
stderrData += data.toString();
|
||||
});
|
||||
|
||||
child.on("error", async (error) => {
|
||||
|
||||
Reference in New Issue
Block a user