restore as a page (#87)

* 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.

* refactor: path selector design

* refactor: unify restore snapshot dialogs

* refactor: restore snapshot as a page

* chore: fix liniting issues

* chore(create-notification): remove un-used prop

---------

Co-authored-by: Deepseek1 <Deepseek1@users.noreply.github.com>
This commit is contained in:
Nico
2025-11-30 16:43:34 +01:00
committed by Nicolas Meienberger
parent 03b898f84c
commit 0287bca4bb
18 changed files with 1933 additions and 2014 deletions

View File

@@ -240,7 +240,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
key={selectedSnapshot?.short_id}
snapshot={selectedSnapshot}
repositoryName={schedule.repository.name}
volume={schedule.volume}
backupId={schedule.id.toString()}
onDeleteSnapshot={handleDeleteSnapshot}
isDeletingSnapshot={deleteSnapshot.isPending}
/>

View File

@@ -0,0 +1,54 @@
import { redirect } from "react-router";
import { getBackupSchedule, getSnapshotDetails } from "~/client/api-client";
import { RestoreForm } from "~/client/components/restore-form";
import type { Route } from "./+types/restore-snapshot";
export const handle = {
breadcrumb: (match: Route.MetaArgs) => [
{ label: "Backups", href: "/backups" },
{ label: `Schedule #${match.params.id}`, href: `/backups/${match.params.id}` },
{ label: match.params.snapshotId },
{ label: "Restore" },
],
};
export function meta({ params }: Route.MetaArgs) {
return [
{ title: `Zerobyte - Restore Snapshot ${params.snapshotId}` },
{
name: "description",
content: "Restore files from a backup snapshot.",
},
];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
if (!schedule.data) return redirect("/backups");
const repositoryName = schedule.data.repository.name;
const snapshot = await getSnapshotDetails({
path: { name: repositoryName, snapshotId: params.snapshotId },
});
if (!snapshot.data) return redirect(`/backups/${params.id}`);
return {
snapshot: snapshot.data,
repositoryName,
snapshotId: params.snapshotId,
backupId: params.id,
};
};
export default function RestoreSnapshotFromBackupPage({ loaderData }: Route.ComponentProps) {
const { snapshot, repositoryName, snapshotId, backupId } = loaderData;
return (
<RestoreForm
snapshot={snapshot}
repositoryName={repositoryName}
snapshotId={snapshotId}
returnPath={`/backups/${backupId}`}
/>
);
}