diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index b45285ea..d85c5eee 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1390,6 +1390,7 @@ export type ListBackupSchedulesResponses = { keepWithinDuration?: string; keepYearly?: number; } | null; + shortId: string; updatedAt: number; volume: { autoRemount: boolean; @@ -1500,6 +1501,7 @@ export type CreateBackupScheduleResponses = { keepWithinDuration?: string; keepYearly?: number; } | null; + shortId: string; updatedAt: number; volumeId: number; }; @@ -1637,6 +1639,7 @@ export type GetBackupScheduleResponses = { keepWithinDuration?: string; keepYearly?: number; } | null; + shortId: string; updatedAt: number; volume: { autoRemount: boolean; @@ -1748,6 +1751,7 @@ export type UpdateBackupScheduleResponses = { keepWithinDuration?: string; keepYearly?: number; } | null; + shortId: string; updatedAt: number; volumeId: number; }; @@ -1865,6 +1869,7 @@ export type GetBackupScheduleForVolumeResponses = { keepWithinDuration?: string; keepYearly?: number; } | null; + shortId: string; updatedAt: number; volume: { autoRemount: boolean; diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 97a934b6..dfee2792 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -85,8 +85,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { {snapshots.map((snapshot) => { - const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag)); - const backup = backups.find((b) => backupIds.includes(b.id)); + const backup = backups.find((b) => snapshot.tags.includes(b.shortId)); return ( { if (!value.data) return null; - const backupIds = value.data.tags.map(Number).filter((tag) => !Number.isNaN(tag)); - const backupSchedule = schedules.data?.find((s) => backupIds.includes(s.id)); + const backupSchedule = schedules.data?.find((s) => value.data.tags.includes(s.shortId)); return ( <> diff --git a/app/client/modules/repositories/tabs/snapshots.tsx b/app/client/modules/repositories/tabs/snapshots.tsx index ea36cba4..dea2a5d8 100644 --- a/app/client/modules/repositories/tabs/snapshots.tsx +++ b/app/client/modules/repositories/tabs/snapshots.tsx @@ -29,8 +29,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => { if (!searchQuery) return true; const searchLower = searchQuery.toLowerCase(); - const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag)); - const backup = schedules.data?.find((b) => backupIds.includes(b.id)); + const backup = schedules.data?.find((b) => snapshot.tags.includes(b.shortId)); return ( snapshot.short_id.toLowerCase().includes(searchLower) || diff --git a/app/drizzle/0027_careful_cammi.sql b/app/drizzle/0027_careful_cammi.sql new file mode 100644 index 00000000..7bc6ccb4 --- /dev/null +++ b/app/drizzle/0027_careful_cammi.sql @@ -0,0 +1,3 @@ +ALTER TABLE `backup_schedules_table` ADD `short_id` text;--> statement-breakpoint +UPDATE `backup_schedules_table` SET `short_id` = lower(hex(randomblob(4))) WHERE `short_id` IS NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`); diff --git a/app/drizzle/0028_third_amazoness.sql b/app/drizzle/0028_third_amazoness.sql new file mode 100644 index 00000000..8851dea1 --- /dev/null +++ b/app/drizzle/0028_third_amazoness.sql @@ -0,0 +1,31 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_backup_schedules_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `volume_id` integer NOT NULL, + `repository_id` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `cron_expression` text NOT NULL, + `retention_policy` text, + `exclude_patterns` text DEFAULT '[]', + `exclude_if_present` text DEFAULT '[]', + `include_patterns` text DEFAULT '[]', + `last_backup_at` integer, + `last_backup_status` text, + `last_backup_error` text, + `next_backup_at` integer, + `one_file_system` integer DEFAULT false NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_backup_schedules_table`("id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at") SELECT "id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at" FROM `backup_schedules_table`;--> statement-breakpoint +DROP TABLE `backup_schedules_table`;--> statement-breakpoint +ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`); \ No newline at end of file diff --git a/app/drizzle/meta/0027_snapshot.json b/app/drizzle/meta/0027_snapshot.json new file mode 100644 index 00000000..6d2b4be7 --- /dev/null +++ b/app/drizzle/meta/0027_snapshot.json @@ -0,0 +1,846 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "94ebc8c3-f11f-48cb-9814-d8d550422d26", + "prevId": "19421265-4e3a-46b8-9ca1-a01d1e293dbc", + "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_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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 + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "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": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "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 + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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": { + "backup_schedules_table_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "backup_schedules_table_name_unique": { + "name": "backup_schedules_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "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 + } + }, + "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/0028_snapshot.json b/app/drizzle/meta/0028_snapshot.json new file mode 100644 index 00000000..e42b2c1b --- /dev/null +++ b/app/drizzle/meta/0028_snapshot.json @@ -0,0 +1,846 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2837bed4-34fb-4d16-b331-7b6d483979bc", + "prevId": "94ebc8c3-f11f-48cb-9814-d8d550422d26", + "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_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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 + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "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": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "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 + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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": { + "backup_schedules_table_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "backup_schedules_table_name_unique": { + "name": "backup_schedules_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "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 + } + }, + "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 8e10286e..a8daef0a 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -190,6 +190,20 @@ "when": 1766765013108, "tag": "0026_migrate-local-repo-paths", "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1766778073418, + "tag": "0027_careful_cammi", + "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1766778162985, + "tag": "0028_third_amazoness", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/server/core/constants.ts b/app/server/core/constants.ts index 98a38724..e0134cb1 100644 --- a/app/server/core/constants.ts +++ b/app/server/core/constants.ts @@ -6,4 +6,4 @@ export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass"; export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE]; -export const REQUIRED_MIGRATIONS = ["v0.14.0"]; +export const REQUIRED_MIGRATIONS = ["v0.21.0"]; diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 295a7c1e..61c82982 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -69,6 +69,7 @@ export type RepositoryInsert = typeof repositoriesTable.$inferInsert; */ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { id: int().primaryKey({ autoIncrement: true }), + shortId: text("short_id").notNull().unique(), name: text().notNull().unique(), volumeId: int("volume_id") .notNull() diff --git a/app/server/index.ts b/app/server/index.ts index 5e290a63..2027a223 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -1,7 +1,7 @@ import { createHonoServer } from "react-router-hono-server/bun"; import { runDbMigrations } from "./db/db"; import { startup } from "./modules/lifecycle/startup"; -import { migrateToShortIds } from "./modules/lifecycle/migration"; +import { retagSnapshots } from "./modules/lifecycle/migration"; import { logger } from "./utils/logger"; import { shutdown } from "./modules/lifecycle/shutdown"; import { REQUIRED_MIGRATIONS } from "./core/constants"; @@ -13,7 +13,7 @@ const app = createApp(); runDbMigrations(); -await migrateToShortIds(); +await retagSnapshots(); await validateRequiredMigrations(REQUIRED_MIGRATIONS); startup(); diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 1d6c14c1..fa21cfa5 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -17,6 +17,7 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer; const backupScheduleSchema = type({ id: "number", + shortId: "string", name: "string", volumeId: "number", repositoryId: "string", diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 120dc7cc..ea2b5fe5 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -14,6 +14,7 @@ import { notificationsService } from "../notifications/notifications.service"; import { repoMutex } from "../../core/repository-mutex"; import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility"; import path from "node:path"; +import { generateShortId } from "~/server/utils/id"; const runningBackups = new Map(); @@ -126,6 +127,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { includePatterns: data.includePatterns ?? [], oneFileSystem: data.oneFileSystem, nextBackupAt: nextBackupAt, + shortId: generateShortId(), }) .returning(); @@ -277,7 +279,7 @@ const executeBackup = async (scheduleId: number, manual = false) => { oneFileSystem?: boolean; signal?: AbortSignal; } = { - tags: [schedule.id.toString()], + tags: [schedule.shortId], oneFileSystem: schedule.oneFileSystem, signal: abortController.signal, }; @@ -476,7 +478,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => { logger.info(`running retention policy (forget) for schedule ${scheduleId}`); const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:${scheduleId}`); try { - await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() }); + await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.shortId }); } finally { releaseLock(); } @@ -570,6 +572,14 @@ const copyToMirrors = async ( sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] }, retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"], ) => { + const schedule = await db.query.backupSchedulesTable.findFirst({ + where: eq(backupSchedulesTable.id, scheduleId), + }); + + if (!schedule) { + throw new NotFoundError("Backup schedule not found"); + } + const mirrors = await db.query.backupScheduleMirrorsTable.findMany({ where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId), with: { repository: true }, @@ -599,7 +609,7 @@ const copyToMirrors = async ( const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`); try { - await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() }); + await restic.copy(sourceRepository.config, mirror.repository.config, { tag: schedule.shortId }); } finally { releaseSource(); releaseMirror(); diff --git a/app/server/modules/lifecycle/migration.ts b/app/server/modules/lifecycle/migration.ts index 3cf1eed1..32fb6a52 100644 --- a/app/server/modules/lifecycle/migration.ts +++ b/app/server/modules/lifecycle/migration.ts @@ -1,14 +1,13 @@ -import * as fs from "node:fs/promises"; -import * as path from "node:path"; import { eq } from "drizzle-orm"; import { db } from "../../db/db"; import { repositoriesTable } from "../../db/schema"; -import { VOLUME_MOUNT_BASE, REPOSITORY_BASE } from "../../core/constants"; import { logger } from "../../utils/logger"; import { hasMigrationCheckpoint, recordMigrationCheckpoint } from "./checkpoint"; -import type { RepositoryConfig } from "~/schemas/restic"; +import { toMessage } from "~/server/utils/errors"; +import { safeSpawn } from "~/server/utils/spawn"; +import { addCommonArgs, buildEnv, buildRepoUrl, cleanupTemporaryKeys } from "~/server/utils/restic"; -const MIGRATION_VERSION = "v0.14.0"; +const MIGRATION_VERSION = "v0.21.0"; interface MigrationResult { success: boolean; @@ -28,19 +27,17 @@ export class MigrationError extends Error { } } -export const migrateToShortIds = async () => { +export const retagSnapshots = async () => { const alreadyMigrated = await hasMigrationCheckpoint(MIGRATION_VERSION); if (alreadyMigrated) { logger.debug(`Migration ${MIGRATION_VERSION} already completed, skipping.`); return; } - logger.info(`Starting short ID migration (${MIGRATION_VERSION})...`); + logger.info(`Starting snapshots retagging migration (${MIGRATION_VERSION})...`); - const volumeResult = await migrateVolumeFolders(); - const repoResult = await migrateRepositoryFolders(); - - const allErrors = [...volumeResult.errors, ...repoResult.errors]; + const result = await migrateSnapshotsToShortIdTag(); + const allErrors = [...result.errors]; if (allErrors.length > 0) { for (const err of allErrors) { @@ -51,148 +48,45 @@ export const migrateToShortIds = async () => { await recordMigrationCheckpoint(MIGRATION_VERSION); - logger.info(`Short ID migration (${MIGRATION_VERSION}) complete.`); + logger.info(`Snapshots retagging migration (${MIGRATION_VERSION}) complete.`); }; -const migrateVolumeFolders = async (): Promise => { +const migrateSnapshotsToShortIdTag = async (): Promise => { const errors: Array<{ name: string; error: string }> = []; - const volumes = await db.query.volumesTable.findMany({}); + const backupSchedules = await db.query.backupSchedulesTable.findMany({}); - for (const volume of volumes) { - if (volume.config.backend === "directory") { + for (const schedule of backupSchedules) { + const oldTag = schedule.id.toString(); + const newTag = schedule.shortId; + + const repository = await db.query.repositoriesTable.findFirst({ + where: eq(repositoriesTable.id, schedule.repositoryId), + }); + + if (!repository) { + errors.push({ name: `schedule:${schedule.name}`, error: `Associated repository not found` }); continue; } - const oldPath = path.join(VOLUME_MOUNT_BASE, volume.name); - const newPath = path.join(VOLUME_MOUNT_BASE, volume.shortId); + const repoUrl = buildRepoUrl(repository.config); + const env = await buildEnv(repository.config); - const oldExists = await pathExists(oldPath); - const newExists = await pathExists(newPath); + const args = ["--repo", repoUrl, "tag", "--tag", oldTag, "--add", newTag, "--remove", oldTag]; - if (oldExists && !newExists) { - try { - logger.info(`Migrating volume folder: ${oldPath} -> ${newPath}`); - await fs.rename(oldPath, newPath); - logger.info(`Successfully migrated volume folder for "${volume.name}"`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - errors.push({ name: `volume:${volume.name}`, error: errorMessage }); - } - } else if (oldExists && newExists) { - logger.warn( - `Both old (${oldPath}) and new (${newPath}) paths exist for volume "${volume.name}". Manual intervention may be required.`, - ); + addCommonArgs(args, env); + + logger.info(`Migrating snapshots for schedule '${schedule.name}' from tag '${oldTag}' to '${newTag}'`); + const res = await safeSpawn({ command: "restic", args, env }); + await cleanupTemporaryKeys(repository.config, env); + + if (res.exitCode !== 0) { + logger.error(`Restic tag failed: ${res.stderr}`); + errors.push({ name: `schedule:${schedule.name}`, error: `Restic tag command failed: ${toMessage(res.stderr)}` }); + continue; } + + logger.info(`Migrated snapshots for schedule '${schedule.name}' from tag '${oldTag}' to '${newTag}'`); } return { success: errors.length === 0, errors }; }; - -const migrateRepositoryFolders = async (): Promise => { - const errors: Array<{ name: string; error: string }> = []; - const repositories = await db.query.repositoriesTable.findMany({}); - - for (const repo of repositories) { - if (repo.config.backend !== "local") { - continue; - } - - const config = repo.config as Extract; - - if (config.isExistingRepository) { - logger.debug(`Skipping imported repository "${repo.name}" - folder path is user-defined`); - continue; - } - - if (config.name === repo.shortId) { - continue; - } - - const basePath = config.path || REPOSITORY_BASE; - const oldPath = path.join(basePath, config.name); - const newPath = path.join(basePath, repo.shortId); - - const oldExists = await pathExists(oldPath); - const newExists = await pathExists(newPath); - - if (oldExists && !newExists) { - try { - logger.info(`Migrating repository folder: ${oldPath} -> ${newPath}`); - await fs.rename(oldPath, newPath); - - const updatedConfig: RepositoryConfig = { - ...config, - name: repo.shortId, - }; - - await db - .update(repositoriesTable) - .set({ - config: updatedConfig, - updatedAt: Date.now(), - }) - .where(eq(repositoriesTable.id, repo.id)); - - logger.info(`Successfully migrated repository folder and config for "${repo.name}"`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - errors.push({ name: `repository:${repo.name}`, error: errorMessage }); - } - } else if (oldExists && newExists) { - logger.warn( - `Both old (${oldPath}) and new (${newPath}) paths exist for repository "${repo.name}". Manual intervention may be required.`, - ); - } else if (!oldExists && !newExists) { - try { - logger.info(`Updating config.name for repository "${repo.name}" (no folder exists yet)`); - - const updatedConfig: RepositoryConfig = { - ...config, - name: repo.shortId, - }; - - await db - .update(repositoriesTable) - .set({ - config: updatedConfig, - updatedAt: Date.now(), - }) - .where(eq(repositoriesTable.id, repo.id)); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - errors.push({ name: `repository:${repo.name}`, error: errorMessage }); - } - } else if (newExists && !oldExists && config.name !== repo.shortId) { - try { - logger.info(`Folder already at new path, updating config.name for repository "${repo.name}"`); - - const updatedConfig: RepositoryConfig = { - ...config, - name: repo.shortId, - }; - - await db - .update(repositoriesTable) - .set({ - config: updatedConfig, - updatedAt: Date.now(), - }) - .where(eq(repositoriesTable.id, repo.id)); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - errors.push({ name: `repository:${repo.name}`, error: errorMessage }); - } - } - } - - return { success: errors.length === 0, errors }; -}; - -const pathExists = async (p: string): Promise => { - try { - await fs.access(p); - return true; - } catch { - return false; - } -}; diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index 561db566..23f16a08 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -161,7 +161,7 @@ const listSnapshots = async (id: string, backupId?: string) => { let snapshots = []; if (backupId) { - snapshots = await restic.snapshots(repository.config, { tags: [backupId.toString()] }); + snapshots = await restic.snapshots(repository.config, { tags: [backupId] }); } else { snapshots = await restic.snapshots(repository.config); } diff --git a/app/server/utils/id.ts b/app/server/utils/id.ts index 7104e114..18bc2030 100644 --- a/app/server/utils/id.ts +++ b/app/server/utils/id.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; -export const generateShortId = (length = 5): string => { +export const generateShortId = (length = 8): string => { const bytesNeeded = Math.ceil((length * 3) / 4); - return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length).toLowerCase(); + return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length); }; diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index ec975ff5..fdf705cb 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -72,7 +72,7 @@ const ensurePassfile = async () => { } }; -const buildRepoUrl = (config: RepositoryConfig): string => { +export const buildRepoUrl = (config: RepositoryConfig): string => { switch (config.backend) { case "local": if (config.isExistingRepository) { @@ -105,7 +105,7 @@ const buildRepoUrl = (config: RepositoryConfig): string => { } }; -const buildEnv = async (config: RepositoryConfig) => { +export const buildEnv = async (config: RepositoryConfig) => { const env: Record = { RESTIC_CACHE_DIR: "/var/lib/zerobyte/restic/cache", PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin", @@ -805,7 +805,7 @@ const copy = async ( }; }; -const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { +export const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { if (config.backend === "sftp" && env._SFTP_KEY_PATH) { await fs.unlink(env._SFTP_KEY_PATH).catch(() => {}); } else if (config.isExistingRepository && config.customPassword && env.RESTIC_PASSWORD_FILE) { @@ -815,7 +815,7 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { +export const addCommonArgs = (args: string[], env: Record) => { args.push("--json"); if (env._SFTP_SSH_ARGS) { diff --git a/app/test/helpers/backup.ts b/app/test/helpers/backup.ts index 4e8be3fe..67cae6f0 100644 --- a/app/test/helpers/backup.ts +++ b/app/test/helpers/backup.ts @@ -8,6 +8,7 @@ export const createTestBackupSchedule = async (overrides: Partial