mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: unify backend and frontend servers (#3)
* refactor: unify backend and frontend servers * refactor: correct paths for openapi & drizzle * refactor: move api-client to client * fix: drizzle paths * chore: fix linting issues * fix: form reset issue
This commit is contained in:
81
app/server/modules/backups/backups.controller.ts
Normal file
81
app/server/modules/backups/backups.controller.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Hono } from "hono";
|
||||
import { validator } from "hono-openapi";
|
||||
import {
|
||||
createBackupScheduleBody,
|
||||
createBackupScheduleDto,
|
||||
deleteBackupScheduleDto,
|
||||
getBackupScheduleDto,
|
||||
getBackupScheduleForVolumeDto,
|
||||
listBackupSchedulesDto,
|
||||
runBackupNowDto,
|
||||
stopBackupDto,
|
||||
updateBackupScheduleDto,
|
||||
updateBackupScheduleBody,
|
||||
type CreateBackupScheduleDto,
|
||||
type DeleteBackupScheduleDto,
|
||||
type GetBackupScheduleDto,
|
||||
type GetBackupScheduleForVolumeResponseDto,
|
||||
type ListBackupSchedulesResponseDto,
|
||||
type RunBackupNowDto,
|
||||
type StopBackupDto,
|
||||
type UpdateBackupScheduleDto,
|
||||
} from "./backups.dto";
|
||||
import { backupsService } from "./backups.service";
|
||||
|
||||
export const backupScheduleController = new Hono()
|
||||
.get("/", listBackupSchedulesDto, async (c) => {
|
||||
const schedules = await backupsService.listSchedules();
|
||||
|
||||
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
|
||||
})
|
||||
.get("/:scheduleId", getBackupScheduleDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
const schedule = await backupsService.getSchedule(Number(scheduleId));
|
||||
|
||||
return c.json<GetBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => {
|
||||
const volumeId = c.req.param("volumeId");
|
||||
const schedule = await backupsService.getScheduleForVolume(Number(volumeId));
|
||||
|
||||
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
||||
})
|
||||
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const schedule = await backupsService.createSchedule(body);
|
||||
|
||||
return c.json<CreateBackupScheduleDto>(schedule, 201);
|
||||
})
|
||||
.patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const schedule = await backupsService.updateSchedule(Number(scheduleId), body);
|
||||
|
||||
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
||||
})
|
||||
.delete("/:scheduleId", deleteBackupScheduleDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
await backupsService.deleteSchedule(Number(scheduleId));
|
||||
|
||||
return c.json<DeleteBackupScheduleDto>({ success: true }, 200);
|
||||
})
|
||||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
backupsService.executeBackup(Number(scheduleId), true).catch((error) => {
|
||||
console.error("Backup execution failed:", error);
|
||||
});
|
||||
|
||||
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||
})
|
||||
.post("/:scheduleId/stop", stopBackupDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
await backupsService.stopBackup(Number(scheduleId));
|
||||
|
||||
return c.json<StopBackupDto>({ success: true }, 200);
|
||||
});
|
||||
253
app/server/modules/backups/backups.dto.ts
Normal file
253
app/server/modules/backups/backups.dto.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { type } from "arktype";
|
||||
import { describeRoute, resolver } from "hono-openapi";
|
||||
import { volumeSchema } from "../volumes/volume.dto";
|
||||
import { repositorySchema } from "../repositories/repositories.dto";
|
||||
|
||||
const retentionPolicySchema = type({
|
||||
keepLast: "number?",
|
||||
keepHourly: "number?",
|
||||
keepDaily: "number?",
|
||||
keepWeekly: "number?",
|
||||
keepMonthly: "number?",
|
||||
keepYearly: "number?",
|
||||
keepWithinDuration: "string?",
|
||||
});
|
||||
|
||||
export type RetentionPolicy = typeof retentionPolicySchema.infer;
|
||||
|
||||
const backupScheduleSchema = type({
|
||||
id: "number",
|
||||
volumeId: "number",
|
||||
repositoryId: "string",
|
||||
enabled: "boolean",
|
||||
cronExpression: "string",
|
||||
retentionPolicy: retentionPolicySchema.or("null"),
|
||||
excludePatterns: "string[] | null",
|
||||
includePatterns: "string[] | null",
|
||||
lastBackupAt: "number | null",
|
||||
lastBackupStatus: "'success' | 'error' | 'in_progress' | null",
|
||||
lastBackupError: "string | null",
|
||||
nextBackupAt: "number | null",
|
||||
createdAt: "number",
|
||||
updatedAt: "number",
|
||||
}).and(
|
||||
type({
|
||||
volume: volumeSchema,
|
||||
repository: repositorySchema,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* List all backup schedules
|
||||
*/
|
||||
export const listBackupSchedulesResponse = backupScheduleSchema.array();
|
||||
|
||||
export type ListBackupSchedulesResponseDto = typeof listBackupSchedulesResponse.infer;
|
||||
|
||||
export const listBackupSchedulesDto = describeRoute({
|
||||
description: "List all backup schedules",
|
||||
tags: ["Backups"],
|
||||
operationId: "listBackupSchedules",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of backup schedules",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(listBackupSchedulesResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a single backup schedule
|
||||
*/
|
||||
export const getBackupScheduleResponse = backupScheduleSchema;
|
||||
|
||||
export type GetBackupScheduleDto = typeof getBackupScheduleResponse.infer;
|
||||
|
||||
export const getBackupScheduleDto = describeRoute({
|
||||
description: "Get a backup schedule by ID",
|
||||
tags: ["Backups"],
|
||||
operationId: "getBackupSchedule",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup schedule details",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(getBackupScheduleResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getBackupScheduleForVolumeResponse = backupScheduleSchema.or("null");
|
||||
|
||||
export type GetBackupScheduleForVolumeResponseDto = typeof getBackupScheduleForVolumeResponse.infer;
|
||||
|
||||
export const getBackupScheduleForVolumeDto = describeRoute({
|
||||
description: "Get a backup schedule for a specific volume",
|
||||
tags: ["Backups"],
|
||||
operationId: "getBackupScheduleForVolume",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup schedule details for the volume",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(getBackupScheduleForVolumeResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new backup schedule
|
||||
*/
|
||||
export const createBackupScheduleBody = type({
|
||||
volumeId: "number",
|
||||
repositoryId: "string",
|
||||
enabled: "boolean",
|
||||
cronExpression: "string",
|
||||
retentionPolicy: retentionPolicySchema.optional(),
|
||||
excludePatterns: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
tags: "string[]?",
|
||||
});
|
||||
|
||||
export type CreateBackupScheduleBody = typeof createBackupScheduleBody.infer;
|
||||
|
||||
export const createBackupScheduleResponse = backupScheduleSchema.omit("volume", "repository");
|
||||
|
||||
export type CreateBackupScheduleDto = typeof createBackupScheduleResponse.infer;
|
||||
|
||||
export const createBackupScheduleDto = describeRoute({
|
||||
description: "Create a new backup schedule for a volume",
|
||||
operationId: "createBackupSchedule",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
201: {
|
||||
description: "Backup schedule created successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(createBackupScheduleResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Update a backup schedule
|
||||
*/
|
||||
export const updateBackupScheduleBody = type({
|
||||
repositoryId: "string",
|
||||
enabled: "boolean?",
|
||||
cronExpression: "string",
|
||||
retentionPolicy: retentionPolicySchema.optional(),
|
||||
excludePatterns: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
tags: "string[]?",
|
||||
});
|
||||
|
||||
export type UpdateBackupScheduleBody = typeof updateBackupScheduleBody.infer;
|
||||
|
||||
export const updateBackupScheduleResponse = backupScheduleSchema.omit("volume", "repository");
|
||||
|
||||
export type UpdateBackupScheduleDto = typeof updateBackupScheduleResponse.infer;
|
||||
|
||||
export const updateBackupScheduleDto = describeRoute({
|
||||
description: "Update a backup schedule",
|
||||
operationId: "updateBackupSchedule",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup schedule updated successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(updateBackupScheduleResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a backup schedule
|
||||
*/
|
||||
export const deleteBackupScheduleResponse = type({
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export type DeleteBackupScheduleDto = typeof deleteBackupScheduleResponse.infer;
|
||||
|
||||
export const deleteBackupScheduleDto = describeRoute({
|
||||
description: "Delete a backup schedule",
|
||||
operationId: "deleteBackupSchedule",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup schedule deleted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(deleteBackupScheduleResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Run a backup immediately
|
||||
*/
|
||||
export const runBackupNowResponse = type({
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export type RunBackupNowDto = typeof runBackupNowResponse.infer;
|
||||
|
||||
export const runBackupNowDto = describeRoute({
|
||||
description: "Trigger a backup immediately for a schedule",
|
||||
operationId: "runBackupNow",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup started successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(runBackupNowResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Stop a running backup
|
||||
*/
|
||||
export const stopBackupResponse = type({
|
||||
success: "boolean",
|
||||
});
|
||||
|
||||
export type StopBackupDto = typeof stopBackupResponse.infer;
|
||||
|
||||
export const stopBackupDto = describeRoute({
|
||||
description: "Stop a backup that is currently in progress",
|
||||
operationId: "stopBackup",
|
||||
tags: ["Backups"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Backup stopped successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(stopBackupResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
409: {
|
||||
description: "No backup is currently running for this schedule",
|
||||
},
|
||||
},
|
||||
});
|
||||
353
app/server/modules/backups/backups.service.ts
Normal file
353
app/server/modules/backups/backups.service.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import cron from "node-cron";
|
||||
import { CronExpressionParser } from "cron-parser";
|
||||
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
|
||||
import { db } from "../../db/db";
|
||||
import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/schema";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { serverEvents } from "../../core/events";
|
||||
|
||||
const runningBackups = new Map<number, AbortController>();
|
||||
|
||||
const calculateNextRun = (cronExpression: string): number => {
|
||||
try {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: new Date(),
|
||||
tz: "UTC",
|
||||
});
|
||||
|
||||
return interval.next().getTime();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse cron expression "${cronExpression}": ${error}`);
|
||||
const fallback = new Date();
|
||||
fallback.setMinutes(fallback.getMinutes() + 1);
|
||||
return fallback.getTime();
|
||||
}
|
||||
};
|
||||
|
||||
const listSchedules = async () => {
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
with: {
|
||||
volume: true,
|
||||
repository: true,
|
||||
},
|
||||
});
|
||||
return schedules;
|
||||
};
|
||||
|
||||
const getSchedule = async (scheduleId: number) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(volumesTable.id, scheduleId),
|
||||
with: {
|
||||
volume: true,
|
||||
repository: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
return schedule;
|
||||
};
|
||||
|
||||
const createSchedule = async (data: CreateBackupScheduleBody) => {
|
||||
if (!cron.validate(data.cronExpression)) {
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, data.volumeId),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
throw new NotFoundError("Volume not found");
|
||||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const nextBackupAt = calculateNextRun(data.cronExpression);
|
||||
|
||||
const [newSchedule] = await db
|
||||
.insert(backupSchedulesTable)
|
||||
.values({
|
||||
volumeId: data.volumeId,
|
||||
repositoryId: data.repositoryId,
|
||||
enabled: data.enabled,
|
||||
cronExpression: data.cronExpression,
|
||||
retentionPolicy: data.retentionPolicy ?? null,
|
||||
excludePatterns: data.excludePatterns ?? [],
|
||||
includePatterns: data.includePatterns ?? [],
|
||||
nextBackupAt: nextBackupAt,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!newSchedule) {
|
||||
throw new Error("Failed to create backup schedule");
|
||||
}
|
||||
|
||||
return newSchedule;
|
||||
};
|
||||
|
||||
const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
if (data.cronExpression && !cron.validate(data.cronExpression)) {
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, data.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const cronExpression = data.cronExpression ?? schedule.cronExpression;
|
||||
const nextBackupAt = data.cronExpression ? calculateNextRun(cronExpression) : schedule.nextBackupAt;
|
||||
|
||||
const [updated] = await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({ ...data, nextBackupAt, updatedAt: Date.now() })
|
||||
.where(eq(backupSchedulesTable.id, scheduleId))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update backup schedule");
|
||||
}
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
const deleteSchedule = async (scheduleId: number) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
await db.delete(backupSchedulesTable).where(eq(backupSchedulesTable.id, scheduleId));
|
||||
};
|
||||
|
||||
const executeBackup = async (scheduleId: number, manual = false) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
if (!schedule.enabled && !manual) {
|
||||
logger.info(`Backup schedule ${scheduleId} is disabled. Skipping execution.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (schedule.lastBackupStatus === "in_progress") {
|
||||
logger.info(`Backup schedule ${scheduleId} is already in progress. Skipping execution.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, schedule.volumeId),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
throw new NotFoundError("Volume not found");
|
||||
}
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, schedule.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
if (volume.status !== "mounted") {
|
||||
throw new BadRequestError("Volume is not mounted");
|
||||
}
|
||||
|
||||
logger.info(`Starting backup for volume ${volume.name} to repository ${repository.name}`);
|
||||
|
||||
serverEvents.emit("backup:started", {
|
||||
scheduleId,
|
||||
volumeName: volume.name,
|
||||
repositoryName: repository.name,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({ lastBackupStatus: "in_progress", updatedAt: Date.now(), lastBackupError: null })
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
const abortController = new AbortController();
|
||||
runningBackups.set(scheduleId, abortController);
|
||||
|
||||
try {
|
||||
const volumePath = getVolumePath(volume);
|
||||
|
||||
const backupOptions: {
|
||||
exclude?: string[];
|
||||
include?: string[];
|
||||
tags?: string[];
|
||||
signal?: AbortSignal;
|
||||
} = {
|
||||
tags: [schedule.id.toString()],
|
||||
signal: abortController.signal,
|
||||
};
|
||||
|
||||
if (schedule.excludePatterns && schedule.excludePatterns.length > 0) {
|
||||
backupOptions.exclude = schedule.excludePatterns;
|
||||
}
|
||||
|
||||
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
|
||||
backupOptions.include = schedule.includePatterns;
|
||||
}
|
||||
|
||||
await restic.backup(repository.config, volumePath, {
|
||||
...backupOptions,
|
||||
onProgress: (progress) => {
|
||||
serverEvents.emit("backup:progress", {
|
||||
scheduleId,
|
||||
volumeName: volume.name,
|
||||
repositoryName: repository.name,
|
||||
...progress,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (schedule.retentionPolicy) {
|
||||
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
|
||||
}
|
||||
|
||||
const nextBackupAt = calculateNextRun(schedule.cronExpression);
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupAt: Date.now(),
|
||||
lastBackupStatus: "success",
|
||||
lastBackupError: null,
|
||||
nextBackupAt: nextBackupAt,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
logger.info(`Backup completed successfully for volume ${volume.name} to repository ${repository.name}`);
|
||||
|
||||
serverEvents.emit("backup:completed", {
|
||||
scheduleId,
|
||||
volumeName: volume.name,
|
||||
repositoryName: repository.name,
|
||||
status: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Backup failed for volume ${volume.name} to repository ${repository.name}: ${toMessage(error)}`);
|
||||
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupAt: Date.now(),
|
||||
lastBackupStatus: "error",
|
||||
lastBackupError: toMessage(error),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
serverEvents.emit("backup:completed", {
|
||||
scheduleId,
|
||||
volumeName: volume.name,
|
||||
repositoryName: repository.name,
|
||||
status: "error",
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
runningBackups.delete(scheduleId);
|
||||
}
|
||||
};
|
||||
|
||||
const getSchedulesToExecute = async () => {
|
||||
const now = Date.now();
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: eq(backupSchedulesTable.enabled, true),
|
||||
});
|
||||
|
||||
const schedulesToRun: number[] = [];
|
||||
|
||||
for (const schedule of schedules) {
|
||||
if (!schedule.nextBackupAt || schedule.nextBackupAt <= now) {
|
||||
schedulesToRun.push(schedule.id);
|
||||
}
|
||||
}
|
||||
|
||||
return schedulesToRun;
|
||||
};
|
||||
|
||||
const getScheduleForVolume = async (volumeId: number) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.volumeId, volumeId),
|
||||
with: { volume: true, repository: true },
|
||||
});
|
||||
|
||||
return schedule ?? null;
|
||||
};
|
||||
|
||||
const stopBackup = async (scheduleId: number) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupStatus: "error",
|
||||
lastBackupError: "Backup was stopped by user",
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
|
||||
const abortController = runningBackups.get(scheduleId);
|
||||
if (!abortController) {
|
||||
throw new ConflictError("No backup is currently running for this schedule");
|
||||
}
|
||||
|
||||
logger.info(`Stopping backup for schedule ${scheduleId}`);
|
||||
|
||||
abortController.abort();
|
||||
};
|
||||
|
||||
export const backupsService = {
|
||||
listSchedules,
|
||||
getSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
deleteSchedule,
|
||||
executeBackup,
|
||||
getSchedulesToExecute,
|
||||
getScheduleForVolume,
|
||||
stopBackup,
|
||||
};
|
||||
Reference in New Issue
Block a user