Backup: restore snapshots into ~/bos-restore-<id>
Some checks failed
dev release / build (push) Successful in 3m12s
beta (rc) release / build (push) Has been skipped
release / build (push) Failing after 2m59s

Add a real restic restore that writes into a new directory instead of
$HOME, keep dry-run, confirm in the UI, and exclude container storage.
This commit is contained in:
Breadway 2026-08-16 00:53:00 +08:00
parent 73589ae36d
commit 11c4ecb3cc
3 changed files with 193 additions and 13 deletions

View file

@ -24,6 +24,7 @@
has_password: boolean;
snapshots: ResticSnapshot[];
error: string | null;
home: string;
}
let st = $state<BackupStatus | null>(null);
@ -31,10 +32,27 @@
let password = $state("");
let snapshots = $state<ResticSnapshot[]>([]);
let selected = $state("latest");
let restoreTarget = $state("");
let lastAutoTarget = $state("");
let log = $state<string[]>([]);
let busy = $state(false);
let message = $state("");
function defaultTarget(id: string): string {
const home = st?.home ?? "";
if (!home) return "";
return `${home}/bos-restore-${id || "latest"}`;
}
$effect(() => {
if (!st?.home) return;
const auto = defaultTarget(selected || "latest");
if (restoreTarget === "" || restoreTarget === lastAutoTarget) {
if (restoreTarget !== auto) restoreTarget = auto;
if (lastAutoTarget !== auto) lastAutoTarget = auto;
}
});
async function refresh() {
st = await invoke<BackupStatus>("get_backup_config");
repo = st.repo;
@ -79,6 +97,26 @@
snapshots = [];
}
}
async function restore(dryRun: boolean) {
const snap = selected || "latest";
const target = restoreTarget.trim() || defaultTarget(snap);
const home = st?.home ?? "";
if (!dryRun && home && (target === home || target === `${home}/`)) {
message = "Refusing to restore onto $HOME. Leave the default ~/bos-restore-<id> or pick another folder.";
return;
}
if (!dryRun) {
const ok = confirm(
`Restore snapshot ${snap} into ${target}?\n\nFiles go into that directory. Your live home is not overwritten.`,
);
if (!ok) return;
}
await run(dryRun ? "restic_restore_dry_run" : "restic_restore", {
snapshot: snap,
target,
});
}
</script>
<ViewScaffold title="Backup">
@ -100,18 +138,29 @@
{/if}
</Group>
<Group title="Actions" hint="Backup covers $HOME and skips caches, Trash, Steam, cargo/rustup, node_modules, target, and .git. Restore is dry-run only.">
<Group
title="Actions"
hint="This backs up your @home life — documents, configs, the stuff snapper does not. Snapshots on the Snapshots page are root (@) only. Skips caches, Trash, Steam, containers, cargo/rustup, Flatpak, node_modules, target, and .git."
>
<div class="btn-row">
<button disabled={busy} onclick={() => run("restic_init")}>Init repo</button>
<button class="primary" disabled={busy} onclick={() => run("restic_backup")}>Backup home</button>
<button disabled={busy} onclick={listSnaps}>List snapshots</button>
<button disabled={busy} onclick={() => run("restic_restore_dry_run", { snapshot: selected || "latest" })}>
Restore dry-run
</button>
</div>
{#if message}<Hint text={message} />{/if}
</Group>
<Group
title="Restore"
hint="Writes into a new folder (default ~/bos-restore-<id>). Does not overwrite $HOME. Dry-run previews the same target."
>
<FileField label="Restore into" bind:value={restoreTarget} mode="folder" placeholder="/home/you/bos-restore-latest" />
<div class="btn-row">
<button disabled={busy || !st?.restic_installed} onclick={() => restore(true)}>Restore dry-run</button>
<button class="primary" disabled={busy || !st?.restic_installed} onclick={() => restore(false)}>Restore</button>
</div>
</Group>
<Group title="Snapshots" wide>
{#if snapshots.length === 0}
<EmptyState icon={Archive} title="No snapshots loaded" hint="Init, backup, then list." />