sync: snapshot the index on every successful sync and ride a copy to the destination#91
Merged
Merged
Conversation
…the destination After a sync run reaches a terminal success/partial state, take one VACUUM INTO snapshot of the global index (reusing store.Backup) to a local tier and, for destination syncs, ride the same file along to <dest>/<volume>/.squirrel-index/ so the catalog inherits the same redundancy as the data it describes. - New [backups] config table (enabled/dir/keep/cloud/cloud_keep), on-by-default and zero-config: an absent table resolves to defaults. - Snapshotter coordinates one VACUUM per `squirrel sync` invocation and fans the single file out to the local tier and every destination; rotation bounds the local dir (keep) and each cloud .squirrel-index/ dir (cloud_keep, via rclone lsf + deletefile). - Snapshot/upload failures surface on Report.SnapshotErr and are logged; they never flip a successful sync to failed or mutate rep.Status. - Trigger sites are Sync and SyncNode after the run's terminal row is committed; peer-sync takes the local snapshot only (no ride-along). Scheduled syncs on the agent snapshot too. - .squirrel-index/ added to all three reserved-path filters: the sync and restore --filter lists and isReservedSyncPath/isReservedFolderPath in peer-sync, so a snapshot is never treated as user content. Closes #75
There was a problem hiding this comment.
Pull request overview
This PR implements issue #75 by adding automatic SQLite index snapshots after successful sync runs, storing them locally with rotation and (for destination syncs) uploading the same snapshot as a “ride-along” into each destination volume’s .squirrel-index/, also with rotation. It also ensures .squirrel-index/ is treated as a reserved path and excluded from sync/restore transfers and peer-sync discovery.
Changes:
- Add
Snapshotterto take oneVACUUM INTOsnapshot persquirrel syncinvocation and fan it out to local + destination copies, surfacing failures viaReport.SnapshotErrwithout failing the sync. - Exclude
.squirrel-index/in rclone sync/restore filters and in peer-sync reserved-path checks. - Add
[backups]config block with defaults (enabled by default) and tests; wire snapshot behavior into CLI sync and agent scheduled syncs.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sync/sync.go | Adds .squirrel-index constant, Options.Snapshot, Report.SnapshotErr, and filter exclusions for sync/restore. |
| sync/snapshot.go | Implements Snapshotter (local snapshot, cloud ride-along upload, local/cloud rotation). |
| sync/snapshot_test.go | Adds tests covering ride-along, disable flags, error surfacing without status flip, rotation bounds, and reserved-path guards. |
| sync/rclone.go | Adds “plain” rclone helpers (copyto, lsf, deletefile) to support snapshot ride-along and rotation. |
| sync/node.go | Ensures snapshot hook runs after peer-sync terminal run state is committed; expands reserved-path predicates. |
| README.md | Documents snapshot-on-sync behavior and destination layout including .squirrel-index/. |
| config/config.go | Adds resolved Backups config to Config and resolves it during load. |
| config/backups.go | Introduces [backups] config with defaults and validation. |
| config/backups_test.go | Tests defaults, overrides, disabling, negative retention rejection, and unknown-field rejection. |
| cmd/squirrel/sync.go | Wires Snapshotter creation into squirrel sync (shared across pairs). |
| cmd/squirrel/agent.go | Enables snapshot-on-sync for scheduled sync kicks via agent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…amp example Address Copilot review on #91: - rotateSnapshots sorted by modtime only; coarse-resolution filesystems or same-tick writes could order equal modtimes non-deterministically and rotate away a newer snapshot. Break ties by filename (which embeds a sortable timestamp) so rotation stays deterministic and chronological. - README destination-layout example used an extended ISO8601 timestamp with dashes/colons; the code uses the compact 20060102T150405.000Z layout. Match the example to what users actually see.
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.
What
Implements #75. The catalog should be as redundant as the data it describes, so after every successful sync squirrel now:
VACUUM INTOsnapshot of the global index (reusingstore.Backup) to a local tier (default<dir of db>/backups/), namedindex-<ISO8601>-run-<id>.db, withkeep-N rotation.<dest.root>/<volume>/.squirrel-index/index-<ISO8601>-run-<id>.dbvia the rclone wrapper (copyto), then rotates that dir tocloud_keep(rclonelsfnewest-N +deletefile).One
VACUUMpersquirrel syncinvocation, fanned out to the local tier and every destination — never one VACUUM per pair.Decisions honoured
index.db(no per-volume filtering); a copy lands under each synced volume's.squirrel-index/.[backups]table = enabled with defaults.index-<ISO8601>-run-<id>.db— lexically sortable and traceable to its producing run.Config
enabled = falsedisables both halves;cloud = falsekeeps the local snapshot but uploads nothing.Guards (non-negotiable)
.squirrel-index/added to all three reserved-path filters: the--filterlists inbuildRcloneArgsandbuildRestoreArgs, plusisReservedSyncPath/isReservedFolderPathin peer-sync.Report.SnapshotErr(mirroringFinishErr) and is logged — it never flips a successful sync to failed or mutatesrep.Status.SyncNodewas split intorunNodeSessionso its deferredfinishRunfires first).Tests
.squirrel-index/, is the global catalog (carries rows for a volume never synced to that destination), opens cleanly + passes integrity check, local-tier copy present with matching name, filename carries the run id..squirrel-index/excluded from sync (source-leak test), restore + sync arg builders (filter assertion), and peer-sync (isReservedSyncPath/isReservedFolderPath).SnapshotErr.enabled=false(nil Snapshotter → no-op) andcloud=false(local snapshot, no upload).enabled=false/cloud=false, negative-keep rejection, unknown-field rejection.go vet ./...,go test ./..., andgo test -race ./sync/...all pass.Out of scope (per ticket)
Litestream; peer ride-along; snapshot on restore; encrypting the snapshot at rest (docs note recommends a private bucket / SSE — added to the README).
Closes #75
Generated by Claude Code