From 1f9826fe72e5fdc7628012b6925c89b36a711336 Mon Sep 17 00:00:00 2001 From: Deepseek1 Date: Fri, 28 Nov 2025 12:09:14 +0100 Subject: [PATCH] feat: add custom restore target directory Adds the ability to restore snapshots to a custom directory instead of only the original path. Closes #12. Changes: - Add target parameter to restore API endpoint - Add directory picker UI in file browser restore dialog - Add target input field in snapshot restore form - Create reusable PathSelector component Note: Run `bun run gen:api-client` after merging to regenerate types. --- app/client/components/path-selector.tsx | 52 +++++++++++++++++++ .../components/snapshot-file-browser.tsx | 16 +++++- .../components/restore-snapshot-dialog.tsx | 1 + .../components/restore-snapshot-form.tsx | 19 +++++++ .../modules/repositories/repositories.dto.ts | 1 + .../repositories/repositories.service.ts | 5 +- 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 app/client/components/path-selector.tsx diff --git a/app/client/components/path-selector.tsx b/app/client/components/path-selector.tsx new file mode 100644 index 00000000..4d6e6a4d --- /dev/null +++ b/app/client/components/path-selector.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { DirectoryBrowser } from "./directory-browser"; +import { Button } from "./ui/button"; + +type Props = { + value: string; + onChange: (path: string) => void; + label?: string; +}; + +/** + * A reusable path selector component that shows the selected path + * with a "Change" button, and expands to a DirectoryBrowser when clicked. + * Matches the pattern used in the volume creation form. + */ +export const PathSelector = ({ value, onChange, label = "Selected path:" }: Props) => { + const [showBrowser, setShowBrowser] = useState(false); + + if (showBrowser) { + return ( +
+ { + onChange(path); + setShowBrowser(false); + }} + selectedPath={value} + /> + +
+ ); + } + + return ( +
+
+
{label}
+
{value}
+
+ +
+ ); +}; diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 8a848bd5..6bff4bcb 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -18,6 +18,7 @@ import { AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; +import { PathSelector } from "~/client/components/path-selector"; import type { Snapshot, Volume } from "~/client/lib/types"; import { toast } from "sonner"; import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; @@ -42,6 +43,7 @@ export const SnapshotFileBrowser = (props: Props) => { const [deleteExtraFiles, setDeleteExtraFiles] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [excludeXattr, setExcludeXattr] = useState(""); + const [targetPath, setTargetPath] = useState("/"); const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/"; @@ -131,6 +133,7 @@ export const SnapshotFileBrowser = (props: Props) => { path: { name: repositoryName }, body: { snapshotId: snapshot.short_id, + target: targetPath || undefined, include: includePaths, delete: deleteExtraFiles, excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined, @@ -138,7 +141,7 @@ export const SnapshotFileBrowser = (props: Props) => { }); setShowRestoreDialog(false); - }, [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot, deleteExtraFiles, excludeXattr]); + }, [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot, deleteExtraFiles, excludeXattr, targetPath]); return (
@@ -232,6 +235,17 @@ export const SnapshotFileBrowser = (props: Props) => {
+
+ + +

+ Files will be restored to this location. Default "/" restores to original paths. +

+