Documentation
¶
Overview ¶
Package snapshot implements docs/multi-device-storage.md §3.6's "snapshot 起点復旧" path: a periodic, atomic point-in-time copy of the Hub's authoritative state that a backup peer can take over.
One snapshot is one directory under <configdir>/snapshots/<ts>/:
kojo.db — SQLite VACUUM INTO copy (consistent) blobs/global/... — copy of the global blob tree manifest.json — schema version, ts, sha256 of kojo.db
The KEK (auth/kek.bin) is intentionally NOT included — operators must back it up separately so a snapshot leak cannot decrypt secret kv rows on its own. Per-peer secrets (machine credentials, local-scope blobs) are also excluded; restoring those is the new Hub's own responsibility.
This package owns the on-disk layout and the atomic creation guarantees. All exposed functions are safe to call concurrently with running writers: VACUUM INTO takes a SQLite read lock that snapshots a consistent view, and the blob tree copy walks the filesystem.
Index ¶
Constants ¶
const DBFileName = "kojo.db"
DBFileName inside a snapshot dir mirrors the live store filename so a restore is "stop service, swap kojo.db, start service".
const ManifestFileName = "manifest.json"
ManifestFileName lives in every snapshot dir and lists the things that should be there. Restore tooling reads this first to validate the snapshot before swapping it in.
Variables ¶
This section is empty.
Functions ¶
func Apply ¶
func Apply(srcDir, targetConfigDir string, opts ApplyOptions) error
Apply restores a snapshot into the given target config directory. Manifest version + DB sha256 are verified before any data is laid down so a corrupted / mid-rsync snapshot fails the operation cleanly without touching the target.
Order of operations (every safety check runs BEFORE any write):
- Load + validate manifest.
- Verify snapshot DB sha256.
- Manifest BlobScopes ↔ source disk reconciliation.
- Existing-target overwrite gate (Force).
- Symlink / non-directory checks on every restore-touched subtree (configdir, blobs/, blobs/global/, auth/) — a planted symlink anywhere on the chain would let copyTree walk outside the configdir.
- Only after every check passes do we mkdir, chmod, copy.
Items intentionally NOT restored:
- <target>/auth/kek.bin: the KEK is operator-managed via a side channel (snapshot intentionally excludes it; restoring it from a stale snapshot would break envelope decryption if the KEK has been rotated). The operator must supply a matching KEK to the target before kojo can boot.
- <target>/auth/agent_tokens/*: per-peer machine credentials stay on each peer; peer-replicated kv rows in kojo.db carry the hashes.
- Per-peer scoped blobs / credentials.db: these stay on their respective peers. The new Hub serves them through the same peer-fan-out path the original Hub did.
Returns a non-nil error if any precondition fails. On partial-write failures past the preflight gate (e.g. the filesystem fills mid-copy) the target may be left in an inconsistent state; the runbook in docs/snapshot-restore.md tells the operator to wipe the target dir and retry.
func Take ¶
func Take(ctx context.Context, st *store.Store, blobRoot, root string, opts Options) (string, error)
Take creates a new snapshot under <root>/snapshots/<ts>/ and returns the absolute path to the directory. The directory layout is atomic in the sense that the manifest is written LAST — a crashed snapshot has no manifest and restore tooling treats manifest-less directories as in-progress / abandoned.
`st` provides the SQLite database connection. `blobRoot` is the directory containing the live blob scope subdirs; only `blobRoot/global` is copied. `root` is the Hub's config dir (the function creates `root/snapshots/<ts>/` under it).
On any failure mid-way the partially-written snapshot dir is left in place — the function does NOT clean up so an operator can inspect and recover. `kojo --clean snapshots` (Phase 6 #18) removes stale partials.
Types ¶
type ApplyOptions ¶
type ApplyOptions struct {
// Force lets the restore proceed even when a live kojo.db already
// exists at the target. The default refuses to overwrite a
// non-empty target so an accidental `kojo --restore` on a Hub that
// is still running can't silently nuke its DB.
//
// Even with Force, Apply NEVER overwrites a target whose
// configdir lock is held — the configdir.Acquire path layers on
// top in cmd/kojo and the operator is required to stop the
// running kojo before restoring (docs/snapshot-restore.md §
// "Restore on a backup peer" step 1).
Force bool
}
ApplyOptions narrows Apply(). All fields are optional.
type Manifest ¶
type Manifest struct {
// Version of the manifest format itself. Bumped only when the
// shape changes incompatibly.
Version int `json:"version"`
// Timestamp the snapshot started, RFC3339.
StartedAt string `json:"started_at"`
// SchemaVersion of kojo.db at the time of snapshot.
SchemaVersion int `json:"schema_version"`
// DBSHA256 over the snapshot's kojo.db file. Lets the restore
// path verify the file wasn't truncated mid-rsync.
DBSHA256 string `json:"db_sha256"`
// DBSize bytes; redundant with sha256 but cheap and fast to check.
DBSize int64 `json:"db_size"`
// BlobScopes describes which blob scopes were captured; v1 always
// only captures "global" (peer-shared content). "local"/"machine"
// are per-peer and stay on the original Hub.
BlobScopes []string `json:"blob_scopes"`
// HostHint records the os.Hostname() at snapshot time. Purely
// informational; restore on a different host is the whole point.
HostHint string `json:"host_hint"`
}
Manifest is the on-disk metadata file. Stable shape — restore tooling and human operators rely on it.
func LoadManifest ¶
LoadManifest reads and validates the manifest at dir/manifest.json. Used by restore tooling and `kojo snapshot list` (Phase 6).