Skip to main content
A snapshot in Cloudstic is a complete, immutable, point-in-time representation of your backup. Unlike incremental backup systems that require replaying deltas, every Cloudstic snapshot is a full checkpoint that can be restored independently.

Snapshot Object

A snapshot is a JSON object stored at snapshot/<sha256>:
Location: internal/core/models.go:78-89

Fields

Location: docs/spec.md:174-199
Every snapshot is a complete checkpoint - you can restore any snapshot without accessing previous backups.

The HAMT Tree

The heart of a snapshot is the HAMT (Hash Array Mapped Trie) - a persistent Merkle tree structure that maps file IDs to their metadata references.

What is a HAMT?

A HAMT is a data structure that combines:
  • Hash table performance (O(log₃₂ n) lookup)
  • Persistent updates (old versions remain valid)
  • Merkle tree properties (structural sharing via content addressing)
Location: internal/hamt/hamt.go:1-602

Structure

Cloudstic uses a 32-way branching HAMT:
Configuration:
  • Bits per level: 5
  • Branching factor: 32
  • Max leaf size: 32 entries
  • Max depth: 6 levels
Location: internal/hamt/hamt.go:14-18

Node Types

Internal Node

Fields:
  • bitmap (uint32) - Bit vector indicating which child slots are populated (popcount compression)
  • children - Array of node/<hash> references
Location: internal/core/models.go:55-60 and docs/spec.md:141-153
The bitmap is a compact representation of which children exist. Bit i set means child slot i is populated. The children array contains only the populated slots (no null entries).

Leaf Node

Fields:
  • entries - Array of (key, filemeta ref) pairs
    • key: File ID from the source (Google Drive file ID, relative path, etc.)
    • filemeta: Reference to the file’s metadata object
Location: internal/core/models.go:62-66 and docs/spec.md:154-167

HAMT Operations

The HAMT supports purely functional operations:
Location: internal/hamt/hamt.go:41-107
All mutations are purely functional - Insert() returns a new root reference while the old root remains valid. This enables structural sharing between snapshots.

Structural Sharing

The HAMT is a Merkle tree: each node’s hash depends on its children’s hashes. When you modify one entry, only nodes along the path from that entry to the root need to be rewritten.

Example: Modifying One File

