feat(installer): back up what a run is about to overwrite #11

Merged
whitlocktech merged 1 commits from feat/backup-before-overwrite into edge 2026-08-05 17:53:47 +00:00
Member

What & why

PLAN.md §5.3 (scope decision in docs#96). Before anything is written, every file this run will replace is copied into <state>/backups/<utc-stamp>/ beside a manifest.json naming where each came from. --no-backup opts out; --verify takes none.

Scoped by what cannot be fetched again

The sidecar binary and the overlay files are re-downloadable and hash-named in the bundle. The database is a cache with a schemalink/sidecar/src/store.rs creates every table IF NOT EXISTS, and every one holds shard state the sweeps repopulate. Copying those would be bulk with no recovery value, and the bulk is not free: it would bury the two things that matter.

What a run can destroy for good is short:

  1. An operator's edits to a deployed .cs file. Phase 1 overwrites those unconditionally and by design (Bridge.cfg is the one exception) — so the one place this tool knowingly discards work is the one place it should keep a copy first.
  2. sidecar.toml, whose token the website already holds.

Two deviations from §5.3 as written, both found by building it

  • The trigger is "this run is about to overwrite something", not "an update, or an install over an existing record". §5.3 justified the latter with "a first install overwrites nothing" — which is not true of a tree deployed by hand per INSTALL.md Appendix A2, a path this project documents and recommends while the binary is unreleased. There, a first install finds .cs files that differ, plans them as Change, and overwrites them with no record anywhere of what was there. The direct test covers that and still writes nothing for a genuine first install, because there is nothing to copy. docs#96 is updated to match.
  • sidecar.toml joins a backup that is already being taken and is never the reason for one. Nothing in the installer rewrites it, so making it a trigger would leave a dated directory after every no-op update — the same failure as an empty backup, one step along. It is copied so a restored set of files arrives with the token that matches them.

Details that are load-bearing

  • The directory is created lazily and the manifest is written last. A directory carrying a manifest is a complete backup, and list/prune consider only those — so a run interrupted mid-copy can neither be mistaken for a backup nor evict a good one by being newer. It is left on disk for a human rather than silently deleted.
  • Three are kept (KEEP). An unbounded directory of ServUO source copies on a shard host is its own support problem.
  • uninstall keeps them and lists them in the report — the durable artifact, since by the time it is read the run that took the backup is long out of the scrollback. --purge removes them, alongside the config, the database and the cached patch set. Same rule, same reason: they are the only offline record of what was here before.
  • doctor reports the newest, always : having no backup is correct on a host that has never overwritten anything.
  • Restoring stays printed, not done, as the uninstall report is — the installer cannot know what has changed since, and putting an old .cs back over a newer overlay eats work rather than saving it.

How it was tested

fmt, clippy -D warnings and 144 tests on both Linux (Docker) and the Windows host. Then live, against two scratch ServUO trees built from the real 57.4 files:

# Case Result
1 Clean first install no backups/ directory created at all
2 update after editing a deployed .cs copy holds the edit, tree gets the release's file, manifest lists it and sidecar.toml
3 No-op update no backup
4 --no-backup with an edit present overwritten, no copy
5 --verify with an edit present nothing written, edit survives
6 A fourth backup oldest pruned, three kept
7 doctor ✓ Backups 2026-08-05T10:44:46Z — 2 file(s) replaced by update to bundle 2026.08.04
8 uninstall three kept, listed in the report
9 uninstall --purge directory gone
10 install --patches pre-patch Logging.cs captured (diff confirms the backup has no OnWrite hook, the tree does); the two rung-0 patches wrote nothing and correctly captured nothing

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why [PLAN.md §5.3](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/PLAN.md) (scope decision in docs#96). Before anything is written, every file this run will replace is copied into `<state>/backups/<utc-stamp>/` beside a `manifest.json` naming where each came from. `--no-backup` opts out; `--verify` takes none. ### Scoped by what cannot be fetched again The sidecar binary and the overlay files are re-downloadable and hash-named in the bundle. The database is a **cache with a schema** — `link/sidecar/src/store.rs` creates every table `IF NOT EXISTS`, and every one holds shard state the sweeps repopulate. Copying those would be bulk with no recovery value, and the bulk is not free: it would bury the two things that matter. What a run can destroy for good is short: 1. **An operator's edits to a deployed `.cs` file.** Phase 1 overwrites those *unconditionally and by design* (`Bridge.cfg` is the one exception) — so the one place this tool knowingly discards work is the one place it should keep a copy first. 2. **`sidecar.toml`**, whose token the website already holds. ### Two deviations from §5.3 as written, both found by building it - **The trigger is "this run is about to overwrite something"**, not "an `update`, or an `install` over an existing record". §5.3 justified the latter with *"a first install overwrites nothing"* — which is not true of a tree deployed **by hand per INSTALL.md Appendix A2**, a path this project documents and recommends while the binary is unreleased. There, a first `install` finds `.cs` files that differ, plans them as `Change`, and overwrites them with no record anywhere of what was there. The direct test covers that and still writes nothing for a genuine first install, because there is nothing to copy. docs#96 is updated to match. - **`sidecar.toml` joins a backup that is already being taken and is never the reason for one.** Nothing in the installer rewrites it, so making it a trigger would leave a dated directory after every no-op `update` — the same failure as an empty backup, one step along. It is copied so a restored set of files arrives with the token that matches them. ### Details that are load-bearing - **The directory is created lazily and the manifest is written last.** A directory carrying a manifest is a complete backup, and `list`/`prune` consider only those — so a run interrupted mid-copy can neither be mistaken for a backup nor evict a good one by being newer. It is left on disk for a human rather than silently deleted. - **Three are kept** (`KEEP`). An unbounded directory of ServUO source copies on a shard host is its own support problem. - **`uninstall` keeps them** and lists them in the report — the durable artifact, since by the time it is read the run that took the backup is long out of the scrollback. `--purge` removes them, alongside the config, the database and the cached patch set. Same rule, same reason: they are the only offline record of what was here before. - **`doctor` reports the newest**, always `✓`: having no backup is correct on a host that has never overwritten anything. - **Restoring stays printed, not done**, as the uninstall report is — the installer cannot know what has changed since, and putting an old `.cs` back over a newer overlay eats work rather than saving it. ## How it was tested `fmt`, `clippy -D warnings` and 144 tests on **both** Linux (Docker) and the Windows host. Then live, against two scratch ServUO trees built from the real 57.4 files: | # | Case | Result | |---|---|---| | 1 | Clean first install | no `backups/` directory created at all | | 2 | `update` after editing a deployed `.cs` | copy holds the edit, tree gets the release's file, manifest lists it **and** `sidecar.toml` | | 3 | No-op `update` | no backup | | 4 | `--no-backup` with an edit present | overwritten, no copy | | 5 | `--verify` with an edit present | nothing written, edit survives | | 6 | A fourth backup | oldest pruned, three kept | | 7 | `doctor` | `✓ Backups 2026-08-05T10:44:46Z — 2 file(s) replaced by update to bundle 2026.08.04` | | 8 | `uninstall` | three kept, listed in the report | | 9 | `uninstall --purge` | directory gone | | 10 | `install --patches` | pre-patch `Logging.cs` captured (diff confirms the backup has no `OnWrite` hook, the tree does); the two rung-0 patches wrote nothing and correctly captured nothing | ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-05 10:48:07 +00:00
feat(installer): back up what a run is about to overwrite
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m59s
82900da939
PLAN.md §5.3. Before anything is written, every file this run will
replace is copied into <state>/backups/<utc-stamp>/ with a manifest
naming where each came from. --no-backup opts out; --verify takes none.

Scoped by what cannot be fetched again. The sidecar binary and the
overlay files are re-downloadable and hash-named in the bundle, and the
database is a cache with a schema -- link's store.rs creates every table
IF NOT EXISTS over shard state the sweeps repopulate. What a run can
destroy for good is an operator's edits to a deployed .cs file, which
Phase 1 overwrites unconditionally and by design, and sidecar.toml,
whose token the website already holds.

Two deviations from §5.3 as written, both found by building it:

- The trigger is "this run is about to overwrite something", not "an
  update, or an install over an existing record". §5.3 justified the
  latter with "a first install overwrites nothing" -- which is not true
  of a tree deployed by hand per INSTALL.md Appendix A2, a documented
  path. There the first install finds .cs files that differ, plans them
  as Change, and overwrites them with no record anywhere. The direct
  test covers that case and still writes nothing for a genuine first
  install, because there is nothing to copy.
- sidecar.toml joins a backup that is already being taken and is never
  the reason for one. Nothing here rewrites it, so making it a trigger
  would put a dated directory on disk after every no-op update; it is
  copied so a restored set of files comes with the token that matches
  them.

The directory is created lazily and the manifest is written last, so a
directory carrying one is a complete backup -- and pruning only
considers those, so a run interrupted mid-copy cannot evict a good
backup by being newer than it. Three are kept. uninstall keeps them and
names them in its report; --purge removes them, alongside the config,
the database and the cached patch set. doctor reports the newest.

Restoring stays printed rather than done, as the uninstall report is:
the installer cannot know what has changed since, and putting an old
.cs file back over a newer overlay eats work rather than saving it.

Verified live against two scratch ServUO trees built from the real 57.4
files: a clean first install leaving no backups directory at all, an
update after editing a deployed .cs (copy holds the edit, tree gets the
release's file, manifest lists both it and sidecar.toml), a no-op update
taking none, --no-backup and --verify each taking none, a fourth backup
pruning the oldest, doctor's row, uninstall keeping three and listing
them, --purge removing them, and a --patches run capturing the
pre-patch Logging.cs while the two rung-0 patches correctly captured
nothing. fmt, clippy -D warnings and 144 tests on both Linux and
Windows.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 07a2cca5f6 into edge 2026-08-05 17:53:47 +00:00
whitlocktech deleted branch feat/backup-before-overwrite 2026-08-05 17:53:48 +00:00
Sign in to join this conversation.
No description provided.