mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(restore): delete files not in snapshot option
This commit is contained in:
@@ -871,6 +871,7 @@ export type ListSnapshotFilesResponse = ListSnapshotFilesResponses[keyof ListSna
|
|||||||
export type RestoreSnapshotData = {
|
export type RestoreSnapshotData = {
|
||||||
body?: {
|
body?: {
|
||||||
snapshotId: string;
|
snapshotId: string;
|
||||||
|
delete?: boolean;
|
||||||
exclude?: Array<string>;
|
exclude?: Array<string>;
|
||||||
include?: Array<string>;
|
include?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/api-client/
|
|||||||
import { FileTree, type FileEntry } from "~/components/file-tree";
|
import { FileTree, type FileEntry } from "~/components/file-tree";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -33,6 +35,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
||||||
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
||||||
const [showRestoreDialog, setShowRestoreDialog] = useState(false);
|
const [showRestoreDialog, setShowRestoreDialog] = useState(false);
|
||||||
|
const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
|
||||||
|
|
||||||
const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "";
|
const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "";
|
||||||
|
|
||||||
@@ -175,10 +178,12 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
body: {
|
body: {
|
||||||
snapshotId: snapshot.short_id,
|
snapshotId: snapshot.short_id,
|
||||||
include: includePaths,
|
include: includePaths,
|
||||||
|
delete: deleteExtraFiles,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowRestoreDialog(false);
|
setShowRestoreDialog(false);
|
||||||
}, [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot]);
|
}, [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot, deleteExtraFiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -235,10 +240,22 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Confirm Restore</AlertDialogTitle>
|
<AlertDialogTitle>Confirm Restore</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This will restore {selectedPaths.size} selected {selectedPaths.size === 1 ? "item" : "items"} from the
|
{selectedPaths.size > 0
|
||||||
snapshot. Existing files will be overwritten by what's in the snapshot. This action cannot be undone.
|
? `This will restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"} from the snapshot.`
|
||||||
|
: "This will restore everything from the snapshot."}{" "}
|
||||||
|
Existing files will be overwritten by what's in the snapshot. This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
<div className="flex items-center space-x-2 py-4">
|
||||||
|
<Checkbox
|
||||||
|
id="delete-extra"
|
||||||
|
checked={deleteExtraFiles}
|
||||||
|
onCheckedChange={(checked) => setDeleteExtraFiles(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="delete-extra" className="text-sm font-normal cursor-pointer">
|
||||||
|
Delete files not present in the snapshot?
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleConfirmRestore}>Confirm</AlertDialogAction>
|
<AlertDialogAction onClick={handleConfirmRestore}>Confirm</AlertDialogAction>
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ export const repositoriesController = new Hono()
|
|||||||
)
|
)
|
||||||
.post("/:name/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
|
.post("/:name/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const { snapshotId, include, exclude } = c.req.valid("json");
|
const { snapshotId, ...options } = c.req.valid("json");
|
||||||
|
|
||||||
const result = await repositoriesService.restoreSnapshot(name, snapshotId, { include, exclude });
|
const result = await repositoriesService.restoreSnapshot(name, snapshotId, options);
|
||||||
|
|
||||||
return c.json<RestoreSnapshotDto>(result, 200);
|
return c.json<RestoreSnapshotDto>(result, 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export const restoreSnapshotBody = type({
|
|||||||
snapshotId: "string",
|
snapshotId: "string",
|
||||||
include: "string[]?",
|
include: "string[]?",
|
||||||
exclude: "string[]?",
|
exclude: "string[]?",
|
||||||
|
delete: "boolean?",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer;
|
export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer;
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ const listSnapshotFiles = async (name: string, snapshotId: string, path?: string
|
|||||||
const restoreSnapshot = async (
|
const restoreSnapshot = async (
|
||||||
name: string,
|
name: string,
|
||||||
snapshotId: string,
|
snapshotId: string,
|
||||||
options?: { include?: string[]; exclude?: string[] },
|
options?: { include?: string[]; exclude?: string[]; delete?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const repository = await db.query.repositoriesTable.findFirst({
|
const repository = await db.query.repositoriesTable.findFirst({
|
||||||
where: eq(repositoriesTable.name, name),
|
where: eq(repositoriesTable.name, name),
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ const restore = async (
|
|||||||
include?: string[];
|
include?: string[];
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
path?: string;
|
path?: string;
|
||||||
|
delete?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
@@ -205,7 +206,11 @@ const restore = async (
|
|||||||
args[args.length - 4] = `${snapshotId}:${options.path}`;
|
args[args.length - 4] = `${snapshotId}:${options.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.include && options.include.length === 0) {
|
if (options?.delete) {
|
||||||
|
args.push("--delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.include?.length) {
|
||||||
for (const pattern of options.include) {
|
for (const pattern of options.include) {
|
||||||
args.push("--include", pattern);
|
args.push("--include", pattern);
|
||||||
}
|
}
|
||||||
@@ -219,6 +224,8 @@ const restore = async (
|
|||||||
|
|
||||||
args.push("--json");
|
args.push("--json");
|
||||||
|
|
||||||
|
console.log("Restic restore command:", ["restic", ...args].join(" "));
|
||||||
|
|
||||||
const res = await $`restic ${args}`.env(env).nothrow();
|
const res = await $`restic ${args}`.env(env).nothrow();
|
||||||
|
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user