Skip to main content

Backup Flow

The backup operation is orchestrated by the BackupManager in internal/engine/. Here’s the complete flow:

1. Initialization

Source identity is determined by SourceInfo:
  • type: e.g. “gdrive”, “local”, “onedrive”
  • identity: stable container identity
  • path_id: stable selected-root identity within the container
  • account: friendly display account label
  • path: friendly display path
  • drive_name: friendly container label
Previous snapshot matching prefers the new stable fields and falls back for backward compatibility:
  1. type + identity + path_id
  2. type + identity + path (bridge fallback)
  3. type + volume_uuid + path (legacy fallback)
  4. type + account + path (legacy fallback)

2. Source Scanning

Full scan (for local, sftp, gdrive, onedrive):
Incremental scan (for gdrive-changes, onedrive-changes):
For each file encountered:
  1. Look up the file ID in the old HAMT:
  2. Fast-check metadata (name, size, mtime, type, parents):
    • If identical and the source doesn’t provide a content hash, carry the old hash forward
    • This avoids false-positive diffs for metadata-only changes
  3. Determine action:
    • Unchanged: Re-insert into new HAMT by reference (structural sharing)
    • Changed or new: Queue for upload

3. Upload Phase

Changed/new files are processed by concurrent workers (default: 10 workers):

FastCDC Chunking

From internal/engine/chunker.go and the spec: FastCDC uses a rolling hash to find content-defined boundaries:
  1. Compute a rolling hash of a 64-byte window
  2. When the hash matches a pattern (e.g. last 20 bits are zero), create a boundary
  3. Enforce min/max size constraints
  4. The final chunk may be smaller than the minimum
Content-defined chunking ensures that inserting bytes at the start of a file doesn’t invalidate all subsequent chunks. Only the chunks containing modified data change.

4. HAMT Flush

After all files are uploaded and inserted into the HAMT:
Flush algorithm (BFS from root):
  1. Start a queue with the root node ref
  2. For each node in the queue:
    • If it’s in the in-memory buffer, write it to persistent storage
    • If it’s an internal node, add all child refs to the queue
  3. Discard any buffered nodes that were never visited
This avoids uploading intermediate superseded nodes created during tree construction.

5. Snapshot Commit

The commit point is updating index/latest. Until this write completes, the previous snapshot remains the “latest” and the repository is in a consistent state.

6. Lock Release

The backup lock is released, allowing other operations to proceed.

Restore Flow

The restore operation is orchestrated by the RestoreManager in internal/engine/.

1. Snapshot Resolution

2. HAMT Traversal

Walk the HAMT to collect all file metadata entries:

3. Topological Sort

Ensure parent directories are created before their children:
This handles Google Drive’s multi-parent semantics where a file can appear in multiple directories.

4. Path Reconstruction

Walk the parent chain of each entry to reconstruct the full relative path:
For files with multiple parents (Google Drive shared folders), only the first parent is used to construct the primary path. Other parents are ignored during restore.

5. ZIP Archive Creation

Write entries to a ZIP archive in topologically-sorted order:

6. Output

The ZIP archive is:
  • Written to stdout (CLI)
  • Returned as a byte stream (web API)
  • Saved to a file (with -o flag)

Performance Optimizations

Concurrent Upload

The backup manager uses a worker pool (default: 10 concurrent workers) to parallelize file uploads:

Chunk-Level Deduplication

Before writing a chunk:
The KeyCacheStore layer caches existence checks in a local bbolt database to avoid redundant Exists calls:
  • First check: network request → cache result
  • Subsequent checks: local cache hit (0 network requests)

Content-Level Deduplication

Before streaming a file, check if its content object already exists:
For sources that provide content hashes (Google Drive MD5, OneDrive SHA1), this is a huge optimization.

Packfile Bundling

Small objects (< 512KB) are bundled into 8MB packfiles:
  • Reduces API calls from thousands to dozens
  • LRU cache (128MB) keeps hot packs in memory
  • Typical metadata read: 0-1 network requests after initial pack fetch

Structural Sharing

Unchanged files reuse their filemeta refs, and unchanged subtrees reuse their HAMT node refs:

Error Handling

Transient Errors

Network errors during upload are retried with exponential backoff:

Permanent Errors

Permanent errors (authentication failure, permission denied) abort the backup immediately:

Partial Upload Cleanup

If a backup is interrupted, orphaned objects remain in the store but are not reachable from any snapshot. Running prune after an interrupted backup will:
  1. Mark all reachable objects from existing snapshots
  2. Sweep and delete any orphaned objects
  3. Repack fragmented packfiles
Orphaned objects are harmless. They consume storage but don’t affect backup correctness. Prune reclaims this space.

Diff Operation

The diff command leverages the HAMT’s structural diff:
The diff is structural: if two subtrees have the same root hash, they’re identical and the traversal skips them entirely. This makes diff extremely fast even for snapshots with millions of files.