feat: restore from full backup snapshot#1323
Merged
Merged
Conversation
Adds a live, zero-downtime restore path for Full Backup snapshots (validate, gate writes, swap files, hot-swap AuthService's user store, invalidate sessions, reload branding, resync), an offline `restore-snapshot` CLI subcommand for disaster recovery, and a self-restart fallback for the rare case a failed restore can't be cleanly rolled back. Also fixes a pre-existing gap where branding.json (site name/logo/favicon selection) was never included in snapshot ZIPs. See docs/adr/0009-restore-hot-swap-and-write-gate.md for the design rationale.
- Fix data loss: RollbackAll skipped items whose move-in rename failed after move-aside already succeeded, permanently losing that item while reporting a clean rollback. Track movedAside separately from swapped. - Reject TriggerRestore while a previous restore left the instance in NeedsIntervention state, instead of silently clearing the flag and compounding a half-swapped filesystem. - Fix a race in AuthService.Close() (unguarded read vs. the new mutex) and track the restore goroutine so shutdown waits for an in-flight restore before closing AuthService out from under it. - Fix multi-step AuthService methods (TOTP setup/confirm/disable, recovery code consumption) that could straddle two different user stores if a restore's swap landed mid-request; they now capture the store once per request instead of re-reading it at each call. - Replace the write-gate's substring path exemption (vulnerable to --base-path collisions disabling the gate entirely, and to false positives on similarly-named routes) with proper path-segment matching; also exempt login/refresh/logout, which never touch swapped files. - Use a LocalizedError for the write-gate's 503 response, log the self-restart failure at its swallow point, and move a hardcoded restore.ts error string into the i18n namespace, per repo conventions. - Add an explicit startup warning for live restore on Windows, where renaming users.db while AuthService may still hold it open is unverified.
Also drops the dangling #api-keys-experimental anchor link left over from the API Keys section removal, and fixes the cross-references into the removed sections.
docs/adr and docs/dev are kept as local-only internal notes, not pushed to the shared repo yet — these three (added in the restore feature commit) were pushed by mistake, breaking that convention. Files remain on disk locally, just untracked again.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end “restore from full backup snapshot” support: a backend restore manager that validates/stages/swaps snapshot contents (with rollback + self-restart fallback), global write-gating during the swap, and a frontend UI flow to trigger/poll restore and show progress.
Changes:
- Backend: introduce
internal/restorerestore state machine +/api/admin/restore/*routes, and gate mutating requests during live restore via a global write-gate middleware. - Frontend: add restore API client + Zustand restore store, wire restore actions/progress UI into the Full Backup (snapshot) settings page, and add i18n strings.
- CLI/docs: add
leafwiki restore-snapshot <zip>offline restore subcommand; add dev architecture/source-map/ADR docs and adjust README wording.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/leafwiki-ui/src/stores/restore.ts | New Zustand store to trigger restore and poll restore/resync status. |
| ui/leafwiki-ui/src/locales/en/snapshot.json | Copy update for snapshot page description. |
| ui/leafwiki-ui/src/locales/en/restore.json | New i18n namespace for restore UI strings. |
| ui/leafwiki-ui/src/lib/i18n.ts | Register restore namespace. |
| ui/leafwiki-ui/src/lib/api/restore.ts | New typed client for restore endpoints. |
| ui/leafwiki-ui/src/features/snapshot/SnapshotSettings.tsx | Add restore actions, progress banner, and self-restart intervention UI. |
| README.md | Remove prior Full Backup section; adjust Git Backup note (now potentially inaccurate vs new feature). |
| internal/wiki/wiki.go | Wire restore routes into Wiki composition and expose BrandingService. |
| internal/wiki/restore/routes.go | New admin restore routes (trigger/status/self-restart) with auth+CSRF. |
| internal/wiki/restore/routes_test.go | Route-layer tests for restore handlers. |
| internal/wiki/restore/errors.go | Restore route error mapping + response shape. |
| internal/snapshot/snapshot.go | Include branding.json in snapshots (optional). |
| internal/snapshot/snapshot_test.go | Validate snapshot ZIP now contains branding config; optional-file behavior test. |
| internal/restore/writegate.go | New write-gate primitive (no HTTP dependency) to block mutations during swap. |
| internal/restore/writegate_test.go | Concurrency/behavior tests for WriteGate. |
| internal/restore/swap.go | Snapshot ZIP extract/validate + staged swap/rollback/commit implementation. |
| internal/restore/swap_test.go | Extensive swap/extract validation tests (zip slip, rollback correctness, etc.). |
| internal/restore/selfrestart_windows.go | Windows implementation: spawn new process then exit. |
| internal/restore/selfrestart_unix.go | Unix implementation: exec in-place. |
| internal/restore/offline.go | Offline restore helper used by CLI subcommand. |
| internal/restore/offline_test.go | Offline restore tests (swap + validation failure safety). |
| internal/restore/manager.go | Restore manager orchestration (validate → gate → swap → reopen auth → invalidate sessions → reload branding → resync). |
| internal/restore/manager_test.go | Integration-style tests for Manager happy path and failure modes. |
| internal/restore/job.go | Background job state tracking (phase/done/error/versionWarning/needsIntervention). |
| internal/restore/job_test.go | Job state tests. |
| internal/restore/helpers_test.go | Shared restore test fixtures (fixture snapshots + users DB builders). |
| internal/restore/config.go | Restore manager config wiring. |
| internal/http/router.go | Register global write-gate middleware when enabled. |
| internal/http/middleware/maintenance/writegate.go | Gin middleware wrapper for WriteGate with exempt paths. |
| internal/http/middleware/maintenance/writegate_test.go | Middleware exemption + gating tests. |
| internal/core/auth/session_store.go | Add DeleteAllSessions() to wipe sessions after restore. |
| internal/core/auth/auth_service.go | Add ReplaceUserStore() hot-swap with RWMutex; add InvalidateAllSessions(). |
| internal/core/auth/auth_service_replace_test.go | Tests for ReplaceUserStore + concurrent access + session invalidation. |
| internal/branding/branding_service.go | Add Reload() to refresh in-memory branding after restore swap. |
| internal/branding/branding_service_test.go | Test Reload() behavior. |
| docs/dev/source-map.md | New internal codebase navigation doc (contains some dead links / local paths). |
| docs/dev/architecture.md | New architecture overview doc (contains several dead ADR links). |
| docs/adr/0009-restore-hot-swap-and-write-gate.md | New ADR for restore/write-gate design (contains a dead link). |
| cmd/leafwiki/main.go | Wire snapshot+restore manager, write-gate, and add restore-snapshot subcommand. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- pollResyncTail no longer silently reports success when the tail-end resync job is lost or repeated status polls fail: a new resyncConfirmed flag lets the UI show a distinct warning instead of a false "completed" or a misleading full-restore failure (the restore itself already succeeded by this point). - Surface the "already running" case via a dedicated toast.info message, matching the existing resync_already_running convention in MaintenanceSettings, instead of discarding it for a generic error toast.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.