mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-06-02 21:16:14 -04:00
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import { runBackupLifecycle } from "@zerobyte/core/backup-hooks";
|
|
import type { BackupSchedule, Volume, Repository } from "../../db/schema";
|
|
import { config } from "../../core/config";
|
|
import { restic, resticDeps } from "../../core/restic";
|
|
import type { BackupRunPayload } from "@zerobyte/contracts/agent-protocol";
|
|
import { agentManager, type BackupExecutionProgress } from "../agents/agents-manager";
|
|
import { LOCAL_AGENT_ID } from "../agents/constants";
|
|
import { getVolumePath } from "../volumes/helpers";
|
|
import { decryptVolumeConfig } from "../volumes/volume-config-secrets";
|
|
import { decryptRepositoryConfig } from "../repositories/repository-config-secrets";
|
|
import { createBackupOptions } from "./backup.helpers";
|
|
import { runEffectPromise, toErrorDetails } from "../../utils/errors";
|
|
import { BadRequestError } from "http-errors-enhanced";
|
|
|
|
const FUSE_VOLUME_BACKENDS = new Set<Volume["type"]>(["rclone", "sftp", "webdav"]);
|
|
const IGNORE_INODE_FLAG = "--ignore-inode";
|
|
type BackupExecutionRequest = {
|
|
jobId: string;
|
|
scheduleId: number;
|
|
schedule: BackupSchedule;
|
|
volume: Volume;
|
|
repository: Repository;
|
|
organizationId: string;
|
|
signal: AbortSignal;
|
|
onProgress: (progress: BackupExecutionProgress) => void;
|
|
};
|
|
|
|
export type { BackupExecutionResult } from "../agents/agents-manager";
|
|
|
|
const activeControllersByScheduleId = new Map<number, { abortController: AbortController; agentId: string | null }>();
|
|
|
|
const getBackupExecutionAgentId = (volume: Volume, repository: Repository) => {
|
|
if (repository.type === "local" && volume.agentId !== LOCAL_AGENT_ID) {
|
|
throw new BadRequestError(`Local repository "${repository.name}" can only be used with the local agent`);
|
|
}
|
|
|
|
return volume.agentId;
|
|
};
|
|
|
|
const createBackupRunPayload = async ({
|
|
jobId,
|
|
schedule,
|
|
volume,
|
|
repository,
|
|
organizationId,
|
|
}: BackupExecutionRequest): Promise<BackupRunPayload> => {
|
|
const agentVolume = { ...volume, config: await decryptVolumeConfig(volume.config) };
|
|
const customResticParams = schedule.customResticParams ?? [];
|
|
|
|
const repositoryConfig = await decryptRepositoryConfig(repository.config);
|
|
const encryptedResticPassword = await resticDeps.getOrganizationResticPassword(organizationId);
|
|
const resticPassword = await resticDeps.resolveSecret(encryptedResticPassword);
|
|
|
|
return {
|
|
jobId,
|
|
scheduleId: schedule.shortId,
|
|
organizationId,
|
|
volume: agentVolume,
|
|
repositoryConfig,
|
|
options: {
|
|
oneFileSystem: schedule.oneFileSystem,
|
|
excludePatterns: schedule.excludePatterns,
|
|
excludeIfPresent: schedule.excludeIfPresent,
|
|
includePaths: schedule.includePaths,
|
|
includePatterns: schedule.includePatterns,
|
|
customResticParams:
|
|
FUSE_VOLUME_BACKENDS.has(volume.type) && !customResticParams.includes(IGNORE_INODE_FLAG)
|
|
? [...customResticParams, IGNORE_INODE_FLAG]
|
|
: customResticParams,
|
|
compressionMode: repository.compressionMode ?? "auto",
|
|
},
|
|
runtime: {
|
|
password: resticPassword,
|
|
},
|
|
webhooks: schedule.backupWebhooks ?? { pre: null, post: null },
|
|
webhookAllowedOrigins: config.webhookAllowedOrigins,
|
|
webhookTimeoutMs: config.webhookTimeout * 1000,
|
|
};
|
|
};
|
|
|
|
const executeBackupWithoutAgent = async (
|
|
payload: BackupRunPayload,
|
|
{ schedule, volume, signal, onProgress }: BackupExecutionRequest,
|
|
) => {
|
|
const sourcePath = getVolumePath(volume);
|
|
const { signal: _, ...backupOptions } = createBackupOptions(schedule, sourcePath, signal);
|
|
const options = {
|
|
...backupOptions,
|
|
customResticParams: payload.options.customResticParams ?? [],
|
|
compressionMode: payload.options.compressionMode,
|
|
};
|
|
|
|
return runEffectPromise(
|
|
runBackupLifecycle({
|
|
restic,
|
|
repositoryConfig: payload.repositoryConfig,
|
|
sourcePath,
|
|
jobId: payload.jobId,
|
|
scheduleId: payload.scheduleId,
|
|
organizationId: payload.organizationId,
|
|
options,
|
|
webhooks: payload.webhooks,
|
|
webhookAllowedOrigins: payload.webhookAllowedOrigins,
|
|
webhookTimeoutMs: payload.webhookTimeoutMs,
|
|
signal,
|
|
onProgress,
|
|
formatError: toErrorDetails,
|
|
}),
|
|
);
|
|
};
|
|
|
|
export const backupExecutor = {
|
|
track: (scheduleId: number) => {
|
|
const abortController = new AbortController();
|
|
activeControllersByScheduleId.set(scheduleId, { abortController, agentId: null });
|
|
return abortController;
|
|
},
|
|
untrack: (scheduleId: number, abortController: AbortController) => {
|
|
if (activeControllersByScheduleId.get(scheduleId)?.abortController === abortController) {
|
|
activeControllersByScheduleId.delete(scheduleId);
|
|
}
|
|
},
|
|
execute: async (request: BackupExecutionRequest) => {
|
|
const trackedExecution = activeControllersByScheduleId.get(request.scheduleId);
|
|
if (!trackedExecution || trackedExecution.abortController.signal !== request.signal) {
|
|
throw new Error(`Backup execution for schedule ${request.scheduleId} was not tracked`);
|
|
}
|
|
|
|
if (request.signal.aborted) {
|
|
throw request.signal.reason || new Error("Operation aborted");
|
|
}
|
|
|
|
const payload = await createBackupRunPayload(request);
|
|
|
|
if (request.signal.aborted) {
|
|
throw request.signal.reason || new Error("Operation aborted");
|
|
}
|
|
|
|
const executionAgentId = getBackupExecutionAgentId(request.volume, request.repository);
|
|
trackedExecution.agentId = executionAgentId;
|
|
|
|
const executionResult = await agentManager.runBackup(executionAgentId, {
|
|
scheduleId: request.scheduleId,
|
|
payload,
|
|
signal: request.signal,
|
|
onProgress: request.onProgress,
|
|
});
|
|
|
|
if (
|
|
executionResult.status === "unavailable" &&
|
|
executionAgentId === LOCAL_AGENT_ID &&
|
|
!config.flags.enableLocalAgent
|
|
) {
|
|
return executeBackupWithoutAgent(payload, request);
|
|
}
|
|
|
|
return executionResult;
|
|
},
|
|
cancel: async (scheduleId: number) => {
|
|
const trackedExecution = activeControllersByScheduleId.get(scheduleId);
|
|
if (!trackedExecution) {
|
|
return false;
|
|
}
|
|
|
|
trackedExecution.abortController.abort();
|
|
if (!trackedExecution.agentId) {
|
|
return true;
|
|
}
|
|
|
|
await agentManager.cancelBackup(trackedExecution.agentId, scheduleId);
|
|
return true;
|
|
},
|
|
};
|