Skip to content

Client Filesystem

Draft

Clients hold keys, so a client stores plaintext. A library routinely holds assets whose original is server-only, depending on the sync scope, so the layout represents an asset with or without its original. The layout is a contract: rebuild assumes exactly these names. The offline-first requirements it serves are owned by Local Gallery.

{library_root}/
├── media/{YYYY}/{YYYY-MM}/
│ ├── {uuid}.{ext} # original (plaintext; absent if not synced locally)
│ ├── {uuid}.cbor # canonical signed metadata sidecar
│ └── {uuid}.provenance.cbor # append-only signed provenance chain
├── cache/ # thumbnails/, meta/, transcodes/ — sharded {uuid[0:2]}/{uuid[2:4]}
├── index/library.sqlite # rebuildable query + vector index
└── .library/
├── version, config, lock
├── docs/{albums,stacks}/{uuid}.cbor # signed album and stack label records
└── quarantine/{uuid}.{ext} + {uuid}.reason.json
  • media/ is canonical: original, sidecar, and provenance chain. The library MUST be reconstructible from these three files alone. Stems are UUIDv7; extensions lowercase. The provenance file is never deleted, so a hard-deleted asset leaves a tombstone-with-history.
  • A trashed asset keeps its original, sidecar and chain in its media/ bundle until purge: trash is a signed delete record in the chain, not a location, so no file moves and a rebuild derives Trashed from the chain (Deletion State Machine). Purge removes the original, the sidecar and every cache/ derivative, and leaves the chain. A bundle holding only a chain headed by a delete is the asset’s peering tombstone; no file outside media/ records a purge, so a rebuild reproduces Purged from the chain alone.
  • The date bucket is fixed at import from the capture timestamp then known. A later capture-date correction does not move the bundle; the sidecar is authoritative and maintenance repairs drift opportunistically.
  • cache/ is derived and deletable at any time; never a source of truth. Formats: Thumbnails.
  • index/library.sqlite is a query cache over the sidecars, the sqlite-vec index for AI, and the substrate for view albums. It is migrated forward in place, not dropped: a rebuild cannot recover projections never written to disk (Versioning).
  • .library/: the lock file MUST prevent two processes opening one library. quarantine/ preserves irreplaceable bytes that failed validation, verbatim, with the rejection reason (Quarantine Surfaces). A version bump rebuilds derived structures only and never touches canonical files. docs/ holds each album’s and stack’s signed label record as canonical CBOR, plaintext at rest like a sidecar and sealed under the album’s AMK whenever it leaves the device.

Android and iOS use the app’s private sandbox with the same logical model. Rebuildable derivatives MUST NOT be placed in OS-managed cache locations: the OS evicts indiscriminately, and an expensive-to-regenerate thumbnail is not disposable.

SQLite may lag the filesystem. The client checks file existence before acting on a row and rebuilds from sidecars on structural inconsistency (Maintenance).

Capsule reclaims space itself. Eligible: cache/ and fetched-but-unpinned originals. Never eligible: canonical media/ files the device holds as source of truth, sidecars, provenance chains, and library.sqlite. An evicted representation re-fetches transparently via tiered fetch.

The reclaimable set is held within a user-configurable budget (capsule-library::library::cache_sweep):

  • Viewing stamps a last-access time; eviction is least-recently-accessed first.
  • At equal recency, evict original → preview → thumbnail. The metadata tier (sidecar + LQIP) is never reclaimed, so an asset always stays listable.
  • Pinned representations and device-owned originals are exempt from automatic eviction.

The budget follows the connection class (capsule-sdk::cache_budget). The user’s base budget (default 2 GiB) is retained as-is on unmetered, doubled on metered and constrained, tripled on adverse and quadrupled on offline, because re-fetching an evicted representation grows costlier in that order. Every class but unmetered also defers bulk transfers: a push on such a class runs staged whatever the device’s upload policy, so the index tier still publishes each asset while preview and original wait for an unmetered link. The client samples the class through a port before each sweep and each transfer, and re-plans when it changes; a desktop with no OS signal reports unmetered. The base budget is a user setting (capsule cache budget --set); capsule cache sweep runs the sweep under the current plan.

Releasing a device-owned original (via the user-driven path or streaming import) MUST pass the verify-before-destroy gate. Once released it is an ordinary server-only, re-fetchable asset.

The client surfaces the largest consumers and lets the user delete selectively, including pinned content. It never touches canonical media/ files, and releasing a device-owned original is gated as above.

  • Init/open round-trip (unit). Directories and version/config present; re-open is idempotent.
  • Date bucketing (unit). Asset lands in media/{YYYY}/{YYYY-MM}/; unexplained drift triggers repair.
  • Lock contention (smoke). A second process is refused with a structured error.
  • Sandbox placement (smoke, per platform). Library in app-private storage; survives cold start.
  • Index rebuild (smoke). Drop library.sqlite; re-open; queries match.
  • Eviction (unit). LRU order, tier order at equal recency, recency promotion, pin exemption; no canonical file touched; metadata tier never reclaimed.
  • Connection-class budget (unit). Each class yields its budget and bulk deferral; a class change mid-session re-plans without evicting pinned or device-owned originals.
  • Release gate (smoke). Non-durable verdict retains the original and surfaces it; durable releases.