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:
@@ -1181,7 +1181,7 @@ export type ListBackupSchedulesResponses = {
|
|||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
||||||
@@ -1351,7 +1351,7 @@ export type CreateBackupScheduleResponses = {
|
|||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
retentionPolicy: {
|
retentionPolicy: {
|
||||||
@@ -1412,7 +1412,7 @@ export type GetBackupScheduleResponses = {
|
|||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
||||||
@@ -1583,7 +1583,7 @@ export type UpdateBackupScheduleResponses = {
|
|||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
retentionPolicy: {
|
retentionPolicy: {
|
||||||
@@ -1624,7 +1624,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'better' | 'fastest' | 'max' | 'off' | null;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { BackendType } from "~/schemas/volumes";
|
|||||||
|
|
||||||
type VolumeIconProps = {
|
type VolumeIconProps = {
|
||||||
backend: BackendType;
|
backend: BackendType;
|
||||||
size?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIconAndColor = (backend: BackendType) => {
|
const getIconAndColor = (backend: BackendType) => {
|
||||||
@@ -41,12 +40,12 @@ const getIconAndColor = (backend: BackendType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => {
|
export const VolumeIcon = ({ backend }: VolumeIconProps) => {
|
||||||
const { icon: Icon, label } = getIconAndColor(backend);
|
const { icon: Icon, label } = getIconAndColor(backend);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
|
<span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
|
||||||
<Icon size={size} />
|
<Icon className="h-4 w-4" />
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,10 +164,20 @@ export const ScheduleSummary = (props: Props) => {
|
|||||||
{schedule.lastBackupStatus === "success" && "✓ Success"}
|
{schedule.lastBackupStatus === "success" && "✓ Success"}
|
||||||
{schedule.lastBackupStatus === "error" && "✗ Error"}
|
{schedule.lastBackupStatus === "error" && "✗ Error"}
|
||||||
{schedule.lastBackupStatus === "in_progress" && "⟳ in progress..."}
|
{schedule.lastBackupStatus === "in_progress" && "⟳ in progress..."}
|
||||||
|
{schedule.lastBackupStatus === "warning" && "! Warning"}
|
||||||
{!schedule.lastBackupStatus && "—"}
|
{!schedule.lastBackupStatus && "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{schedule.lastBackupStatus === "warning" && (
|
||||||
|
<div className="md:col-span-2 lg:col-span-4">
|
||||||
|
<p className="text-xs uppercase text-muted-foreground">Warning Details</p>
|
||||||
|
<p className="font-mono text-sm text-yellow-600 whitespace-pre-wrap break-all">
|
||||||
|
Last backup completed with warnings. Check your container logs for more details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{schedule.lastBackupError && (
|
{schedule.lastBackupError && (
|
||||||
<div className="md:col-span-2 lg:col-span-4">
|
<div className="md:col-span-2 lg:col-span-4">
|
||||||
<p className="text-xs uppercase text-muted-foreground">Error Details</p>
|
<p className="text-xs uppercase text-muted-foreground">Error Details</p>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
|||||||
|
|
||||||
{volume.status[0].toUpperCase() + volume.status.slice(1)}
|
{volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
<VolumeIcon size={14} backend={volume?.config.backend} />
|
<VolumeIcon backend={volume?.config.backend} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export const NOTIFICATION_EVENTS = {
|
|||||||
start: "start",
|
start: "start",
|
||||||
success: "success",
|
success: "success",
|
||||||
failure: "failure",
|
failure: "failure",
|
||||||
|
warning: "warning",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type NotificationEvent = keyof typeof NOTIFICATION_EVENTS;
|
export type NotificationEvent = keyof typeof NOTIFICATION_EVENTS;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface ServerEvents {
|
|||||||
scheduleId: number;
|
scheduleId: number;
|
||||||
volumeName: string;
|
volumeName: string;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
status: "success" | "error" | "stopped";
|
status: "success" | "error" | "stopped" | "warning";
|
||||||
}) => void;
|
}) => void;
|
||||||
"volume:mounted": (data: { volumeName: string }) => void;
|
"volume:mounted": (data: { volumeName: string }) => void;
|
||||||
"volume:unmounted": (data: { volumeName: string }) => void;
|
"volume:unmounted": (data: { volumeName: string }) => void;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|||||||
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||||
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||||
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
||||||
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress">(),
|
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
|
||||||
lastBackupError: text("last_backup_error"),
|
lastBackupError: text("last_backup_error"),
|
||||||
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const backupScheduleSchema = type({
|
|||||||
excludePatterns: "string[] | null",
|
excludePatterns: "string[] | null",
|
||||||
includePatterns: "string[] | null",
|
includePatterns: "string[] | null",
|
||||||
lastBackupAt: "number | null",
|
lastBackupAt: "number | null",
|
||||||
lastBackupStatus: "'success' | 'error' | 'in_progress' | null",
|
lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null",
|
||||||
lastBackupError: "string | null",
|
lastBackupError: "string | null",
|
||||||
nextBackupAt: "number | null",
|
nextBackupAt: "number | null",
|
||||||
createdAt: "number",
|
createdAt: "number",
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||||||
backupOptions.include = schedule.includePatterns;
|
backupOptions.include = schedule.includePatterns;
|
||||||
}
|
}
|
||||||
|
|
||||||
await restic.backup(repository.config, volumePath, {
|
const { exitCode } = await restic.backup(repository.config, volumePath, {
|
||||||
...backupOptions,
|
...backupOptions,
|
||||||
compressionMode: repository.compressionMode ?? "auto",
|
compressionMode: repository.compressionMode ?? "auto",
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
@@ -258,24 +258,28 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||||||
.update(backupSchedulesTable)
|
.update(backupSchedulesTable)
|
||||||
.set({
|
.set({
|
||||||
lastBackupAt: Date.now(),
|
lastBackupAt: Date.now(),
|
||||||
lastBackupStatus: "success",
|
lastBackupStatus: exitCode === 0 ? "success" : "warning",
|
||||||
lastBackupError: null,
|
lastBackupError: null,
|
||||||
nextBackupAt: nextBackupAt,
|
nextBackupAt: nextBackupAt,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||||
|
|
||||||
logger.info(`Backup completed successfully for volume ${volume.name} to repository ${repository.name}`);
|
if (exitCode !== 0) {
|
||||||
|
logger.warn(`Backup completed with warnings for volume ${volume.name} to repository ${repository.name}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`Backup completed successfully for volume ${volume.name} to repository ${repository.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
serverEvents.emit("backup:completed", {
|
serverEvents.emit("backup:completed", {
|
||||||
scheduleId,
|
scheduleId,
|
||||||
volumeName: volume.name,
|
volumeName: volume.name,
|
||||||
repositoryName: repository.name,
|
repositoryName: repository.name,
|
||||||
status: "success",
|
status: exitCode === 0 ? "success" : "warning",
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationsService
|
notificationsService
|
||||||
.sendBackupNotification(scheduleId, "success", {
|
.sendBackupNotification(scheduleId, exitCode === 0 ? "success" : "warning", {
|
||||||
volumeName: volume.name,
|
volumeName: volume.name,
|
||||||
repositoryName: repository.name,
|
repositoryName: repository.name,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const eventsController = new Hono().get("/", (c) => {
|
|||||||
scheduleId: number;
|
scheduleId: number;
|
||||||
volumeName: string;
|
volumeName: string;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
status: "success" | "error" | "stopped";
|
status: "success" | "error" | "stopped" | "warning";
|
||||||
}) => {
|
}) => {
|
||||||
stream.writeSSE({
|
stream.writeSSE({
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ const sendBackupNotification = async (
|
|||||||
case "success":
|
case "success":
|
||||||
return assignment.notifyOnSuccess;
|
return assignment.notifyOnSuccess;
|
||||||
case "failure":
|
case "failure":
|
||||||
|
case "warning":
|
||||||
return assignment.notifyOnFailure;
|
return assignment.notifyOnFailure;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -367,7 +368,7 @@ function buildNotificationMessage(
|
|||||||
|
|
||||||
case "success":
|
case "success":
|
||||||
return {
|
return {
|
||||||
title: "✅ Backup Completed Successfully",
|
title: "✅ Backup Completed successfully",
|
||||||
body: [
|
body: [
|
||||||
`Volume: ${context.volumeName}`,
|
`Volume: ${context.volumeName}`,
|
||||||
`Repository: ${context.repositoryName}`,
|
`Repository: ${context.repositoryName}`,
|
||||||
@@ -381,9 +382,26 @@ function buildNotificationMessage(
|
|||||||
.join("\n"),
|
.join("\n"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case "warning":
|
||||||
|
return {
|
||||||
|
title: "! Backup completed with warnings",
|
||||||
|
body: [
|
||||||
|
`Volume: ${context.volumeName}`,
|
||||||
|
`Repository: ${context.repositoryName}`,
|
||||||
|
context.duration ? `Duration: ${Math.round(context.duration / 1000)}s` : null,
|
||||||
|
context.filesProcessed !== undefined ? `Files: ${context.filesProcessed}` : null,
|
||||||
|
context.bytesProcessed ? `Size: ${context.bytesProcessed}` : null,
|
||||||
|
context.snapshotId ? `Snapshot: ${context.snapshotId}` : null,
|
||||||
|
context.error ? `Warning: ${context.error}` : null,
|
||||||
|
`Time: ${date} - ${time}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
};
|
||||||
|
|
||||||
case "failure":
|
case "failure":
|
||||||
return {
|
return {
|
||||||
title: "❌ Backup Failed",
|
title: "❌ Backup failed",
|
||||||
body: [
|
body: [
|
||||||
`Volume: ${context.volumeName}`,
|
`Volume: ${context.volumeName}`,
|
||||||
`Repository: ${context.repositoryName}`,
|
`Repository: ${context.repositoryName}`,
|
||||||
|
|||||||
@@ -17,3 +17,25 @@ export const toMessage = (err: unknown): string => {
|
|||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
return sanitizeSensitiveData(message);
|
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 type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||||
import { safeSpawn } from "./spawn";
|
import { safeSpawn } from "./spawn";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
|
import { ResticError } from "./errors";
|
||||||
|
|
||||||
const backupOutputSchema = type({
|
const backupOutputSchema = type({
|
||||||
message_type: "'summary'",
|
message_type: "'summary'",
|
||||||
@@ -313,34 +314,41 @@ const backup = async (
|
|||||||
streamProgress(data);
|
streamProgress(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStderr: (error) => {
|
|
||||||
logger.error(error.trim());
|
|
||||||
},
|
|
||||||
finally: async () => {
|
finally: async () => {
|
||||||
includeFile && (await fs.unlink(includeFile).catch(() => {}));
|
includeFile && (await fs.unlink(includeFile).catch(() => {}));
|
||||||
await cleanupTemporaryKeys(config, env);
|
await cleanupTemporaryKeys(config, env);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode === 3) {
|
||||||
logger.error(`Restic backup failed: ${res.stderr}`);
|
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(" ")}`);
|
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 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) {
|
if (result instanceof type.errors) {
|
||||||
logger.error(`Restic backup output validation failed: ${result}`);
|
logger.error(`Restic backup output validation failed: ${result}`);
|
||||||
|
return { result: null, exitCode: res.exitCode };
|
||||||
throw new Error(`Restic backup output validation failed: ${result}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return { result, exitCode: res.exitCode };
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreOutputSchema = type({
|
const restoreOutputSchema = type({
|
||||||
@@ -404,7 +412,7 @@ const restore = async (
|
|||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic restore failed: ${res.stderr}`);
|
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();
|
const stdout = res.text();
|
||||||
@@ -517,7 +525,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
|||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic forget failed: ${res.stderr}`);
|
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 };
|
return { success: true };
|
||||||
@@ -535,7 +543,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
|||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic snapshot deletion failed: ${res.stderr}`);
|
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 };
|
return { success: true };
|
||||||
@@ -585,7 +593,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
|
|||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic ls failed: ${res.stderr}`);
|
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
|
// 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) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic unlock failed: ${res.stderr}`);
|
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}`);
|
logger.info(`Restic unlock succeeded for repository: ${repoUrl}`);
|
||||||
@@ -697,7 +705,7 @@ const repairIndex = async (config: RepositoryConfig) => {
|
|||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
logger.error(`Restic repair index failed: ${stderr}`);
|
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}`);
|
logger.info(`Restic repair index completed for repository: ${repoUrl}`);
|
||||||
|
|||||||
@@ -41,9 +41,8 @@ export const safeSpawn = (params: Params) => {
|
|||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
if (callbacks.onStderr) {
|
if (callbacks.onStderr) {
|
||||||
callbacks.onStderr(data.toString());
|
callbacks.onStderr(data.toString());
|
||||||
} else {
|
|
||||||
stderrData += data.toString();
|
|
||||||
}
|
}
|
||||||
|
stderrData += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", async (error) => {
|
child.on("error", async (error) => {
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ services:
|
|||||||
|
|
||||||
- ./app:/app/app
|
- ./app:/app/app
|
||||||
- ~/.config/rclone:/root/.config/rclone
|
- ~/.config/rclone:/root/.config/rclone
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte:rshared
|
# - /run/docker/plugins:/run/docker/plugins
|
||||||
- /run/docker/plugins:/run/docker/plugins
|
# - /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
|
|
||||||
zerobyte-prod:
|
zerobyte-prod:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user