What changed:
  • 1 leaf node (E' contains the updated filemeta ref)
  • 2 internal nodes (B' and root_b have different hashes due to child changes)
  • Nodes A, C, D, F are reused by reference (same hashes)
Storage cost:
  • 1 new filemeta object (~300 bytes)
  • 3 new node objects (~150 bytes each)
  • Total: ~750 bytes for a metadata-only change in a 1M-file backup
Location: docs/spec.md:306-320
For a 1 million file backup where 100 files change:
  • 100 new filemeta objects (~30KB)
  • ~20 new HAMT nodes (~5KB)
  • Total metadata: ~35KB instead of re-writing the entire tree

Path Key Computation

To distribute keys evenly across the HAMT, the file ID is hashed:
Location: internal/hamt/hamt.go:144-146 At each level, 5 bits of the path key determine which child to follow:
Location: internal/hamt/hamt.go:148-162 Example:
  • At root (level 0): follow child 14
  • At level 1: follow child 31
  • At level 2: follow child 11
  • At level 3: reach leaf with matching entries

TransactionalStore

During a backup, the HAMT tree is updated incrementally as files are processed. To avoid writing intermediate nodes that may be superseded, Cloudstic uses a TransactionalStore.

Workflow

Location: internal/hamt/cache.go
Only nodes reachable from the final root are flushed to the object store. Intermediate nodes that were superseded during the backup are discarded.
This prevents orphaned nodes from accumulating in the store.

FileMeta Structure

Each HAMT entry points to a filemeta/<hash> object:
Location: internal/core/models.go:29-43 and docs/spec.md:103-130

Fields

Multi-parent support: A file can have multiple parents (Google Drive allows this). The parents array contains references to parent filemeta objects, not raw file IDs.For newly written snapshots, Cloudstic reconstructs full relative paths from the parent chain plus name instead of persisting a redundant paths array in every filemeta object.

Content Objects

The content_ref field in a FileMeta points to a content/<hash> object (if empty, falls back to content_hash for backwards compatibility):
For very small files (< 4KB), data is inlined:
Location: internal/core/models.go:20-27 and docs/spec.md:84-100

Chunk Objects

Each chunk reference points to a chunk/<hash> object containing raw file data:
  • FastCDC boundaries: 512KB min, 1MB avg, 8MB max
  • Deduplicated by HMAC-SHA256 (encrypted) or SHA-256 (unencrypted)
  • Compressed with zstd
  • Encrypted with AES-256-GCM (if encryption enabled)
Location: docs/spec.md:65-82

Complete Snapshot Hierarchy

Here’s the full object graph for a snapshot: Object counts for a typical 1TB backup:
  • 1 snapshot (~500 bytes)
  • ~100,000 HAMT nodes (~500KB compressed)
  • ~1,000,000 filemeta objects (~100MB compressed)
  • ~50,000 content objects (~5MB)
  • ~1,000,000 chunks (~1TB raw data, ~800GB compressed+encrypted)

Snapshot Discovery

Snapshots are discovered via two mechanisms:

1. Latest Pointer

Location: internal/core/models.go:92-96 This mutable pointer provides fast access to the most recent snapshot.

2. Snapshot Catalog

Location: internal/core/models.go:98-110 and docs/spec.md:223 The catalog contains lightweight summaries of all snapshots, avoiding the need to fetch each snapshot object individually during listing operations.
The catalog is self-healing: if corrupted or missing, it’s rebuilt by listing snapshot/* objects and fetching their metadata.

Change Tokens (Incremental Sources)

For sources that support incremental scans (e.g., Google Drive Changes API), the snapshot stores an opaque change token:
Location: docs/spec.md:203-212 On the next backup:
  1. Read the previous snapshot’s change_token
  2. Pass it to the source’s WalkChanges() method
  3. Source returns only files changed since that token
  4. New snapshot stores the updated token
This dramatically reduces scan time for large backups where few files change.

Snapshot Operations

Listing Snapshots

Output:
Location: cmd/cloudstic/main.go (runList)

Inspecting a Snapshot

Output:
Location: cmd/cloudstic/main.go (runLs)

Comparing Snapshots

Output:
Location: internal/hamt/hamt.go:70-83 (Diff method)
The Diff operation leverages the HAMT structure for efficient comparison - it only traverses subtrees where hashes differ.

Restoring a Snapshot

Restores the entire snapshot as a ZIP archive. Location: internal/engine/restore.go

Deleting a Snapshot

Deletes the snapshot object. Use --prune to also run garbage collection and reclaim space from unreferenced objects. Location: internal/engine/forget.go and docs/spec.md:276-279

Garbage Collection

Snapshots share objects (chunks, content, filemeta, nodes) via content addressing. Deleting a snapshot doesn’t immediately reclaim space - you need to run prune:
Algorithm:
Location: docs/storage-model.md:64-78 and docs/spec.md:280-285
Prune requires an exclusive lock (index/lock.exclusive). Any backup or restore that starts while prune is running will fail immediately with a lock error, and prune itself will fail immediately if a backup or restore is already in progress. If you run prune in a scheduled job, ensure it does not overlap with backup schedules.

Source Identity

The source field enables multi-source repositories:
For local sources backing up portable drives, identity is anchored to the partition UUID:
Location: internal/core/models.go and docs/spec.md Snapshots with different source identities are treated independently:
  • Retention policies can target specific sources
  • Incremental backups look for the previous snapshot with the same source identity
  • Different Google accounts in the same repo won’t interfere
  • Portable drives are matched by stable identity regardless of hostname or mount point
Legacy snapshots may still include volume_uuid and volume_label; new releases keep reading them for compatibility fallback.

Further Reading