From fc6f628dd4de805766936a200b9bbad649fbc5a1 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 3 Dec 2025 20:24:41 +0100 Subject: [PATCH] chore: pr feedbacks --- .../components/schedule-mirrors-config.tsx | 14 +- app/drizzle/0018_bizarre_zzzax.sql | 2 +- app/drizzle/0019_heavy_shen.sql | 1 + app/drizzle/meta/0019_snapshot.json | 792 ++++++++++++++++++ app/drizzle/meta/_journal.json | 283 ++++--- app/server/db/schema.ts | 34 +- app/server/modules/backups/backups.service.ts | 22 +- app/server/utils/restic.ts | 27 +- 8 files changed, 994 insertions(+), 181 deletions(-) create mode 100644 app/drizzle/0019_heavy_shen.sql create mode 100644 app/drizzle/meta/0019_snapshot.json diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx index 1abbde2..419f5e0 100644 --- a/app/client/modules/backups/components/schedule-mirrors-config.tsx +++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx @@ -73,7 +73,7 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit }, [compatibility]); useEffect(() => { - if (currentMirrors) { + if (currentMirrors && !hasChanges) { const map = new Map(); for (const mirror of currentMirrors) { map.set(mirror.repositoryId, { @@ -87,7 +87,7 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit setAssignments(map); } - }, [currentMirrors]); + }, [currentMirrors, hasChanges]); const addRepository = (repositoryId: string) => { const newAssignments = new Map(assignments); @@ -155,10 +155,6 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit } }; - const getRepositoryById = (id: string) => { - return repositories?.find((r) => r.id === id); - }; - const selectableRepositories = repositories?.filter((r) => { if (r.id === primaryRepositoryId) return false; @@ -172,16 +168,16 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit }); const assignedRepositories = Array.from(assignments.keys()) - .map((id) => getRepositoryById(id)) + .map((id) => repositories?.find((r) => r.id === id)) .filter((r) => r !== undefined); - const getStatusVariant = (status: "success" | "error" | null): "success" | "error" | "neutral" => { + const getStatusVariant = (status: "success" | "error" | null) => { if (status === "success") return "success"; if (status === "error") return "error"; return "neutral"; }; - const getStatusLabel = (assignment: MirrorAssignment): string => { + const getStatusLabel = (assignment: MirrorAssignment) => { if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) { return assignment.lastCopyError; } diff --git a/app/drizzle/0018_bizarre_zzzax.sql b/app/drizzle/0018_bizarre_zzzax.sql index aeabbe0..0b72b21 100644 --- a/app/drizzle/0018_bizarre_zzzax.sql +++ b/app/drizzle/0018_bizarre_zzzax.sql @@ -23,7 +23,6 @@ CREATE TABLE `__new_app_metadata` ( INSERT INTO `__new_app_metadata`("key", "value", "created_at", "updated_at") SELECT "key", "value", "created_at", "updated_at" FROM `app_metadata`;--> statement-breakpoint DROP TABLE `app_metadata`;--> statement-breakpoint ALTER TABLE `__new_app_metadata` RENAME TO `app_metadata`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint CREATE TABLE `__new_backup_schedule_notifications_table` ( `schedule_id` integer NOT NULL, `destination_id` integer NOT NULL, @@ -137,3 +136,4 @@ DROP TABLE `volumes_table`;--> statement-breakpoint ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`); +PRAGMA foreign_keys=ON;--> statement-breakpoint diff --git a/app/drizzle/0019_heavy_shen.sql b/app/drizzle/0019_heavy_shen.sql new file mode 100644 index 0000000..98b747b --- /dev/null +++ b/app/drizzle/0019_heavy_shen.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`); \ No newline at end of file diff --git a/app/drizzle/meta/0019_snapshot.json b/app/drizzle/meta/0019_snapshot.json new file mode 100644 index 0000000..ff1f037 --- /dev/null +++ b/app/drizzle/meta/0019_snapshot.json @@ -0,0 +1,792 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dedfb246-68e7-4590-af52-6476eb2999d1", + "prevId": "121ef03c-eb5a-4b97-b2f1-4add6adfb080", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "columns": [ + "schedule_id", + "repository_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": [ + "schedule_id", + "destination_id" + ], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index 7625c24..4ae3a51 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,139 +1,146 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1755765658194, - "tag": "0000_known_madelyne_pryor", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1755775437391, - "tag": "0001_far_frank_castle", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1756930554198, - "tag": "0002_cheerful_randall", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1758653407064, - "tag": "0003_mature_hellcat", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1758961535488, - "tag": "0004_wealthy_tomas", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1759416698274, - "tag": "0005_simple_alice", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1760734377440, - "tag": "0006_secret_micromacro", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1761224911352, - "tag": "0007_watery_sersi", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1761414054481, - "tag": "0008_silent_lady_bullseye", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1762095226041, - "tag": "0009_little_adam_warlock", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1762610065889, - "tag": "0010_perfect_proemial_gods", - "breakpoints": true - }, - { - "idx": 11, - "version": "6", - "when": 1763644043601, - "tag": "0011_familiar_stone_men", - "breakpoints": true - }, - { - "idx": 12, - "version": "6", - "when": 1764100562084, - "tag": "0012_add_short_ids", - "breakpoints": true - }, - { - "idx": 13, - "version": "6", - "when": 1764182159797, - "tag": "0013_elite_sprite", - "breakpoints": true - }, - { - "idx": 14, - "version": "6", - "when": 1764182405089, - "tag": "0014_wild_echo", - "breakpoints": true - }, - { - "idx": 15, - "version": "6", - "when": 1764182465287, - "tag": "0015_jazzy_sersi", - "breakpoints": true - }, - { - "idx": 16, - "version": "6", - "when": 1764194697035, - "tag": "0016_fix-timestamps-to-ms", - "breakpoints": true - }, - { - "idx": 17, - "version": "6", - "when": 1764357897219, - "tag": "0017_fix-compression-modes", - "breakpoints": true - }, - { - "idx": 18, - "version": "6", - "when": 1764619898949, - "tag": "0018_bizarre_zzzax", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1755765658194, + "tag": "0000_known_madelyne_pryor", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755775437391, + "tag": "0001_far_frank_castle", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1756930554198, + "tag": "0002_cheerful_randall", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1758653407064, + "tag": "0003_mature_hellcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1758961535488, + "tag": "0004_wealthy_tomas", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1759416698274, + "tag": "0005_simple_alice", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1760734377440, + "tag": "0006_secret_micromacro", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1761224911352, + "tag": "0007_watery_sersi", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1761414054481, + "tag": "0008_silent_lady_bullseye", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1762095226041, + "tag": "0009_little_adam_warlock", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1762610065889, + "tag": "0010_perfect_proemial_gods", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1763644043601, + "tag": "0011_familiar_stone_men", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1764100562084, + "tag": "0012_add_short_ids", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1764182159797, + "tag": "0013_elite_sprite", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1764182405089, + "tag": "0014_wild_echo", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1764182465287, + "tag": "0015_jazzy_sersi", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1764194697035, + "tag": "0016_fix-timestamps-to-ms", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1764357897219, + "tag": "0017_fix-compression-modes", + "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1764619898949, + "tag": "0018_bizarre_zzzax", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764790151212, + "tag": "0019_heavy_shen", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index f2452de..6ef9037 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,5 +1,5 @@ import { relations, sql } from "drizzle-orm"; -import { int, integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"; +import { int, integer, sqliteTable, text, primaryKey, uniqueIndex, unique } from "drizzle-orm/sqlite-core"; import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; @@ -160,20 +160,24 @@ export type BackupScheduleNotification = typeof backupScheduleNotificationsTable * Backup Schedule Mirrors Junction Table (Many-to-Many) * Allows copying snapshots to secondary repositories after backup completes */ -export const backupScheduleMirrorsTable = sqliteTable("backup_schedule_mirrors_table", { - id: int().primaryKey({ autoIncrement: true }), - scheduleId: int("schedule_id") - .notNull() - .references(() => backupSchedulesTable.id, { onDelete: "cascade" }), - repositoryId: text("repository_id") - .notNull() - .references(() => repositoriesTable.id, { onDelete: "cascade" }), - enabled: int("enabled", { mode: "boolean" }).notNull().default(true), - lastCopyAt: int("last_copy_at", { mode: "number" }), - lastCopyStatus: text("last_copy_status").$type<"success" | "error">(), - lastCopyError: text("last_copy_error"), - createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), -}); +export const backupScheduleMirrorsTable = sqliteTable( + "backup_schedule_mirrors_table", + { + id: int().primaryKey({ autoIncrement: true }), + scheduleId: int("schedule_id") + .notNull() + .references(() => backupSchedulesTable.id, { onDelete: "cascade" }), + repositoryId: text("repository_id") + .notNull() + .references(() => repositoriesTable.id, { onDelete: "cascade" }), + enabled: int("enabled", { mode: "boolean" }).notNull().default(true), + lastCopyAt: int("last_copy_at", { mode: "number" }), + lastCopyStatus: text("last_copy_status").$type<"success" | "error">(), + lastCopyError: text("last_copy_error"), + createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), + }, + (table) => [unique().on(table.scheduleId, table.repositoryId)], +); export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({ schedule: one(backupSchedulesTable, { diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index b60e827..df9e42b 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -522,13 +522,25 @@ const copyToMirrors = async ( repositoryName: mirror.repository.name, }); - await restic.copy(sourceRepository.config, mirror.repository.config, { - tag: scheduleId.toString(), - }); + const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`); + const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`); + + try { + await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() }); + } finally { + releaseSource(); + releaseMirror(); + } if (retentionPolicy) { - logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`); - await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() }); + const releaseForget = await repoMutex.acquireExclusive(mirror.repository.id, `forget:mirror:${scheduleId}`); + + try { + logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`); + await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() }); + } finally { + releaseForget(); + } } await db diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 45136ff..84eaf08 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -202,7 +202,7 @@ const init = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["init", "--repo", repoUrl]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -278,7 +278,7 @@ const backup = async ( } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const logData = throttle((data: string) => { logger.info(data.trim()); @@ -404,7 +404,7 @@ const restore = async ( } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); logger.debug(`Executing: restic ${args.join(" ")}`); const res = await $`restic ${args}`.env(env).nothrow(); @@ -467,7 +467,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow().quiet(); await cleanupTemporaryKeys(config, env); @@ -516,7 +516,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: } args.push("--prune"); - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -534,7 +534,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => { const env = await buildEnv(config); const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -584,7 +584,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) = args.push(path); } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(config, env); @@ -635,7 +635,7 @@ const unlock = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["unlock", "--repo", repoUrl, "--remove-all"]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -659,7 +659,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean }) args.push("--read-data"); } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -693,7 +693,7 @@ const repairIndex = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["repair", "index", "--repo", repoUrl]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -746,7 +746,7 @@ const copy = async ( args.push("latest"); } - addCommonArgs(args, destConfig, destEnv); + addCommonArgs(args, env); if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); @@ -785,9 +785,10 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { +const addCommonArgs = (args: string[], env: Record) => { args.push("--retry-lock", "1m", "--json"); - if (config.backend === "sftp" && env._SFTP_SSH_ARGS) { + + if (env._SFTP_SSH_ARGS) { args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`); } };