Files
zerobyte/app/server/modules/backups/backup-executor.ts
Nico c371676ad0 feat(agent): add standalone agent runtime (#761)
* feat(agent): add standalone agent runtime

* fix(backups): bridge local executor to Effect restic API

* fix(agent): add Bun and DOM types to agent tsconfig

* refactor: wrap backup error in a tagged effect error

* fix: pr feedbacks
2026-04-08 20:47:15 +02:00

93 lines
2.6 KiB
TypeScript

import { Effect } from "effect";
import { restic } from "../../core/restic";
import type { BackupSchedule, Repository, Volume } from "../../db/schema";
import type { ResticBackupOutputDto, ResticBackupProgressDto } from "@zerobyte/core/restic";
import { createBackupOptions } from "./backup.helpers";
import { getVolumePath } from "../volumes/helpers";
type BackupExecutionRequest = {
scheduleId: number;
schedule: BackupSchedule;
volume: Volume;
repository: Repository;
organizationId: string;
signal: AbortSignal;
onProgress: (progress: BackupExecutionProgress) => void;
};
export type BackupExecutionProgress = ResticBackupProgressDto;
export type BackupExecutionResult =
| {
status: "unavailable";
error: Error;
}
| {
status: "completed";
exitCode: number;
result: ResticBackupOutputDto | null;
warningDetails: string | null;
}
| {
status: "failed";
error: unknown;
}
| {
status: "cancelled";
message?: string;
};
const activeControllersByScheduleId = new Map<number, AbortController>();
export const backupExecutor = {
track: (scheduleId: number) => {
const abortController = new AbortController();
activeControllersByScheduleId.set(scheduleId, abortController);
return abortController;
},
untrack: (scheduleId: number, abortController: AbortController) => {
if (activeControllersByScheduleId.get(scheduleId) === abortController) {
activeControllersByScheduleId.delete(scheduleId);
}
},
execute: async (params: BackupExecutionRequest): Promise<BackupExecutionResult> => {
const { schedule, volume, repository, organizationId, signal, onProgress } = params;
try {
const volumePath = getVolumePath(volume);
const backupOptions = createBackupOptions(schedule, volumePath, signal);
const execution = await Effect.runPromise(
restic
.backup(repository.config, volumePath, {
...backupOptions,
compressionMode: repository.compressionMode ?? "auto",
organizationId,
onProgress,
})
.pipe(
Effect.map((result) => ({ success: true as const, result })),
Effect.catchAll((error) => Effect.succeed({ success: false as const, error })),
),
);
if (!execution.success) {
throw execution.error;
}
const { exitCode, result, warningDetails } = execution.result;
return { status: "completed", exitCode, result, warningDetails };
} catch (error) {
return { status: "failed", error };
}
},
cancel: (scheduleId: number) => {
const abortController = activeControllersByScheduleId.get(scheduleId);
if (!abortController) {
return false;
}
abortController.abort();
return true;
},
};