Mirror Snapshot Sync#
Mirror snapshot sync is the mechanism by which zerobyte retroactively copies existing snapshots from a primary backup repository into a mirror repository. It was introduced in PR #755 to address the gap where snapshots created before a mirror was added were never automatically propagated.
The feature exposes two backend endpoints :
GET /backups/:shortId/mirrors/:mirrorShortId/status— enumerates and compares snapshots between the source and mirror repos, returningsourceCount,mirrorCount, and amissingSnapshotslist.POST /backups/:shortId/mirrors/:mirrorShortId/sync— accepts an optionalsnapshotIdsarray and enqueues a background copy task.
Snapshot Enumeration Logic#
Status endpoint (getMirrorSyncStatus)#
getMirrorSyncStatus in backups.service.ts drives all snapshot discovery for the dialog. The key steps:
- Resolves the schedule and mirror repository records from the database.
- Calls
restic.snapshots()concurrently for both the source and mirror repositories, filtering by the schedule'sshortIdas a tag . - Builds a
Setof mirror snapshot timestamps and computesmissingSnapshotsas the source snapshots whosetimefield is absent from that set . - Returns
{ sourceCount, mirrorCount, missingSnapshots }— each missing entry includesshort_id,time, andsize(fromsummary.total_bytes_processed).
Critical detail: snapshots are matched by timestamp (
s.time), not by snapshot ID. Two snapshots are considered equivalent if theirtimestrings are equal.
Low-level restic snapshots command#
snapshots.ts in packages/core spawns restic snapshots --repo <url> --tag <scheduleShortId> --json and parses the output against snapshotInfoSchema . If the exit code is non-zero, the entire call throws and the status endpoint will fail. If Zod validation fails, it also throws — both paths surface as a ResticSnapshotsCommandError .
The --tag filter is the sole scoping mechanism: only snapshots tagged with the schedule's shortId are returned. Snapshots created outside zerobyte, or tagged differently, are invisible to this endpoint.
Frontend Dialog#
MirrorSyncDialog (mirror-sync-dialog.tsx) fetches sync status via getMirrorSyncStatusOptions as soon as a mirror is selected . The query is only enabled when mirror !== null .
Rendering paths:
| State | UI |
|---|---|
| Loading | "Loading snapshot status…" |
missingSnapshots.length === 0 | "All N snapshots are already synced" |
| Missing snapshots exist | Paginated table with checkboxes, ID, date, size |
All missing snapshots are not pre-selected by default — selectedSnapshotIds starts as an empty Set and is reset whenever the active mirrorShortId changes . The "Sync N snapshots" button stays disabled until at least one snapshot is checked .
On submit, the selected short_id values are sent as snapshotIds in the POST body .
Sync Execution#
startMirrorSync builds a MirrorSyncExecutionPlan and hands it to commands.createMirrorSync(plan).start(). The command implementation lives in mirror-sync-command.ts.
Inside executeMirrorSync, the command:
- Sets task phase to
"preparing". - Resolves the
snapshotIdsfrom the task store (allowing late binding after the task is queued). - Calls
restic.copy()with--tag <scheduleShortId>scoping and optional per-snapshot IDs. - Optionally applies the schedule's retention policy after copy completes.
Both manual (user-initiated via UI) and post-backup (automatic, triggered after each scheduled backup) sync paths share this command. Post-backup syncs always carry a non-empty snapshotIds array; manual syncs may omit it to copy all missing snapshots .
Known Bug: Dialog Shows 0 Snapshots#
Issue #1139 (open as of 2026-09-03, zerobyte v0.42): the mirror sync dialog reports "Sync 0 snapshots" even when the source repository contains snapshots.
Observed setup: source = B2/S3 bucket, mirror = iCloud via rclone. The rclone config, /dev/fuse, and CAP_SYS_ADMIN were all verified working . Despite this, getMirrorSyncStatus returns sourceCount: 0 (inferred from empty missingSnapshots and the greyed-out button).
Most likely root causes to investigate:
-
Tag mismatch —
restic.snapshots()filters strictly by--tag <scheduleShortId>. If existing source snapshots were created before the schedule'sshortIdwas assigned or tagged differently, they will not appear. Confirm by runningrestic snapshots --repo <source-url> --jsondirectly and checking thetagsfield on each snapshot. -
restic snapshotsexit code > 0 on the source repo — Any non-zero exit causesgetMirrorSyncStatusto throw rather than return partial results . Check server logs forRestic snapshots retrieval failedmessages. -
Zod schema validation failure — If restic returns snapshot JSON with an unexpected shape (e.g., a missing required field),
snapshotInfoSchema.array().safeParse()fails and the endpoint throws . Check forRestic snapshots output validation failedin logs. -
rclone repo not yet initialised — If the iCloud repo has never been written to,
restic snapshotsagainst it may error. This would not directly affect source snapshot enumeration but can cause the concurrentEffect.allcall to fail entirely , returning nothing for either side.
Key Files & Entry Points#
| Layer | File | Role |
|---|---|---|
| Service | backups.service.ts | getMirrorSyncStatus, startMirrorSync |
| Command | mirror-sync-command.ts | Snapshot copy execution, task lifecycle |
| Restic | snapshots.ts | restic snapshots subprocess wrapper |
| UI dialog | mirror-sync-dialog.tsx | Snapshot selection UI |
| PR | #755 | Original feature design & rationale |
| Bug | #1139 | 0-snapshot enumeration report (open) |