Content and Storage¶
Docbank keeps original files and their versions. Fotobank records how those files form a photo library and creates replaceable files for browsing and editing. This page explains how data moves between those stores.
Authority model¶
Docbank is authoritative for every imported photo, video, RAW file, and XMP sidecar. Fotobank SQLite stores product meaning and the exact Docbank node, virtual path, version, SHA-256, and size for each file. NAS and flash artifact storage holds rebuildable files. Complete recovery archives live in explicitly initialized backup repositories; the running library reads its media from Docbank.
Import semantics¶
ImportService runs the existing pipeline on the daemon-owned catalog and
vault. The CLI sends a local source path through the authenticated operator
API and renders streamed progress; it opens no application storage. The same
import lock excludes overlapping imports and content recovery. Each import
uses the daemon's effective AI settings at the time it acquires that lock.
The importer copies files and leaves source paths and bytes untouched.
It observes size and modification time twice across imports.settle_interval,
then revalidates them immediately before creating content.
Related files are grouped before any reservation. A JPEG and camera RAW with the same directory and normalized stem become one asset; an XMP becomes a sidecar of the RAW when present, otherwise of the primary. XMP-only and ambiguous groups are rejected. The importer computes SHA-256 and size, reserves stable IDs and durable operations, creates each file in Docbank, records exact receipts, and makes the asset ready only after every operation is applied. Before the ready transition, the importer asks Docbank to ensure source metadata for the exact primary version and stores a queryable copy in Fotobank. It records the version, extractor, and checksum used to produce that copy. Thumbnail, full-text, and AI work starts after the asset is ready.
An owner can reserve a SHA-256 identity only once. Concurrent imports either create that reservation or resume the committed winner. Re-running an import after interruption reuses its asset, file, operation, and virtual-path IDs; Docbank's idempotent create then returns the original receipt.
Docbank SHA-256 is the content identity. Fotobank decides separately whether equal bytes mean a duplicate import, another file in an asset, or a distinct product item. Content deduplication does not decide product identity.
Docbank boundary¶
Only internal/content imports go.kenn.io/docbank. It owns:
- vault open and close;
- stable virtual-path construction;
- create requests with required expected SHA-256 and size;
- exact-version and current-version reads;
- exact-version source-metadata processing and projection into dependency-free values;
- exact-version canonical visual-preview processing and verified reads;
- backup repository creation, verification, isolated restore, and coordinated forgetting and pruning;
- bounded traversal of an owner's media subtree for reconciliation;
- error translation into Fotobank sentinels; and
- serialization of content mutations to bound local concurrency.
A coordinated backup declares the Fotobank SQLite snapshot as a host file and provides the callback that creates it. Docbank runs that callback inside its short metadata freeze, captures the host file in the same manifest as the content snapshot, and releases content writers before immutable backup bytes stream. Host files that contain credentials or tokens retain Docbank's sensitivity marker and require an explicit plaintext-backup opt-in. Backup and restore reports are projected into Fotobank types, and restore always targets a separate vault root rather than replacing the open vault. Manual and scheduled capture both reuse the daemon's live adapter. The adapter supplies Docbank with every configured NAS and flash-managed root as protected storage. Restore rejects their descendants and filesystem aliases before it creates or overwrites a target. One boundary set retains each configured alias, its validated destination, and the target resolved when the adapter opened. Restore, import, and checkout validation resolve that complete set again before use, so a retargeted storage alias does not expose either its earlier or current destination. Configuration validation keeps canonical roots for ordinary I/O but also preserves absolute unresolved versions of the paths the operator configured for this boundary.
BackupRepository.Restore can recover without an open source adapter. It
accepts protected storage roots from its caller and delegates restoration and
target coordination to Docbank. The product archive command supplies those
roots from configuration, restores into an empty separate target, and checks
the restored Fotobank catalog's references before reporting success. It does
not bootstrap a new source vault to recover an old one.
Fotobank owns scheduled retention selection: only points tagged
fotobank:scheduled are eligible, and selection runs only after a complete
archive is published. BackupRepository.Forget and BackupRepository.Prune
delegate mutation and repository locking to Docbank. Retained manual archives
continue to protect their referenced bytes during pruning. The scheduler
never removes repository files directly or prunes the live vault. See
backup retention for pack cleanup behavior.
internal/contentresolver is the shared product-to-content boundary above the
adapter. It resolves a ready asset and either its primary or a named attached
file, then binds an immutable version to that file's recorded Docbank node.
Owner and shared-read services apply authorization and hidden-media policy
before opening the resolved reference. Projection workers use the same
resolver without transport authorization.
Current references use the version cached on the file and require Docbank's SHA-256 and size to match the Fotobank projection. Historical references may name any immutable version of the same Docbank node; a version belonging to a different file is rejected even when the caller can access the asset. This is the read primitive used by downloads, projections, and checkout materialization.
The embedded vault is configured with loose compression disabled and Fotobank does not pack imported media. Callers still use public Docbank read APIs and must not derive blob shard paths or depend on the physical representation.
Virtual paths are internal stable names:
The path is allocated before content creation. Public APIs expose the asset UUID, never this path or the sequential Docbank node ID.
internal/content exposes sequential verified readers. The resolver converts
full-stream verification into ordinary read errors at EOF, so consumers must
drain a full stream and handle its terminal error before treating the read as
successful. Exact-version range reads serve video and HTTP Range responses; a
partial range proves catalog access and range bounds, not whole-object
integrity.
Cross-database writes¶
Fotobank SQLite and Docbank cannot commit atomically. Content-changing code uses this durable operation protocol:
- Allocate asset, file, and operation UUIDs and record the expected identity.
- Call Docbank with the stable virtual path and expected content.
- Record the returned node, version, SHA-256, and size in one Fotobank transaction.
- Ensure and project metadata from the exact primary version.
- Make the asset ready and enqueue later projections only when every file is mapped and its source metadata projection is recorded.
Repeating the same create with the same path and identity is idempotent. Different content at the reserved path marks the asset and all sibling operations as a durable conflict. Receipts are rejected after conflict and the asset cannot become ready. A later import of the same source identities resumes pending operations. The content recovery command adopts matching creates left across a process interruption, projects the recovered primary version before completing a fully applied asset, and reports unmatched Docbank files without changing them.
The owner routes serve the primary through /api/v1/media/{asset}/original
and attached RAW/XMP content through
/api/v1/media/{asset}/files/{file}/content. Both authorize through the asset
and read the recorded immutable Docbank version.
Artifact storage¶
internal/storage is the rebuildable artifact boundary. Its filesystem store
writes beneath each owner's opaque directory in the NAS artifact root. An
optional local decorator caches versioned thumbnail keys under
{flash.root}/thumbs; it rejects non-thumbnail keys rather than becoming a
second route for original bytes.
Storage keys are relative POSIX paths. Absolute paths, backslashes, empty segments, and traversal are rejected. Owner storage keys are one safe path component. Writers require the configured NAS root to exist and create only directories beneath an opened root-bound filesystem view; they never recreate the external root during an outage.
This store currently carries only rebuildable thumbnails. Thumbnail keys include the asset/media ID, thumbnail version, and requested size so regeneration never silently reuses an old source projection. The NAS artifact remains the backing copy when the local thumbnail cache is enabled.
For JPEG, PNG, GIF, WebP, and supported camera RAW originals, the thumbnail worker asks Docbank to produce or reuse the canonical preview for the recorded exact version. It validates that the preview belongs to the asset's primary file, verifies the complete preview stream, and derives Fotobank's UI sizes from those JPEG pixels. The current built-in producer rejects JPEGs with an embedded ICC color profile, including tagged sRGB files. Import still stores the original; it does not guarantee a browsing preview. Unsupported and failed preview results become terminal thumbnail outcomes rather than falling back to decoding the authoritative original.
Root isolation¶
The Docbank root must not overlap NAS or flash-managed trees in either
direction. Configuration canonicalizes existing symlinks, rejects
symlink-plus-.. aliases, and compares case-insensitively for portable safety.
Normal commands reject unresolved symlink ancestors. Server validation may
resolve a NAS symlink through its missing external target so health endpoints
remain reachable, while still checking the intended target for overlap before
startup.
The import command applies the same canonical, symlink-aware comparison to its source root before discovery. The source cannot overlap the vault, NAS, or flash root, so authoritative content and managed artifacts cannot become an import source. Discovery traverses that canonical root and rejects supported media paths that are symbolic links instead of following them beyond the validated tree.
Writable checkouts¶
A checkout is an ordinary writable copy for external tools. An edit becomes a
stored version only after an explicit commit. fotobank checkout estimate
reports the distinct file and byte count selected by explicit assets, albums,
inclusive capture-year ranges, or all ready visible assets. Hidden assets are excluded because the CLI has no
hidden-media unlock session. An all-assets checkout requires a caller-supplied
byte ceiling so a second full archive copy is never created implicitly.
Selector validation and candidate loading share one SQLite read transaction,
so an explicit asset cannot disappear between those two views.
fotobank checkout create requires an existing empty directory outside the
Docbank, NAS, and flash-managed roots. It resolves every selected file to its
recorded immutable Docbank version and publishes a verified ordinary copy with
an atomic no-replace operation inside a root-bound filesystem view. It never
hardlinks a writable file to a Docbank content-addressed object. Temporary
copies use a reserved top-level staging directory rather than a user filename
directory. Fotobank removes it before activation and syncs the checkout root on
platforms that support directory synchronization.
The content adapter opens a checkout root, verifies that exact directory
against the canonical path and storage boundaries, then returns an opaque
single-use capability retaining the open directory. Immediately before
transfer, the capability resolves the catalog path again, reapplies the Docbank,
NAS, and flash boundaries, and checks that the path still names the retained
directory at its original canonical path. Materialization uses that same handle
instead of reopening the root. A renamed, replaced, or newly aliased root is
rejected rather than leaving the catalog pointed at a different directory from
the materialized files. Once a checkout row exists, every later failure attempts
the error
transition through a short cleanup context independent of caller cancellation;
if that database write also fails, the returned error reports both failures.
Materialization repeats the retained-directory check before recording each
entry and before activation. A root moved during creation therefore leaves an
explicit errored checkout instead of an active ledger for a different path.
Estimate, create, and commit run through the authenticated local operator
interface using the server's database and vault. The server holds the shared
database lifetime lock before opening SQLite until its pools close. List and
status also query saved state through the daemon; neither command opens a local
database. Archive restore runs only in recovery mode and writes to separate
storage. Creation also holds its exclusive creation lock.
After acquiring the creation lock, the next creator marks any
remaining building rows for its owner as interrupted; a live creator cannot
be misclassified because it would still hold the lock. A building or active
checkout reserves its entire root tree: another checkout cannot use that root,
an ancestor, or a descendant. An operator can empty a partial interrupted
directory and retry it after recovery moves the old row to error.
The capture_date layout keeps every asset's related files together beneath
YYYY/MM/DD/{asset-uuid}/; assets without capture time use
undated/{asset-uuid}/. Original filenames must also be portable to Windows:
checkout rejects reserved device names, reserved characters, control
characters, and trailing dots or spaces on every host. It does not silently
rename originals because normalization can change tool-visible names or
collapse distinct source names onto one working path.
Each entry records its relative path, exact base version, SHA-256, size,
modification time, and platform file identity. Fotobank reopens the final
root-bound path and derives that observation from one file handle; a file
replaced during publication cannot be recorded as a clean entry. Here clean
means the last Fotobank observation matched the base version; it is not a
continuous claim about a writable file after that observation. The operator
must not open or edit the working root until checkout creation returns.
Checkout creation is building until every entry is published and then becomes
active. Selector and file identifiers are detached historical snapshots, so
deleting a source album or asset does not erase the checkout's saved selection
or file bindings. A materialization failure makes the durable checkout error
without pretending the partial working tree is usable.
The server periodically walks every active checkout. A complete scan is the correctness path; filesystem notifications are not required. The scanner opens the cataloged root through the same Docbank and managed-storage boundary used for creation, then traverses it through a root-bound filesystem handle. It never follows a working-file symlink as media.
Changed metadata starts or refreshes a durable settle observation. Size and
modification time and platform file identity must remain unchanged across
separate scans for at least checkouts.settle_interval before Fotobank opens
and hashes the file. The observation survives restart. The unchanged fast path
also requires a non-empty matching file identity; on a platform where identity
is unavailable, Fotobank hashes instead. A stable hash equal to the base
returns a tracked entry to clean, including timestamp-only edits; a different
hash changes it to pending. A missing working file changes the entry to
missing without deleting or modifying its Docbank version. Hashing observes
scan cancellation, while permission and I/O failures on tracked files become
visible entry errors rather than being treated as concurrent edits. Failures
reading untracked files are reported and retained for retry without preventing
missing-file reconciliation elsewhere. A traversal failure is contained to its
affected subtree: tracked files there become errors and pending untracked
candidates remain available for retry, while accessible parts of the checkout
still complete reconciliation. Untracked paths that cannot use the catalog's
portable slash-separated form are reported without entering the durable queue
or stopping reconciliation.
Stable untracked files remain in checkout_scan_candidates with an empty file
ID and pending state for the later new-file import lifecycle. The scanner
always skips its reserved staging directory. Operator-supplied
checkouts.ignore_patterns use Go path.Match syntax and apply only to
untracked files, so they can exclude tool-specific transient files without
hiding a tracked media edit.
fotobank checkout list and fotobank checkout status <checkout-id> expose
the durable ledger without opening Docbank or inspecting the working tree. The
list view reports checkout state and entry counts. The status view includes the
saved selection and every non-clean entry with its recorded error and version
identity. Both operations run through the owner-scoped checkout service, so a
checkout belonging to another owner is indistinguishable from a missing one.
Their JSON forms are the automation boundary for agents and scripts.
checkout retire <checkout-id> --confirm marks the ledger retired through
POST /api/v1/operator/checkouts/{id}/retire. Without confirmation, the CLI
only shows recorded status. Retirement preserves entries, selections, pending
observations and errors for inspection. It releases the live-root reservation
but neither accesses nor deletes the working directory, and does not write
Docbank versions. Missing working directories do not prevent retirement.
Repeated retirement returns the retained status without changing it.
The daemon passes one checkout lifecycle read/write lock to the scanner, materializer, committer and checkout service. Scans, creates and commits hold the read side; retirement holds the write side, waiting for current work. A waiting retirement also blocks new checkout work across the deployment. The scanner rereads state after acquiring the lock, so a previously listed checkout cannot be scanned after retirement. Retired checkouts remain visible in list/status, but cannot be committed. This coordinates daemon operations, not edits by external applications. Reported observations are not a fresh filesystem scan. A released root must still meet normal checkout creation requirements, including being empty, before it can be reused.
Scanning does not write new Docbank versions. fotobank checkout commit
<checkout-id> consumes settled tracked pending entries. Each replacement is
bound to the Docbank node revision for the entry's exact base version, so a
newer head becomes a durable checkout conflict instead of being overwritten.
The pending checkout entry is also the recovery record across the Fotobank and Docbank databases. If Docbank committed the expected bytes but Fotobank did not record the receipt before interruption, the next commit adopts that exact current node version. Any other current identity is a conflict. Applying a receipt advances the product file and checkout base together. A primary-file commit first ensures and projects source metadata for the new exact version; processing failure leaves the checkout entry pending, so retry adopts the already-written Docbank version and tries again. Successful publication stores the new metadata fence, queues a thumbnail, invalidates current AI and vector projections, and refreshes the lexical corpus in one Fotobank transaction without deleting immutable Docbank history.