diff --git a/.gitignore b/.gitignore index d0a5ab04..cbe33bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ node_modules/ playwright/.auth playwright/temp + +.idea/ diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index d5f50256..eabacd0c 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -7,12 +7,15 @@ import { FormLabel, FormMessage, } from "../../../../components/ui/form"; +import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; import { Checkbox } from "../../../../components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible"; import type { RepositoryFormValues } from "../create-repository-form"; import { cn } from "~/client/lib/utils"; +import { BANDWIDTH_UNITS } from "~/schemas/restic"; type Props = { form: UseFormReturn; @@ -21,11 +24,169 @@ type Props = { export const AdvancedForm = ({ form }: Props) => { const insecureTls = form.watch("insecureTls"); const cacert = form.watch("cacert"); + const uploadLimitEnabled = form.watch("uploadLimit.enabled"); + const downloadLimitEnabled = form.watch("downloadLimit.enabled"); return ( Advanced Settings +
+
+
+ ( + + + + +
+ Enable upload speed limit + Limit upload speed to the repository +
+
+ )} + /> +
+ ( + + +
+ field.onChange(parseInt(e.target.value, 10) || 1)} + /> +
+
+
+ +
+ + + )} + /> + ( + + + + + )} + /> +
+
+
+ +
+ ( + + + + +
+ Enable download speed limit + Limit download speed from the repository +
+
+ )} + /> + +
+
+ ( + + +
+ field.onChange(parseInt(e.target.value, 10) || 1)} + /> +
+
+
+
+ + + + )} + /> + ( + + + + + )} + /> +
+
+
+
+ statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'Mbps' NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'Mbps' NOT NULL; \ No newline at end of file diff --git a/app/drizzle/meta/0033_snapshot.json b/app/drizzle/meta/0033_snapshot.json new file mode 100644 index 00000000..fb54b5e3 --- /dev/null +++ b/app/drizzle/meta/0033_snapshot.json @@ -0,0 +1,1243 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9506e3c1-3131-4621-b74b-6023d63a0f00", + "prevId": "ad86ee2a-231e-45d1-9910-e4b7cac6a8af", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "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": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_users_table_id_fk": { + "name": "account_user_id_users_table_id_fk", + "tableFrom": "account", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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 + }, + "upload_limit_enabled": { + "name": "upload_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "upload_limit_value": { + "name": "upload_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_limit_unit": { + "name": "upload_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "download_limit_enabled": { + "name": "download_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "download_limit_value": { + "name": "download_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_limit_unit": { + "name": "download_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_table_token_unique": { + "name": "sessions_table_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "sessionsTable_userId_idx": { + "name": "sessionsTable_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "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": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + "secret" + ], + "isUnique": false + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "two_factor_user_id_users_table_id_fk": { + "name": "two_factor_user_id_users_table_id_fk", + "tableFrom": "two_factor", + "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": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "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)" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "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 8add8b31..c428b6bf 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,237 +1,244 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1755765658194, - "tag": "0000_known_madelyne_pryor", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1755775437391, - "tag": "0001_far_frank_castle", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1756930554198, - "tag": "0002_cheerful_randall", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1758653407064, - "tag": "0003_mature_hellcat", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1758961535488, - "tag": "0004_wealthy_tomas", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1759416698274, - "tag": "0005_simple_alice", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1760734377440, - "tag": "0006_secret_micromacro", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1761224911352, - "tag": "0007_watery_sersi", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1761414054481, - "tag": "0008_silent_lady_bullseye", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1762095226041, - "tag": "0009_little_adam_warlock", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1762610065889, - "tag": "0010_perfect_proemial_gods", - "breakpoints": true - }, - { - "idx": 11, - "version": "6", - "when": 1763644043601, - "tag": "0011_familiar_stone_men", - "breakpoints": true - }, - { - "idx": 12, - "version": "6", - "when": 1764100562084, - "tag": "0012_add_short_ids", - "breakpoints": true - }, - { - "idx": 13, - "version": "6", - "when": 1764182159797, - "tag": "0013_elite_sprite", - "breakpoints": true - }, - { - "idx": 14, - "version": "6", - "when": 1764182405089, - "tag": "0014_wild_echo", - "breakpoints": true - }, - { - "idx": 15, - "version": "6", - "when": 1764182465287, - "tag": "0015_jazzy_sersi", - "breakpoints": true - }, - { - "idx": 16, - "version": "6", - "when": 1764194697035, - "tag": "0016_fix-timestamps-to-ms", - "breakpoints": true - }, - { - "idx": 17, - "version": "6", - "when": 1764357897219, - "tag": "0017_fix-compression-modes", - "breakpoints": true - }, - { - "idx": 18, - "version": "6", - "when": 1764794371040, - "tag": "0018_breezy_invaders", - "breakpoints": true - }, - { - "idx": 19, - "version": "6", - "when": 1764839917446, - "tag": "0019_secret_nomad", - "breakpoints": true - }, - { - "idx": 20, - "version": "6", - "when": 1764847918249, - "tag": "0020_even_dexter_bennett", - "breakpoints": true - }, - { - "idx": 21, - "version": "6", - "when": 1765307881092, - "tag": "0021_steady_viper", - "breakpoints": true - }, - { - "idx": 22, - "version": "6", - "when": 1765794552191, - "tag": "0022_woozy_shen", - "breakpoints": true - }, - { - "idx": 23, - "version": "6", - "when": 1766320570509, - "tag": "0023_special_thor", - "breakpoints": true - }, - { - "idx": 24, - "version": "6", - "when": 1766325504548, - "tag": "0024_schedules-one-fs", - "breakpoints": true - }, - { - "idx": 25, - "version": "6", - "when": 1766431021321, - "tag": "0025_remarkable_pete_wisdom", - "breakpoints": true - }, - { - "idx": 26, - "version": "6", - "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 - }, - { - "idx": 29, - "version": "6", - "when": 1767819883495, - "tag": "0029_boring_luke_cage", - "breakpoints": true - }, - { - "idx": 30, - "version": "6", - "when": 1767821088612, - "tag": "0030_lower-trim-username", - "breakpoints": true - }, - { - "idx": 31, - "version": "6", - "when": 1767863951955, - "tag": "0031_graceful_squadron_supreme", - "breakpoints": true - }, - { - "idx": 32, - "version": "6", - "when": 1768136755563, - "tag": "0032_lowercase-email", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1755765658194, + "tag": "0000_known_madelyne_pryor", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755775437391, + "tag": "0001_far_frank_castle", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1756930554198, + "tag": "0002_cheerful_randall", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1758653407064, + "tag": "0003_mature_hellcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1758961535488, + "tag": "0004_wealthy_tomas", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1759416698274, + "tag": "0005_simple_alice", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1760734377440, + "tag": "0006_secret_micromacro", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1761224911352, + "tag": "0007_watery_sersi", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1761414054481, + "tag": "0008_silent_lady_bullseye", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1762095226041, + "tag": "0009_little_adam_warlock", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1762610065889, + "tag": "0010_perfect_proemial_gods", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1763644043601, + "tag": "0011_familiar_stone_men", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1764100562084, + "tag": "0012_add_short_ids", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1764182159797, + "tag": "0013_elite_sprite", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1764182405089, + "tag": "0014_wild_echo", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1764182465287, + "tag": "0015_jazzy_sersi", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1764194697035, + "tag": "0016_fix-timestamps-to-ms", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1764357897219, + "tag": "0017_fix-compression-modes", + "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1764794371040, + "tag": "0018_breezy_invaders", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764839917446, + "tag": "0019_secret_nomad", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1764847918249, + "tag": "0020_even_dexter_bennett", + "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1765307881092, + "tag": "0021_steady_viper", + "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1765794552191, + "tag": "0022_woozy_shen", + "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1766320570509, + "tag": "0023_special_thor", + "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1766325504548, + "tag": "0024_schedules-one-fs", + "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1766431021321, + "tag": "0025_remarkable_pete_wisdom", + "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "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 + }, + { + "idx": 29, + "version": "6", + "when": 1767819883495, + "tag": "0029_boring_luke_cage", + "breakpoints": true + }, + { + "idx": 30, + "version": "6", + "when": 1767821088612, + "tag": "0030_lower-trim-username", + "breakpoints": true + }, + { + "idx": 31, + "version": "6", + "when": 1767863951955, + "tag": "0031_graceful_squadron_supreme", + "breakpoints": true + }, + { + "idx": 32, + "version": "6", + "when": 1768136755563, + "tag": "0032_lowercase-email", + "breakpoints": true + }, + { + "idx": 33, + "version": "6", + "when": 1768497229303, + "tag": "0033_thankful_master_chief", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index f6001fb1..846fc0c7 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -13,12 +13,31 @@ export const REPOSITORY_BACKENDS = { export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; +export const BANDWIDTH_UNITS = { + Kbps: "Kbps", + Mbps: "Mbps", + Gbps: "Gbps", +} as const; + +export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; + +export const bandwidthLimitSchema = type({ + enabled: "boolean = false", + value: "number > 0 = 1", + unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"), +}); + +export type BandwidthLimit = typeof bandwidthLimitSchema.infer; + // Common fields for all repository configs const baseRepositoryConfigSchema = type({ isExistingRepository: "boolean?", customPassword: "string?", cacert: "string?", insecureTls: "boolean?", + // Bandwidth controls + uploadLimit: bandwidthLimitSchema.optional(), + downloadLimit: bandwidthLimitSchema.optional(), }); export const s3RepositoryConfigSchema = type({ diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 647a2f4e..23ad9da9 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,6 +1,12 @@ import { relations, sql } from "drizzle-orm"; -import { index, int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core"; -import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic"; +import { index, int, integer, sqliteTable, text, real, primaryKey, unique } from "drizzle-orm/sqlite-core"; +import type { + CompressionMode, + RepositoryBackend, + repositoryConfigSchema, + RepositoryStatus, + BandwidthUnit, +} from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; @@ -159,6 +165,12 @@ export const repositoriesTable = sqliteTable("repositories_table", { status: text().$type().default("unknown"), lastChecked: int("last_checked", { mode: "number" }), lastError: text("last_error"), + uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false), + uploadLimitValue: real("upload_limit_value").notNull().default(0), + uploadLimitUnit: text("upload_limit_unit").$type().notNull().default("Mbps"), + downloadLimitEnabled: int("download_limit_enabled", { mode: "boolean" }).notNull().default(false), + downloadLimitValue: real("download_limit_value").notNull().default(0), + downloadLimitUnit: text("download_limit_unit").$type().notNull().default("Mbps"), createdAt: int("created_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 4eabbb49..733c2e1d 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -10,7 +10,7 @@ import { logger } from "./logger"; import { cryptoUtils } from "./crypto"; import type { RetentionPolicy } from "../modules/backups/backups.dto"; import { safeSpawn } from "./spawn"; -import type { CompressionMode, RepositoryConfig, OverwriteMode } from "~/schemas/restic"; +import type { CompressionMode, RepositoryConfig, OverwriteMode, BandwidthLimit } from "~/schemas/restic"; import { ResticError } from "./errors"; const backupOutputSchema = type({ @@ -229,7 +229,7 @@ const init = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["init", "--repo", repoUrl]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -321,7 +321,7 @@ const backup = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const logData = throttle((data: string) => { logger.info(data.trim()); @@ -453,7 +453,7 @@ const restore = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); logger.debug(`Executing: restic ${args.join(" ")}`); const res = await safeSpawn({ command: "restic", args, env }); @@ -516,7 +516,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -565,7 +565,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: } args.push("--prune"); - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -587,7 +587,7 @@ const deleteSnapshots = async (config: RepositoryConfig, snapshotIds: string[]) } const args: string[] = ["--repo", repoUrl, "forget", ...snapshotIds, "--prune"]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -636,7 +636,7 @@ const tagSnapshots = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -686,7 +686,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) = args.push(path); } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -737,7 +737,7 @@ const unlock = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["unlock", "--repo", repoUrl, "--remove-all"]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -761,7 +761,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean }) args.push("--read-data"); } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -794,7 +794,7 @@ const repairIndex = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["repair", "index", "--repo", repoUrl]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -846,10 +846,18 @@ const copy = async ( args.push("latest"); } - addCommonArgs(args, env); + addCommonArgs(args, env, destConfig, { skipBandwidth: true }); - if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { - args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); + const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); + const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit); + + if (sourceDownloadLimit) { + args.push("--limit-download", sourceDownloadLimit); + } + + // Apply upload limit to destination for all backends + if (destUploadLimit) { + args.push("--limit-upload", destUploadLimit); } logger.info(`Copying snapshots from ${sourceRepoUrl} to ${destRepoUrl}...`); @@ -874,29 +882,38 @@ const copy = async ( }; }; -export const cleanupTemporaryKeys = async (env: Record) => { - if (env._SFTP_KEY_PATH) { - await fs.unlink(env._SFTP_KEY_PATH).catch(() => {}); +const formatBandwidthLimit = (limit?: BandwidthLimit): string => { + if (!limit || !limit.enabled || limit.value <= 0) { + return ""; } - if (env._SFTP_KNOWN_HOSTS_PATH) { - await fs.unlink(env._SFTP_KNOWN_HOSTS_PATH).catch(() => {}); + let kibibytesPerSecond: number; + switch (limit.unit) { + case "Kbps": + // Kilobits per second to KiB/s: kilobits→bits→bytes→KiB + kibibytesPerSecond = (limit.value * 1000) / 8 / 1024; + break; + case "Mbps": + // Megabits per second to KiB/s: multiply by 1000000 (Mb to bits), divide by 8 (bits to bytes), then by 1024 (bytes to KiB) + kibibytesPerSecond = (limit.value * 1000000) / (8 * 1024); + break; + case "Gbps": + // Gigabits per second to KiB/s: multiply by 1000000000 (Gb to bits), divide by 8 (bits to bytes), then by 1024 (bytes to KiB) + kibibytesPerSecond = (limit.value * 1000000000) / (8 * 1024); + break; + default: + return ""; } - if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { - await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {}); - } - - if (env.GOOGLE_APPLICATION_CREDENTIALS) { - await fs.unlink(env.GOOGLE_APPLICATION_CREDENTIALS).catch(() => {}); - } - - if (env.RESTIC_CACERT) { - await fs.unlink(env.RESTIC_CACERT).catch(() => {}); - } + return `${Math.floor(kibibytesPerSecond)}`; }; -export const addCommonArgs = (args: string[], env: Record) => { +export const addCommonArgs = ( + args: string[], + env: Record, + config?: RepositoryConfig, + options?: { skipBandwidth?: boolean }, +) => { args.push("--json"); if (env._SFTP_SSH_ARGS) { @@ -910,6 +927,18 @@ export const addCommonArgs = (args: string[], env: Record) => { if (env.RESTIC_CACERT) { args.push("--cacert", env.RESTIC_CACERT); } + + if (config && !options?.skipBandwidth) { + const uploadLimit = formatBandwidthLimit(config.uploadLimit); + if (uploadLimit) { + args.push("--limit-upload", uploadLimit); + } + + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + if (downloadLimit) { + args.push("--limit-download", downloadLimit); + } + } }; export const restic = { @@ -928,3 +957,18 @@ export const restic = { repairIndex, copy, }; + +export const cleanupTemporaryKeys = async (env: Record) => { + const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"]; + + for (const key of keysToClean) { + if (env[key]) { + await fs.unlink(env[key]).catch(() => {}); + } + } + + // Clean up custom password files + if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { + await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {}); + } +};