Compare commits

...

1 commit

Author SHA1 Message Date
Nicolas Meienberger
54eda9d837 refactor: import local repositories use path only and not name
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-26 17:14:16 +01:00
7 changed files with 860 additions and 22 deletions

View file

@ -185,13 +185,13 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete the repository <strong>{data.name}</strong>? This will not remove the Are you sure you want to delete the repository <strong>{data.name}</strong>? This will not remove the
actual data from the backend storage, only the repository configuration will be deleted. actual data from the backend storage, only the repository configuration will be deleted.
<br />
<br />
All backup schedules associated with this repository will also be removed.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<X className="h-4 w-4 mr-2" />
Cancel
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={handleConfirmDelete} onClick={handleConfirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"

View file

@ -32,8 +32,6 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
); );
const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const isImportedLocal = repository.type === "local" && repository.config.isExistingRepository;
const updateMutation = useMutation({ const updateMutation = useMutation({
...updateRepositoryMutation(), ...updateRepositoryMutation(),
onSuccess: () => { onSuccess: () => {
@ -77,13 +75,8 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
placeholder="Repository name" placeholder="Repository name"
maxLength={32} maxLength={32}
minLength={2} minLength={2}
disabled={isImportedLocal}
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Unique identifier for the repository.</p>
{isImportedLocal
? "Imported local repositories cannot be renamed."
: "Unique identifier for the repository."}
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="compressionMode">Compression mode</Label> <Label htmlFor="compressionMode">Compression mode</Label>

View file

@ -0,0 +1,10 @@
-- Migrate local imported repository paths to include the repository name
-- Previously, the path and name were concatenated at runtime. Now the path should be the full path.
UPDATE `repositories_table`
SET `config` = json_set(`config`, '$.path', rtrim(json_extract(`config`, '$.path'), '/') || '/' || json_extract(`config`, '$.name')),
`updated_at` = (unixepoch() * 1000)
WHERE `type` = 'local'
AND json_extract(`config`, '$.isExistingRepository') = true
AND json_extract(`config`, '$.path') IS NOT NULL
AND json_extract(`config`, '$.name') IS NOT NULL;

View file

@ -0,0 +1,832 @@
{
"id": "19421265-4e3a-46b8-9ca1-a01d1e293dbc",
"prevId": "ca46a423-51ca-45ae-9470-f82172a67bd3",
"version": "6",
"dialect": "sqlite",
"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",
"columnsFrom": [
"schedule_id"
],
"tableTo": "backup_schedules_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
},
"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",
"columnsFrom": [
"repository_id"
],
"tableTo": "repositories_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"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",
"columnsFrom": [
"schedule_id"
],
"tableTo": "backup_schedules_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
},
"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",
"columnsFrom": [
"destination_id"
],
"tableTo": "notification_destinations_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"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",
"columnsFrom": [
"volume_id"
],
"tableTo": "volumes_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_schedules_table_repository_id_repositories_table_id_fk": {
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
"tableFrom": "backup_schedules_table",
"columnsFrom": [
"repository_id"
],
"tableTo": "repositories_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"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",
"columnsFrom": [
"user_id"
],
"tableTo": "users_table",
"columnsTo": [
"id"
],
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"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": {
"columns": {},
"schemas": {},
"tables": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -183,6 +183,13 @@
"when": 1766431021321, "when": 1766431021321,
"tag": "0025_remarkable_pete_wisdom", "tag": "0025_remarkable_pete_wisdom",
"breakpoints": true "breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1766765013108,
"tag": "0026_migrate-local-repo-paths",
"breakpoints": true
} }
] ]
} }

View file

@ -1,6 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { eq, or } from "drizzle-orm"; import { eq, or } from "drizzle-orm";
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import { InternalServerError, NotFoundError } from "http-errors-enhanced";
import { db } from "../../db/db"; import { db } from "../../db/db";
import { repositoriesTable } from "../../db/schema"; import { repositoriesTable } from "../../db/schema";
import { toMessage } from "../../utils/errors"; import { toMessage } from "../../utils/errors";
@ -396,15 +396,6 @@ const updateRepository = async (id: string, updates: { name?: string; compressio
throw new NotFoundError("Repository not found"); throw new NotFoundError("Repository not found");
} }
if (
updates.name !== undefined &&
updates.name !== existing.name &&
existing.config.backend === "local" &&
existing.config.isExistingRepository
) {
throw new ConflictError("Cannot rename an imported local repository");
}
const newConfig = repositoryConfigSchema(existing.config); const newConfig = repositoryConfigSchema(existing.config);
if (newConfig instanceof type.errors) { if (newConfig instanceof type.errors) {
throw new InternalServerError("Invalid repository configuration"); throw new InternalServerError("Invalid repository configuration");

View file

@ -74,6 +74,11 @@ const ensurePassfile = async () => {
const buildRepoUrl = (config: RepositoryConfig): string => { const buildRepoUrl = (config: RepositoryConfig): string => {
switch (config.backend) { switch (config.backend) {
case "local": case "local":
if (config.isExistingRepository) {
if (!config.path) throw new Error("Path is required for existing local repositories");
return config.path;
}
return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`; return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`;
case "s3": case "s3":
return `s3:${config.endpoint}/${config.bucket}`; return `s3:${config.endpoint}/${config.bucket}`;