chore: pr feedbacks

This commit is contained in:
Nicolas Meienberger
2025-12-03 20:24:41 +01:00
parent 16b8be2cd9
commit fc6f628dd4
8 changed files with 994 additions and 181 deletions

View File

@@ -73,7 +73,7 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
}, [compatibility]); }, [compatibility]);
useEffect(() => { useEffect(() => {
if (currentMirrors) { if (currentMirrors && !hasChanges) {
const map = new Map<string, MirrorAssignment>(); const map = new Map<string, MirrorAssignment>();
for (const mirror of currentMirrors) { for (const mirror of currentMirrors) {
map.set(mirror.repositoryId, { map.set(mirror.repositoryId, {
@@ -87,7 +87,7 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
setAssignments(map); setAssignments(map);
} }
}, [currentMirrors]); }, [currentMirrors, hasChanges]);
const addRepository = (repositoryId: string) => { const addRepository = (repositoryId: string) => {
const newAssignments = new Map(assignments); 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 = const selectableRepositories =
repositories?.filter((r) => { repositories?.filter((r) => {
if (r.id === primaryRepositoryId) return false; if (r.id === primaryRepositoryId) return false;
@@ -172,16 +168,16 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
}); });
const assignedRepositories = Array.from(assignments.keys()) const assignedRepositories = Array.from(assignments.keys())
.map((id) => getRepositoryById(id)) .map((id) => repositories?.find((r) => r.id === id))
.filter((r) => r !== undefined); .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 === "success") return "success";
if (status === "error") return "error"; if (status === "error") return "error";
return "neutral"; return "neutral";
}; };
const getStatusLabel = (assignment: MirrorAssignment): string => { const getStatusLabel = (assignment: MirrorAssignment) => {
if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) { if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) {
return assignment.lastCopyError; return assignment.lastCopyError;
} }

View File

@@ -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 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 DROP TABLE `app_metadata`;--> statement-breakpoint
ALTER TABLE `__new_app_metadata` RENAME TO `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` ( CREATE TABLE `__new_backup_schedule_notifications_table` (
`schedule_id` integer NOT NULL, `schedule_id` integer NOT NULL,
`destination_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 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_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`); CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);
PRAGMA foreign_keys=ON;--> statement-breakpoint

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`);

View File

@@ -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": {}
}
}

View File

@@ -1,139 +1,146 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"entries": [ "entries": [
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1755765658194, "when": 1755765658194,
"tag": "0000_known_madelyne_pryor", "tag": "0000_known_madelyne_pryor",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1755775437391, "when": 1755775437391,
"tag": "0001_far_frank_castle", "tag": "0001_far_frank_castle",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "6", "version": "6",
"when": 1756930554198, "when": 1756930554198,
"tag": "0002_cheerful_randall", "tag": "0002_cheerful_randall",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "6", "version": "6",
"when": 1758653407064, "when": 1758653407064,
"tag": "0003_mature_hellcat", "tag": "0003_mature_hellcat",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 4, "idx": 4,
"version": "6", "version": "6",
"when": 1758961535488, "when": 1758961535488,
"tag": "0004_wealthy_tomas", "tag": "0004_wealthy_tomas",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 5, "idx": 5,
"version": "6", "version": "6",
"when": 1759416698274, "when": 1759416698274,
"tag": "0005_simple_alice", "tag": "0005_simple_alice",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 6, "idx": 6,
"version": "6", "version": "6",
"when": 1760734377440, "when": 1760734377440,
"tag": "0006_secret_micromacro", "tag": "0006_secret_micromacro",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 7, "idx": 7,
"version": "6", "version": "6",
"when": 1761224911352, "when": 1761224911352,
"tag": "0007_watery_sersi", "tag": "0007_watery_sersi",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 8, "idx": 8,
"version": "6", "version": "6",
"when": 1761414054481, "when": 1761414054481,
"tag": "0008_silent_lady_bullseye", "tag": "0008_silent_lady_bullseye",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 9, "idx": 9,
"version": "6", "version": "6",
"when": 1762095226041, "when": 1762095226041,
"tag": "0009_little_adam_warlock", "tag": "0009_little_adam_warlock",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 10, "idx": 10,
"version": "6", "version": "6",
"when": 1762610065889, "when": 1762610065889,
"tag": "0010_perfect_proemial_gods", "tag": "0010_perfect_proemial_gods",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 11, "idx": 11,
"version": "6", "version": "6",
"when": 1763644043601, "when": 1763644043601,
"tag": "0011_familiar_stone_men", "tag": "0011_familiar_stone_men",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 12, "idx": 12,
"version": "6", "version": "6",
"when": 1764100562084, "when": 1764100562084,
"tag": "0012_add_short_ids", "tag": "0012_add_short_ids",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 13, "idx": 13,
"version": "6", "version": "6",
"when": 1764182159797, "when": 1764182159797,
"tag": "0013_elite_sprite", "tag": "0013_elite_sprite",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 14, "idx": 14,
"version": "6", "version": "6",
"when": 1764182405089, "when": 1764182405089,
"tag": "0014_wild_echo", "tag": "0014_wild_echo",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 15, "idx": 15,
"version": "6", "version": "6",
"when": 1764182465287, "when": 1764182465287,
"tag": "0015_jazzy_sersi", "tag": "0015_jazzy_sersi",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 16, "idx": 16,
"version": "6", "version": "6",
"when": 1764194697035, "when": 1764194697035,
"tag": "0016_fix-timestamps-to-ms", "tag": "0016_fix-timestamps-to-ms",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 17, "idx": 17,
"version": "6", "version": "6",
"when": 1764357897219, "when": 1764357897219,
"tag": "0017_fix-compression-modes", "tag": "0017_fix-compression-modes",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 18, "idx": 18,
"version": "6", "version": "6",
"when": 1764619898949, "when": 1764619898949,
"tag": "0018_bizarre_zzzax", "tag": "0018_bizarre_zzzax",
"breakpoints": true "breakpoints": true
} },
] {
} "idx": 19,
"version": "6",
"when": 1764790151212,
"tag": "0019_heavy_shen",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,5 @@
import { relations, sql } from "drizzle-orm"; 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 { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
@@ -160,20 +160,24 @@ export type BackupScheduleNotification = typeof backupScheduleNotificationsTable
* Backup Schedule Mirrors Junction Table (Many-to-Many) * Backup Schedule Mirrors Junction Table (Many-to-Many)
* Allows copying snapshots to secondary repositories after backup completes * Allows copying snapshots to secondary repositories after backup completes
*/ */
export const backupScheduleMirrorsTable = sqliteTable("backup_schedule_mirrors_table", { export const backupScheduleMirrorsTable = sqliteTable(
id: int().primaryKey({ autoIncrement: true }), "backup_schedule_mirrors_table",
scheduleId: int("schedule_id") {
.notNull() id: int().primaryKey({ autoIncrement: true }),
.references(() => backupSchedulesTable.id, { onDelete: "cascade" }), scheduleId: int("schedule_id")
repositoryId: text("repository_id") .notNull()
.notNull() .references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
.references(() => repositoriesTable.id, { onDelete: "cascade" }), repositoryId: text("repository_id")
enabled: int("enabled", { mode: "boolean" }).notNull().default(true), .notNull()
lastCopyAt: int("last_copy_at", { mode: "number" }), .references(() => repositoriesTable.id, { onDelete: "cascade" }),
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(), enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
lastCopyError: text("last_copy_error"), lastCopyAt: int("last_copy_at", { mode: "number" }),
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), 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 }) => ({ export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({
schedule: one(backupSchedulesTable, { schedule: one(backupSchedulesTable, {

View File

@@ -522,13 +522,25 @@ const copyToMirrors = async (
repositoryName: mirror.repository.name, repositoryName: mirror.repository.name,
}); });
await restic.copy(sourceRepository.config, mirror.repository.config, { const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`);
tag: scheduleId.toString(), 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) { if (retentionPolicy) {
logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`); const releaseForget = await repoMutex.acquireExclusive(mirror.repository.id, `forget:mirror:${scheduleId}`);
await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() });
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 await db

View File

@@ -202,7 +202,7 @@ const init = async (config: RepositoryConfig) => {
const env = await buildEnv(config); const env = await buildEnv(config);
const args = ["init", "--repo", repoUrl]; const args = ["init", "--repo", repoUrl];
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -278,7 +278,7 @@ const backup = async (
} }
} }
addCommonArgs(args, config, env); addCommonArgs(args, env);
const logData = throttle((data: string) => { const logData = throttle((data: string) => {
logger.info(data.trim()); logger.info(data.trim());
@@ -404,7 +404,7 @@ const restore = async (
} }
} }
addCommonArgs(args, config, env); addCommonArgs(args, env);
logger.debug(`Executing: restic ${args.join(" ")}`); logger.debug(`Executing: restic ${args.join(" ")}`);
const res = await $`restic ${args}`.env(env).nothrow(); 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(); const res = await $`restic ${args}`.env(env).nothrow().quiet();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -516,7 +516,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
} }
args.push("--prune"); args.push("--prune");
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -534,7 +534,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
const env = await buildEnv(config); const env = await buildEnv(config);
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"]; const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -584,7 +584,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
args.push(path); args.push(path);
} }
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await safeSpawn({ command: "restic", args, env }); const res = await safeSpawn({ command: "restic", args, env });
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -635,7 +635,7 @@ const unlock = async (config: RepositoryConfig) => {
const env = await buildEnv(config); const env = await buildEnv(config);
const args = ["unlock", "--repo", repoUrl, "--remove-all"]; const args = ["unlock", "--repo", repoUrl, "--remove-all"];
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -659,7 +659,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
args.push("--read-data"); args.push("--read-data");
} }
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -693,7 +693,7 @@ const repairIndex = async (config: RepositoryConfig) => {
const env = await buildEnv(config); const env = await buildEnv(config);
const args = ["repair", "index", "--repo", repoUrl]; const args = ["repair", "index", "--repo", repoUrl];
addCommonArgs(args, config, env); addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow(); const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env); await cleanupTemporaryKeys(config, env);
@@ -746,7 +746,7 @@ const copy = async (
args.push("latest"); args.push("latest");
} }
addCommonArgs(args, destConfig, destEnv); addCommonArgs(args, env);
if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) {
args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`);
@@ -785,9 +785,10 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string
} }
}; };
const addCommonArgs = (args: string[], config: RepositoryConfig, env: Record<string, string>) => { const addCommonArgs = (args: string[], env: Record<string, string>) => {
args.push("--retry-lock", "1m", "--json"); 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}`); args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
} }
}; };