From 492aa4178df87570f8b199c91377acf3e9b9cc56 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:21:08 +0100 Subject: [PATCH] feat: allow to control --one-file-system option from the schedule config (#201) --- .../api-client/core/serverSentEvents.gen.ts | 2 + app/client/api-client/types.gen.ts | 7 + .../components/create-schedule-form.tsx | 25 + .../modules/backups/routes/backup-details.tsx | 2 + .../modules/backups/routes/create-backup.tsx | 1 + app/drizzle/0023_special_thor.sql | 2 + app/drizzle/meta/0023_snapshot.json | 839 ++++++++++++++++++ app/drizzle/meta/_journal.json | 7 + app/server/db/schema.ts | 1 + app/server/modules/backups/backups.dto.ts | 3 + app/server/modules/backups/backups.service.ts | 3 + app/server/utils/restic.ts | 14 +- 12 files changed, 898 insertions(+), 8 deletions(-) create mode 100644 app/drizzle/0023_special_thor.sql create mode 100644 app/drizzle/meta/0023_snapshot.json diff --git a/app/client/api-client/core/serverSentEvents.gen.ts b/app/client/api-client/core/serverSentEvents.gen.ts index f8fd78e..343d25a 100644 --- a/app/client/api-client/core/serverSentEvents.gen.ts +++ b/app/client/api-client/core/serverSentEvents.gen.ts @@ -169,6 +169,8 @@ export const createSseClient = ({ const { done, value } = await reader.read(); if (done) break; buffer += value; + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const chunks = buffer.split('\n\n'); buffer = chunks.pop() ?? ''; diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index eb9dc44..5d01572 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1305,6 +1305,7 @@ export type ListBackupSchedulesResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { @@ -1453,6 +1454,7 @@ export type CreateBackupScheduleData = { excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; + oneFileSystem?: boolean; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1486,6 +1488,7 @@ export type CreateBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repositoryId: string; retentionPolicy: { keepDaily?: number; @@ -1549,6 +1552,7 @@ export type GetBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { @@ -1696,6 +1700,7 @@ export type UpdateBackupScheduleData = { excludePatterns?: Array; includePatterns?: Array; name?: string; + oneFileSystem?: boolean; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1731,6 +1736,7 @@ export type UpdateBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repositoryId: string; retentionPolicy: { keepDaily?: number; @@ -1774,6 +1780,7 @@ export type GetBackupScheduleForVolumeResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index 2cf62e1..56457b9 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -6,6 +6,7 @@ import { useForm } from "react-hook-form"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { Checkbox } from "~/client/components/ui/checkbox"; import { Form, FormControl, @@ -38,6 +39,7 @@ const internalFormSchema = type({ keepWeekly: "number?", keepMonthly: "number?", keepYearly: "number?", + oneFileSystem: "boolean?", }); const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d))); @@ -101,6 +103,7 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined, excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined, + oneFileSystem: schedule.oneFileSystem ?? false, ...schedule.retentionPolicy, }; }; @@ -416,6 +419,24 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: )} /> + ( + + + + +
+ Stay on one file system + + Prevent Restic from crossing file system boundaries. This is useful to avoid backing up network + mounts or other partitions that might be mounted inside your backup source. + +
+
+ )} + /> @@ -629,6 +650,10 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: )} +
+

One file system

+

{formValues.oneFileSystem ? "Enabled" : "Disabled"}

+

Retention

diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 4804c4b..9ddecf1 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -162,6 +162,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon includePatterns: formValues.includePatterns, excludePatterns: formValues.excludePatterns, excludeIfPresent: formValues.excludeIfPresent, + oneFileSystem: formValues.oneFileSystem, }, }); }; @@ -177,6 +178,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon includePatterns: schedule.includePatterns || [], excludePatterns: schedule.excludePatterns || [], excludeIfPresent: schedule.excludeIfPresent || [], + oneFileSystem: schedule.oneFileSystem, }, }); }; diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index f0c4763..f0592b6 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -92,6 +92,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) { includePatterns: formValues.includePatterns, excludePatterns: formValues.excludePatterns, excludeIfPresent: formValues.excludeIfPresent, + oneFileSystem: formValues.oneFileSystem, }, }); }; diff --git a/app/drizzle/0023_special_thor.sql b/app/drizzle/0023_special_thor.sql new file mode 100644 index 0000000..97e81bd --- /dev/null +++ b/app/drizzle/0023_special_thor.sql @@ -0,0 +1,2 @@ +ALTER TABLE `backup_schedules_table` ADD `one_file_system` integer DEFAULT false NOT NULL; +UPDATE `backup_schedules_table` SET `one_file_system` = true; \ No newline at end of file diff --git a/app/drizzle/meta/0023_snapshot.json b/app/drizzle/meta/0023_snapshot.json new file mode 100644 index 0000000..0a2d13f --- /dev/null +++ b/app/drizzle/meta/0023_snapshot.json @@ -0,0 +1,839 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3e3841ca-67a8-493a-a061-9c2a780878ed", + "prevId": "11c24867-3186-4578-b8dd-cee4c48a28d1", + "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 + }, + "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_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 + }, + "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 2b034fb..2d8e50d 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1765794552191, "tag": "0022_woozy_shen", "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1766320570509, + "tag": "0023_special_thor", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index e35ca82..6942bd1 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -94,6 +94,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(), lastBackupError: text("last_backup_error"), nextBackupAt: int("next_backup_at", { mode: "number" }), + oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false), sortOrder: int("sort_order", { mode: "number" }).notNull().default(0), createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 6173bd8..1d6c14c 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -26,6 +26,7 @@ const backupScheduleSchema = type({ excludePatterns: "string[] | null", excludeIfPresent: "string[] | null", includePatterns: "string[] | null", + oneFileSystem: "boolean", lastBackupAt: "number | null", lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null", lastBackupError: "string | null", @@ -131,6 +132,7 @@ export const createBackupScheduleBody = type({ excludePatterns: "string[]?", excludeIfPresent: "string[]?", includePatterns: "string[]?", + oneFileSystem: "boolean?", tags: "string[]?", }); @@ -168,6 +170,7 @@ export const updateBackupScheduleBody = type({ excludePatterns: "string[]?", excludeIfPresent: "string[]?", includePatterns: "string[]?", + oneFileSystem: "boolean?", tags: "string[]?", }); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 1ff5124..f8dc283 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -124,6 +124,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { excludePatterns: data.excludePatterns ?? [], excludeIfPresent: data.excludeIfPresent ?? [], includePatterns: data.includePatterns ?? [], + oneFileSystem: data.oneFileSystem, nextBackupAt: nextBackupAt, }) .returning(); @@ -273,9 +274,11 @@ const executeBackup = async (scheduleId: number, manual = false) => { excludeIfPresent?: string[]; include?: string[]; tags?: string[]; + oneFileSystem?: boolean; signal?: AbortSignal; } = { tags: [schedule.id.toString()], + oneFileSystem: schedule.oneFileSystem, signal: abortController.signal, }; diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 8cb4502..6753e13 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -238,6 +238,7 @@ const backup = async ( excludeIfPresent?: string[]; include?: string[]; tags?: string[]; + oneFileSystem?: boolean; compressionMode?: CompressionMode; signal?: AbortSignal; onProgress?: (progress: BackupProgress) => void; @@ -246,14 +247,11 @@ const backup = async ( const repoUrl = buildRepoUrl(config); const env = await buildEnv(config); - const args: string[] = [ - "--repo", - repoUrl, - "backup", - "--one-file-system", - "--compression", - options?.compressionMode ?? "auto", - ]; + const args: string[] = ["--repo", repoUrl, "backup", "--compression", options?.compressionMode ?? "auto"]; + + if (options?.oneFileSystem) { + args.push("--one-file-system"); + } if (options?.tags && options.tags.length > 0) { for (const tag of options.tags) {