feat: naming backup schedules (#103)

* docs: add agents instructions

* feat: naming backup schedules

* fix: wrong table for filtering
This commit is contained in:
Nico
2025-12-04 18:31:00 +01:00
committed by Nicolas Meienberger
parent a0fa043207
commit 2c11b7c7de
20 changed files with 1382 additions and 434 deletions

View File

@@ -67,6 +67,7 @@ export type Repository = typeof repositoriesTable.$inferSelect;
*/
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull().unique(),
volumeId: int("volume_id")
.notNull()
.references(() => volumesTable.id, { onDelete: "cascade" }),

View File

@@ -17,6 +17,7 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer;
const backupScheduleSchema = type({
id: "number",
name: "string",
volumeId: "number",
repositoryId: "string",
enabled: "boolean",
@@ -120,6 +121,7 @@ export const getBackupScheduleForVolumeDto = describeRoute({
* Create a new backup schedule
*/
export const createBackupScheduleBody = type({
name: "1 <= string <= 32",
volumeId: "number",
repositoryId: "string",
enabled: "boolean",
@@ -156,6 +158,7 @@ export const createBackupScheduleDto = describeRoute({
* Update a backup schedule
*/
export const updateBackupScheduleBody = type({
name: "(1 <= string <= 32)?",
repositoryId: "string",
enabled: "boolean?",
cronExpression: "string",

View File

@@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { and, eq, ne } from "drizzle-orm";
import cron from "node-cron";
import { CronExpressionParser } from "cron-parser";
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
@@ -44,7 +44,7 @@ const listSchedules = async () => {
const getSchedule = async (scheduleId: number) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
where: eq(volumesTable.id, scheduleId),
where: eq(backupSchedulesTable.id, scheduleId),
with: {
volume: true,
repository: true,
@@ -63,6 +63,14 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
throw new BadRequestError("Invalid cron expression");
}
const existingName = await db.query.backupSchedulesTable.findFirst({
where: eq(backupSchedulesTable.name, data.name),
});
if (existingName) {
throw new ConflictError("A backup schedule with this name already exists");
}
const volume = await db.query.volumesTable.findFirst({
where: eq(volumesTable.id, data.volumeId),
});
@@ -84,6 +92,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
const [newSchedule] = await db
.insert(backupSchedulesTable)
.values({
name: data.name,
volumeId: data.volumeId,
repositoryId: data.repositoryId,
enabled: data.enabled,
@@ -115,6 +124,16 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
throw new BadRequestError("Invalid cron expression");
}
if (data.name) {
const existingName = await db.query.backupSchedulesTable.findFirst({
where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)),
});
if (existingName) {
throw new ConflictError("A backup schedule with this name already exists");
}
}
const repository = await db.query.repositoriesTable.findFirst({
where: eq(repositoriesTable.id, data.repositoryId),
});

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { throttle } from "es-toolkit";
import { type } from "arktype";
import { $ } from "bun";
@@ -261,8 +262,9 @@ const backup = async (
let includeFile: string | null = null;
if (options?.include && options.include.length > 0) {
const tmp = await fs.mkdtemp("restic-include");
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-"));
includeFile = path.join(tmp, `include.txt`);
const includePaths = options.include.map((p) => path.join(source, p));
await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8");