diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index e5c39e40..cb1ffead 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -407,7 +407,7 @@ export const deleteRepositoryMutation = (options?: Partial) => createQueryKey('getRepository', options); /** - * Get a single repository by name + * Get a single repository by ID */ export const getRepositoryOptions = (options: Options) => queryOptions>({ queryFn: async ({ queryKey, signal }) => { diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 4f3e083e..6c1978ce 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -170,18 +170,18 @@ export const listRcloneRemotes = (options? /** * Delete a repository */ -export const deleteRepository = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{name}', ...options }); +export const deleteRepository = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{id}', ...options }); /** - * Get a single repository by name + * Get a single repository by ID */ -export const getRepository = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}', ...options }); +export const getRepository = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{id}', ...options }); /** * Update a repository's name or settings */ export const updateRepository = (options: Options) => (options.client ?? client).patch({ - url: '/api/v1/repositories/{name}', + url: '/api/v1/repositories/{id}', ...options, headers: { 'Content-Type': 'application/json', @@ -192,28 +192,28 @@ export const updateRepository = (options: /** * List all snapshots in a repository */ -export const listSnapshots = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots', ...options }); +export const listSnapshots = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{id}/snapshots', ...options }); /** * Delete a specific snapshot from a repository */ -export const deleteSnapshot = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options }); +export const deleteSnapshot = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{id}/snapshots/{snapshotId}', ...options }); /** * Get details of a specific snapshot */ -export const getSnapshotDetails = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options }); +export const getSnapshotDetails = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{id}/snapshots/{snapshotId}', ...options }); /** * List files and directories in a snapshot */ -export const listSnapshotFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files', ...options }); +export const listSnapshotFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{id}/snapshots/{snapshotId}/files', ...options }); /** * Restore a snapshot to a target path on the filesystem */ export const restoreSnapshot = (options: Options) => (options.client ?? client).post({ - url: '/api/v1/repositories/{name}/restore', + url: '/api/v1/repositories/{id}/restore', ...options, headers: { 'Content-Type': 'application/json', @@ -224,7 +224,7 @@ export const restoreSnapshot = (options: O /** * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. */ -export const doctorRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{name}/doctor', ...options }); +export const doctorRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{id}/doctor', ...options }); /** * List all backup schedules diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 5d015721..b45285ea 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -870,6 +870,7 @@ export type CreateRepositoryResponses = { repository: { id: string; name: string; + shortId: string; }; }; }; @@ -898,10 +899,10 @@ export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcl export type DeleteRepositoryData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: '/api/v1/repositories/{name}'; + url: '/api/v1/repositories/{id}'; }; export type DeleteRepositoryResponses = { @@ -918,10 +919,10 @@ export type DeleteRepositoryResponse = DeleteRepositoryResponses[keyof DeleteRep export type GetRepositoryData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: '/api/v1/repositories/{name}'; + url: '/api/v1/repositories/{id}'; }; export type GetRepositoryResponses = { @@ -1011,10 +1012,10 @@ export type UpdateRepositoryData = { name?: string; }; path: { - name: string; + id: string; }; query?: never; - url: '/api/v1/repositories/{name}'; + url: '/api/v1/repositories/{id}'; }; export type UpdateRepositoryErrors = { @@ -1112,12 +1113,12 @@ export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRep export type ListSnapshotsData = { body?: never; path: { - name: string; + id: string; }; query?: { backupId?: string; }; - url: '/api/v1/repositories/{name}/snapshots'; + url: '/api/v1/repositories/{id}/snapshots'; }; export type ListSnapshotsResponses = { @@ -1139,11 +1140,11 @@ export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsRe export type DeleteSnapshotData = { body?: never; path: { - name: string; + id: string; snapshotId: string; }; query?: never; - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}'; + url: '/api/v1/repositories/{id}/snapshots/{snapshotId}'; }; export type DeleteSnapshotResponses = { @@ -1160,11 +1161,11 @@ export type DeleteSnapshotResponse = DeleteSnapshotResponses[keyof DeleteSnapsho export type GetSnapshotDetailsData = { body?: never; path: { - name: string; + id: string; snapshotId: string; }; query?: never; - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}'; + url: '/api/v1/repositories/{id}/snapshots/{snapshotId}'; }; export type GetSnapshotDetailsResponses = { @@ -1186,13 +1187,13 @@ export type GetSnapshotDetailsResponse = GetSnapshotDetailsResponses[keyof GetSn export type ListSnapshotFilesData = { body?: never; path: { - name: string; + id: string; snapshotId: string; }; query?: { path?: string; }; - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files'; + url: '/api/v1/repositories/{id}/snapshots/{snapshotId}/files'; }; export type ListSnapshotFilesResponses = { @@ -1235,10 +1236,10 @@ export type RestoreSnapshotData = { targetPath?: string; }; path: { - name: string; + id: string; }; query?: never; - url: '/api/v1/repositories/{name}/restore'; + url: '/api/v1/repositories/{id}/restore'; }; export type RestoreSnapshotResponses = { @@ -1258,10 +1259,10 @@ export type RestoreSnapshotResponse = RestoreSnapshotResponses[keyof RestoreSnap export type DoctorRepositoryData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: '/api/v1/repositories/{name}/doctor'; + url: '/api/v1/repositories/{id}/doctor'; }; export type DoctorRepositoryResponses = { diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 1891da87..55a6d0ce 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -14,18 +14,18 @@ import { FileTree } from "~/client/components/file-tree"; import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { useFileBrowser } from "~/client/hooks/use-file-browser"; import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic"; -import type { Snapshot } from "~/client/lib/types"; +import type { Repository, Snapshot } from "~/client/lib/types"; type RestoreLocation = "original" | "custom"; interface RestoreFormProps { snapshot: Snapshot; - repositoryName: string; + repository: Repository; snapshotId: string; returnPath: string; } -export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }: RestoreFormProps) { +export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: RestoreFormProps) { const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -42,10 +42,9 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } const { data: filesData, isLoading: filesLoading } = useQuery({ ...listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId }, + path: { id: repository.id, snapshotId }, query: { path: volumeBasePath }, }), - enabled: !!repositoryName && !!snapshotId, }); const stripBasePath = useCallback( @@ -78,7 +77,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } fetchFolder: async (path) => { return await queryClient.ensureQueryData( listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId }, + path: { id: repository.id, snapshotId }, query: { path }, }), ); @@ -86,7 +85,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } prefetchFolder: (path) => { queryClient.prefetchQuery( listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId }, + path: { id: repository.id, snapshotId }, query: { path }, }), ); @@ -111,8 +110,6 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } }); const handleRestore = useCallback(() => { - if (!repositoryName || !snapshotId) return; - const excludeXattrArray = excludeXattr ?.split(",") .map((s) => s.trim()) @@ -125,7 +122,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } const includePaths = pathsArray.map((path) => addBasePath(path)); restoreSnapshot({ - path: { name: repositoryName }, + path: { id: repository.id }, body: { snapshotId, include: includePaths.length > 0 ? includePaths : undefined, @@ -136,7 +133,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath } }, }); }, [ - repositoryName, + repository.id, snapshotId, excludeXattr, restoreLocation, @@ -156,7 +153,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }

Restore Snapshot

- {repositoryName} / {snapshotId} + {repository.name} / {snapshotId}

diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index ebd80fb4..f71737f6 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -25,10 +25,10 @@ import type { BackupSchedule, Snapshot } from "../lib/types"; type Props = { snapshots: Snapshot[]; backups: BackupSchedule[]; - repositoryName: string; + repositoryId: string; }; -export const SnapshotsTable = ({ snapshots, repositoryName, backups }: Props) => { +export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -53,7 +53,7 @@ export const SnapshotsTable = ({ snapshots, repositoryName, backups }: Props) => if (snapshotToDelete) { toast.promise( deleteSnapshot.mutateAsync({ - path: { name: repositoryName, snapshotId: snapshotToDelete }, + path: { id: repositoryId, snapshotId: snapshotToDelete }, }), { loading: "Deleting snapshot...", @@ -65,7 +65,7 @@ export const SnapshotsTable = ({ snapshots, repositoryName, backups }: Props) => }; const handleRowClick = (snapshotId: string) => { - navigate(`/repositories/${repositoryName}/${snapshotId}`); + navigate(`/repositories/${repositoryId}/${snapshotId}`); }; return ( diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx index 419f5e0e..db9eab63 100644 --- a/app/client/modules/backups/components/schedule-mirrors-config.tsx +++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx @@ -285,7 +285,7 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx index 87efda27..5022f416 100644 --- a/app/client/modules/backups/components/schedule-summary.tsx +++ b/app/client/modules/backups/components/schedule-summary.tsx @@ -90,7 +90,7 @@ export const ScheduleSummary = (props: Props) => { {schedule.volume.name} - + {schedule.repository.name} diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 40804f16..a9f95b58 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -11,14 +11,14 @@ import { useFileBrowser } from "~/client/hooks/use-file-browser"; interface Props { snapshot: Snapshot; - repositoryName: string; + repositoryId: string; backupId?: string; onDeleteSnapshot?: (snapshotId: string) => void; isDeletingSnapshot?: boolean; } export const SnapshotFileBrowser = (props: Props) => { - const { snapshot, repositoryName, backupId, onDeleteSnapshot, isDeletingSnapshot } = props; + const { snapshot, repositoryId, backupId, onDeleteSnapshot, isDeletingSnapshot } = props; const queryClient = useQueryClient(); @@ -26,7 +26,7 @@ export const SnapshotFileBrowser = (props: Props) => { const { data: filesData, isLoading: filesLoading } = useQuery({ ...listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId: snapshot.short_id }, + path: { id: repositoryId, snapshotId: snapshot.short_id }, query: { path: volumeBasePath }, }), }); @@ -61,7 +61,7 @@ export const SnapshotFileBrowser = (props: Props) => { fetchFolder: async (path) => { return await queryClient.ensureQueryData( listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId: snapshot.short_id }, + path: { id: repositoryId, snapshotId: snapshot.short_id }, query: { path }, }), ); @@ -69,7 +69,7 @@ export const SnapshotFileBrowser = (props: Props) => { prefetchFolder: (path) => { queryClient.prefetchQuery( listSnapshotFilesOptions({ - path: { name: repositoryName, snapshotId: snapshot.short_id }, + path: { id: repositoryId, snapshotId: snapshot.short_id }, query: { path }, }), ); @@ -82,7 +82,7 @@ export const SnapshotFileBrowser = (props: Props) => { return (
- +
@@ -94,7 +94,7 @@ export const SnapshotFileBrowser = (props: Props) => { to={ backupId ? `/backups/${backupId}/${snapshot.short_id}/restore` - : `/repositories/${repositoryName}/${snapshot.short_id}/restore` + : `/repositories/${repositoryId}/${snapshot.short_id}/restore` } className={buttonVariants({ variant: "primary", size: "sm" })} > diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index a4753005..6753f133 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -80,7 +80,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon isLoading, failureReason, } = useQuery({ - ...listSnapshotsOptions({ path: { name: schedule.repository.name }, query: { backupId: schedule.id.toString() } }), + ...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.id.toString() } }), }); const updateSchedule = useMutation({ @@ -198,7 +198,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon if (snapshotToDelete) { toast.promise( deleteSnapshot.mutateAsync({ - path: { name: schedule.repository.name, snapshotId: snapshotToDelete }, + path: { id: schedule.repository.shortId, snapshotId: snapshotToDelete }, }), { loading: "Deleting snapshot...", @@ -260,7 +260,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon { const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); if (!schedule.data) return redirect("/backups"); - const repositoryName = schedule.data.repository.name; + const repositoryId = schedule.data.repository.id; const snapshot = await getSnapshotDetails({ - path: { name: repositoryName, snapshotId: params.snapshotId }, + path: { id: repositoryId, snapshotId: params.snapshotId }, }); if (!snapshot.data) return redirect(`/backups/${params.id}`); + const repository = await getRepository({ path: { id: repositoryId } }); + if (!repository.data) return redirect(`/backups/${params.id}`); + return { snapshot: snapshot.data, - repositoryName, + repository: repository.data, snapshotId: params.snapshotId, backupId: params.id, }; }; export default function RestoreSnapshotFromBackupPage({ loaderData }: Route.ComponentProps) { - const { snapshot, repositoryName, snapshotId, backupId } = loaderData; + const { snapshot, repository, snapshotId, backupId } = loaderData; return ( diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index 79b8e5d6..dce3ff52 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -3,7 +3,7 @@ import { type } from "arktype"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { Save } from "lucide-react"; -import { cn, slugify } from "~/client/lib/utils"; +import { cn } from "~/client/lib/utils"; import { deepClean } from "~/utils/object"; import { Button } from "../../../components/ui/button"; import { @@ -109,7 +109,7 @@ export const CreateRepositoryForm = ({ field.onChange(slugify(e.target.value))} + onChange={(e) => field.onChange(e.target.value)} maxLength={32} minLength={2} /> diff --git a/app/client/modules/repositories/routes/create-repository.tsx b/app/client/modules/repositories/routes/create-repository.tsx index ec70f112..9d65b084 100644 --- a/app/client/modules/repositories/routes/create-repository.tsx +++ b/app/client/modules/repositories/routes/create-repository.tsx @@ -36,7 +36,7 @@ export default function CreateRepository() { ...createRepositoryMutation(), onSuccess: (data) => { toast.success("Repository created successfully"); - navigate(`/repositories/${data.repository.name}`); + navigate(`/repositories/${data.repository.shortId}`); }, }); diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx index 89e05d24..008efd10 100644 --- a/app/client/modules/repositories/routes/repositories.tsx +++ b/app/client/modules/repositories/routes/repositories.tsx @@ -148,9 +148,9 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { ) : ( filteredRepositories.map((repository) => ( navigate(`/repositories/${repository.name}`)} + onClick={() => navigate(`/repositories/${repository.shortId}`)} > {repository.name} diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index be3e7c99..a06ce51b 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -30,13 +30,13 @@ import { Loader2, Stethoscope, Trash2, X } from "lucide-react"; export const handle = { breadcrumb: (match: Route.MetaArgs) => [ { label: "Repositories", href: "/repositories" }, - { label: match.params.name }, + { label: match.loaderData?.name || match.params.id }, ], }; -export function meta({ params }: Route.MetaArgs) { +export function meta({ params, loaderData }: Route.MetaArgs) { return [ - { title: `Zerobyte - ${params.name}` }, + { title: `Zerobyte - ${loaderData?.name || params.id}` }, { name: "description", content: "View repository configuration, status, and snapshots.", @@ -45,7 +45,7 @@ export function meta({ params }: Route.MetaArgs) { } export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { - const repository = await getRepository({ path: { name: params.name ?? "" } }); + const repository = await getRepository({ path: { id: params.id ?? "" } }); if (repository.data) return repository.data; return redirect("/repositories"); @@ -62,13 +62,13 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro const activeTab = searchParams.get("tab") || "info"; const { data } = useQuery({ - ...getRepositoryOptions({ path: { name: loaderData.name } }), + ...getRepositoryOptions({ path: { id: loaderData.id } }), initialData: loaderData, }); useEffect(() => { - queryClient.prefetchQuery(listSnapshotsOptions({ path: { name: data.name } })); - }, [queryClient, data.name]); + queryClient.prefetchQuery(listSnapshotsOptions({ path: { id: data.id } })); + }, [queryClient, data.id]); const deleteRepo = useMutation({ ...deleteRepositoryMutation(), @@ -108,7 +108,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro const handleConfirmDelete = () => { setShowDeleteConfirm(false); - deleteRepo.mutate({ path: { name: data.name } }); + deleteRepo.mutate({ path: { id: data.id } }); }; const getStepLabel = (step: string) => { @@ -142,7 +142,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro