Embed in Go¶
Use go.kenn.io/docbank to own a document vault inside your Go process.
Your application can store, version, retrieve, and back up documents without
starting or discovering a daemon. The embedded API uses the same tree, content
checks, storage rules, and exclusive vault lock as standalone Docbank.
Use an embedded vault when the application itself should own document lifecycle. Use the HTTP API when independent processes need to share one standalone vault.
Inspect format coverage¶
Vault.FormatCoverage returns the immutable capability snapshot captured after
the configured rendition providers were constructed. Vault.LookupFormat
resolves a catalog ID or extension against that same snapshot:
coverage, err := vault.FormatCoverage(ctx)
if err != nil {
return err
}
lookup, err := vault.LookupFormat(ctx, "wpd")
if err != nil {
return err
}
Returned maps and slices are defensive copies. Changing them does not alter
later calls. Both methods follow the normal vault lifecycle and return
ErrClosed after Vault.Close. See Format coverage
for the seven capabilities and five states.
Transfer explicitly exported email¶
Call Vault.RegisterMailboxArchive(ctx, id, description) once for a stable
source archive identity. Then call Vault.TransferEML(ctx, request, reader)
with a docbank.MailboxTransferRequest: archive ID, stable occurrence
reference, SHA-256, byte size, settings identity, destination directory ID
and document name. Docbank reads only the supplied reader. It does not
discover source applications, accounts or host paths.
The returned receipt binds the ordinary message, decoded attachment documents
and exact retained versions. Equal retries return that receipt. Changing the
source requires ExpectedRevision. A changed target conflicts, and a trashed
target returns a tombstone. These calls use the vault's existing lifecycle and
mutation ownership. See Explicit EML transfers.
Create a vault service¶
Each root is an independent archive. One process may open several non-overlapping roots at once. The same root, an ancestor, or a descendant is rejected while another daemon, embedded vault, or restore target owns it, even when a filesystem alias spells that tree differently.
package archive
import (
"bytes"
"context"
"io"
"go.kenn.io/docbank"
)
func StoreSession(ctx context.Context, root string, sessionID string, jsonl []byte) error {
vault, err := docbank.New(ctx, docbank.Config{Root: root})
if err != nil {
return err
}
defer vault.Close()
_, err = vault.Put(
ctx,
"/sessions/"+sessionID+".jsonl",
bytes.NewReader(jsonl),
docbank.PutOptions{MediaType: "application/x-ndjson"},
)
if err != nil {
return err
}
content, err := vault.OpenContent(ctx, "/sessions/"+sessionID+".jsonl")
if err != nil {
return err
}
defer content.Reader.Close()
_, err = io.Copy(io.Discard, content.Reader)
if err != nil {
return err
}
return content.Reader.Verify()
}
An embedded application can append an origin learned after ingestion with the same immutable fact model:
appended, err := vault.AppendProvenance(ctx, receipt.Node.ID,
docbank.ProvenanceAppendOptions{
IfRevision: receipt.Node.Revision,
Source: docbank.ProvenanceSource{
Kind: "agent", Description: "triage", Reference: "opaque://laptop/report.txt",
},
})
The result contains the updated node, live path, appended fact, and receipt.
Set a positive IfRevision to require the node revision you inspected.
Zero makes the operation unconditional.
To correct an active caller-supplied fact on the same node, set Supersedes
to that fact's identity. Docbank records the source reference exactly as
supplied and never opens it.
Put creates missing virtual directories. Repeating the same bytes and media
type resolves to the current version. Changed bytes append an immutable content
version and keep the node ID. Supply PutOptions.Expected when the caller
already knows the SHA-256 and byte count and wants Docbank to reject a
mismatched stream before it commits any metadata. Supply a positive
PutOptions.IfRevision when replacing content derived from an earlier read.
Docbank rejects the write with ErrStaleRevision if the file has changed since
that revision, including when the new bytes happen to match the current head.
Use Create when an application owns an immutable key and must never replace
different content already stored there. It requires the expected SHA-256 and
size. An exact retry is idempotent. A different byte identity, media type, or
node kind returns ErrContentConflict without appending a version:
receipt, err := vault.Create(ctx, "/records/immutable.jsonl", reader,
docbank.CreateOptions{
MediaType: "application/x-ndjson",
Expected: docbank.ContentIdentity{SHA256: digest, Size: size},
Provenance: &docbank.ProvenanceSource{
Kind: "agent-session",
Description: "local session archive",
Reference: "sessions/01K123.jsonl",
},
},
)
CreateOptions.Provenance lets an embedded owner record where an immutable
document came from without inventing application-specific Docbank tables. The
kind and description identify the source system. The reference is an opaque
source-local URI, archive key, path, or other stable identifier. An optional
ModifiedAt records the source's own timestamp in canonical UTC form. Docbank
does not interpret the reference or grant it retention authority.
For a newly created document, the node, first content version, ingest record,
and provenance fact commit as one metadata transaction. If any part fails,
none is committed. An exact Create retry succeeds only when its active
provenance also matches, so a caller cannot accidentally claim source evidence
that was never recorded. Superseding that matching fact makes the original
request return ErrContentConflict unless another active fact still matches.
A request matching the corrected provenance remains idempotent when the
content and media type are unchanged.
AppendProvenance may supersede active caller-supplied facts on the same node,
including facts supplied through CreateOptions.Provenance. Operational CLI
and watched-folder ingest facts cannot be superseded, because they keep
re-ingest idempotent. Append newly learned origins alongside them instead.
Use Provenance to page through the history, newest first:
page, err := vault.Provenance(ctx, receipt.Node.ID, docbank.ProvenanceOptions{
Limit: 100,
})
if err != nil {
return err
}
for _, fact := range page.Items {
// fact.SourceReference remains the exact opaque identifier supplied above.
_ = fact
}
The node, live path, total, and page come from one read snapshot. Path is
empty for a trashed node. Provenance records origin. It does not prevent trash
empty, version pruning, or garbage collection. Applications must manage any
retention policy for external references themselves.
ContentIdentity always describes decoded document bytes, regardless of
whether those bytes are stored raw, zstd-compressed, or in a pack. SHA-256 is
the canonical logical identity, not a digest of a physical storage file.
Configure document processing¶
Pass named ProcessingProfileConfig values through Config.Processing.Profiles.
Each value binds a canonical portable profile to its provider implementations.
EmbeddingProviders, EmbeddingDisclosures, and Tokenizers use the embedding
binding names from that profile.
Every provider with an operator_network or hosted_provider trust boundary
requires a ProcessingRuntimeDisclosure.Endpoint when docbank.New
opens the vault. Set RenditionDisclosure for a rendition provider and an entry
in EmbeddingDisclosures for each network embedding binding. Opening fails if
an endpoint is missing or invalid. Docbank cannot infer it from a provider
interface.
The endpoint must describe the provider's configured HTTP(S) destination and
contain no URL credentials, query parameters, or fragment. A local_process
provider accepts an empty endpoint or in-process. Docbank fills omitted
processor and deployment identities from the provider and profile, and derives
metadata classes and retained artifact roles. Review the resulting
PlanProcessing response before granting consent. These disclosures are part
of its fingerprint.
For example, given a canonical profile with a rendition provider and a
text embedding binding, the matching constructed providers, and that
binding's tokenizer:
vault, err := docbank.New(ctx, docbank.Config{
Root: root,
Processing: docbank.ProcessingOptions{
Profiles: map[string]docbank.ProcessingProfileConfig{
"private": {
Profile: profile,
RenditionProvider: renditionProvider,
RenditionDisclosure: docbank.ProcessingRuntimeDisclosure{
Endpoint: "http://127.0.0.1:5001",
},
EmbeddingProviders: map[string]document.EmbeddingProvider{
"text": embeddingProvider,
},
EmbeddingDisclosures: map[string]docbank.ProcessingRuntimeDisclosure{
"text": {Endpoint: "http://127.0.0.1:8080"},
},
Tokenizers: map[string]document.Tokenizer{"text": tokenizer},
},
},
},
})
if err != nil {
return err
}
defer vault.Close()
Use the same endpoints when constructing the providers. A disclosure names a
destination but does not configure its transport. Import document from
go.kenn.io/docbank/document. See Document understanding in Go
for provider construction and Document processing
for planning, consent, retained results, and the private-deployment acceptance
runner.
Set DocumentSearchRequest.ContentFirst to prefer content matches, following the
HTTP search ordering contract.
Set DocumentSearchRequest.MediaSources to search only the transcripts of exact
recording sources. Reports then set MediaSourceSelection and list
MediaSelections, and evidence names its MediaSources, following the
HTTP media source contract.
Invalid selectors, or a mode or reranking choice they do not support, match
docbank.ErrMediaSearchInvalid.
Read exact evidence windows¶
Use ReadEvidenceWindow to read a bounded excerpt from a rendition you already
identified. Keep the file's vault, node, content-version and content SHA-256
alongside the attachment, build and sanitized-Markdown SHA-256 returned by
Rendition or document detail metadata. Pass all seven identities in an
EvidenceWindowRequest:
window, err := vault.ReadEvidenceWindow(ctx, docbank.EvidenceWindowRequest{
VaultUID: vault.ID(), NodeID: receipt.Node.ID,
ContentVersionID: receipt.Version.ID, ContentSHA256: receipt.Computed.SHA256,
RenditionAttachmentID: rendition.AttachmentID,
BuildID: rendition.BuildID, RenditionSHA256: rendition.SHA256,
Offset: 0, MaxChars: 8_000,
})
if err != nil {
return err
}
// Keep window.NextOffset to continue from the exclusive end of this window.
Offsets count Unicode scalars. Zero MaxChars selects 8,000. Other values
must be 1 through 16,000. ActualStart, ActualEnd, and NextOffset describe
the returned range, and ResponseBytes counts its UTF-8 bytes. A read starting
at EOF returns empty text with EOF: true. An offset beyond EOF matches
ErrInvalidRenditionWindow. Malformed references match
ErrInvalidEvidenceRequest.
Only the current, live content version and its active rendition are readable.
A rename preserves the reference. A content or rendition replacement, trash,
pruning, or retirement makes the old reference match ErrEvidenceUnavailable.
The read never runs processing, selects a newer rendition, or pins retention.
The vault lifecycle lease covers the internal blob read and cleanup and is
released before the function returns. Callers receive text, not an open stream.
The returned digest is the one the catalog records. A window read can end before whole-artifact checksum verification reaches EOF. Search chunk and embedding segment offsets use their own coordinates. Do not use them as Markdown offsets. Start a document overview at zero, or keep offsets from an earlier window. See the HTTP evidence contract for the equivalent authenticated route.
Retain and process a remote recording¶
Embedded callers can keep the recording identity separate from the protected
reference used to obtain a local file. Submission stores the pending occurrence
and performs no network access. The canonical_url field is optional for
configured-origin requests, and is the identity input for a generic URL
reference when present. See the canonical URL rules
for the permanent source identity. A Cap Cloud share or embed URL uses one
Cap source per video ID.
A Loom share or embed URL uses a route-qualified source because Loom does not
document equality between those routes. Submission keeps the Loom reference
unsupported and performs no network access.
remote, err := vault.SubmitRemoteRecording(ctx, docbank.RemoteRecordingRequest{
OperationID: "00000000-0000-4000-8000-000000000451",
ReferenceURL: "https://private.invalid/share/call?token=synthetic",
CanonicalURL: "https://recordings.invalid/share/call",
Occurrence: docbank.MediaOccurrenceInput{
Ref: "call-1", Revision: "1", Filename: "call.wav",
},
})
if err != nil {
return err
}
If that reply is lost, Vault.RemoteRecordingReceipt reads the saved receipt by
operation ID. ErrNotFound describes only that read. A submission still in
flight can commit afterward.
Obtain the file through the caller's own approved path, then attach it with
its SHA-256 and byte count. WAV and MP3 files use the existing media
rules. Remote MP4 originals use video/mp4, a .mp4 filename, and limits of
20 MiB, 2,088,960 coded pixels, 300,000 milliseconds, and 18,000 frames. The
service's default byte limit is 512 MiB, its hard maximum is 1 GiB, and the
audio duration limit is 24 hours.
original, err := vault.ImportRecordingArtifact(
ctx, docbank.MediaArtifactRequest{
OperationID: "00000000-0000-4000-8000-000000000452",
SourceID: remote.SourceID,
OccurrenceID: remote.OccurrenceID,
Kind: "media",
Origin: "supplied",
Filename: "call.wav",
MediaType: "audio/wav",
SHA256: recordingHash,
ByteLength: recordingSize,
Content: recording,
})
if err != nil {
return err
}
Import a caption or transcript only after the original is bound. A caption
uses application/x-subrip and the built-in supplied-captions profile. It
keeps cue timing and supplied provenance and supports lexical and auto search.
Semantic and hybrid search are not configured for supplied captions. A
transcript uses the built-in supplied-transcript profile after a plan and
consent grant.
caption, err := vault.ImportRecordingArtifact(
ctx, docbank.MediaArtifactRequest{
OperationID: "00000000-0000-4000-8000-000000000453",
SourceID: remote.SourceID, OccurrenceID: original.OccurrenceID,
Kind: "caption", Origin: "supplied", Provider: "loom", Filename: "captions.srt",
MediaType: "application/x-subrip", SHA256: captionHash,
ByteLength: captionSize, Content: captionReader,
})
if err != nil {
return err
}
selector := docbank.ProcessingSelector{
NodeID: nodeID, ContentVersionID: original.ContentVersionID,
Profile: "supplied-captions",
}
plan, err := vault.PlanProcessing(ctx, docbank.ProcessingPlanRequest{
Selector: selector,
})
if err != nil {
return err
}
if _, err := vault.GrantProcessingPlanConsent(
ctx, docbank.ProcessingConsentGrantRequest{
PlanRequest: docbank.ProcessingPlanRequest{Selector: selector},
PlanFingerprint: plan.Fingerprint,
}); err != nil {
return err
}
receipt, err := vault.RetryMedia(ctx, "00000000-0000-4000-8000-000000000454",
remote.SourceID, docbank.MediaProcessingRequest{
Profile: "supplied-captions",
SuppliedInputID: caption.SuppliedInputID,
})
RetryMedia selects the caller's newest visible occurrence for the source
and returns once the work is durably queued. It cannot select an older
occurrence. Use PlanProcessing and StartProcessing with that recording's
node and current content version instead. Read MediaStatus for the newest
attempt and its coverage. Coverage follows the source version selected by the
visible occurrence, so a transcript for older bytes cannot cover a later
recording revision. A recognized Cap Cloud or Loom link returns
unsupported even when Acquire is set, because neither manual reference path
has a supported automatic download owner. Import the caller-held file with
ImportRecordingArtifact. Other remote acquisition remains unavailable.
Origin registration is daemon configuration. Embedded vaults register no
self-hosted Cap origins, so the embedded API does not apply their registered
origin identity rules.
Read the published transcript with the same ID tuple used for processing:
transcript, err := vault.MediaTranscript(ctx, docbank.MediaTranscriptRequest{
SourceID: remote.SourceID,
SourceVersionID: receipt.SourceVersionID,
ContentVersionID: receipt.ContentVersionID,
})
EvidenceState is independent of CoverageState and OperationState.
EvidenceState == "ready" is the only state with text. Units preserve
timing and speaker facts, and Origin distinguishes supplied from
generated evidence. A mismatched content version returns stale without
text, so callers must check the complete tuple before displaying a transcript.
Extract and read source metadata¶
EnsureSourceMetadata verifies and processes one immutable content version
with Docbank's current local extractor, then returns its typed metadata. The
result carries the complete ContentVersion, so callers can bind the facts to
the SHA-256 and node version they describe:
metadata, err := vault.EnsureSourceMetadata(ctx, receipt.Version.ID)
if err != nil {
return err
}
for _, field := range metadata.Fields {
// Interpret fields by namespace, key, and typed value.
_ = field
}
EnsureSourceMetadata runs locally and returns when extraction finishes.
It processes only the requested version and reuses the current extractor's
result when one exists. It does not start background workers.
The method holds the vault's mutation lock during extraction. It verifies the
complete original, including large media files, then applies the
format-specific parsing limits.
Concurrent Put, Create, and maintenance calls wait. If Docbank cannot open
or verify the source bytes, it returns ErrContentUnavailable.
Use SourceMetadata for a read-only lookup. It returns ErrNotFound when no
result has been published. Both methods return all local fields, including
sensitive fields. Your application decides which fields it may disclose.
Publish email attachment documents¶
Use EnsureEmailMetadata to retain the MIME inventory for one email version.
Then call PublishEmailDocuments with a
document.EmailDocumentPublicationRequest: an operation ID, the parent
node/version/hash/size, the returned generation and attachment IDs, and a
destination directory ID with its current revision. No host filesystem path
is accepted. This operation does not run automatically on import.
Publication verifies retained payload bytes and creates ordinary file documents
for the full known attachment inventory in one transaction. Inline resources
are included, but body alternatives and multipart containers are excluded. An
attached email becomes a child email, whose own attachments can be published
separately. An explicitly attached single-part root also becomes a child.
Parts inside encrypted containers stay encrypted relations without child files.
The receipt says whether the MIME inventory is complete or partial. It
records unavailable, unsupported, failed, or encrypted parts and does not
create empty files for them.
Equal bytes share storage but get separate document occurrences. Filenames use
the safe MIME name plus operation and occurrence identifiers. A
reuse selection may instead identify an existing child version with matching
bytes and its node revision. Raw filenames remain in the MIME inventory.
Renames, later content versions, and decoder reprocessing never move an old
relation to a different version.
Keep the operation ID and request unchanged when retrying. The same request
returns the original receipt, including after backup and restore. A changed
request conflicts. EmailDocumentPublication reads that receipt.
EmailDocumentRelations selects either one parent version or one child
version and returns current processing status with each relation. Pages
default to 100 rows, accept at most 250, include a total, and return the next
operation/order pair when another page exists. Publication accepts at most
1,000 MIME parts, 128 MiB per payload, and 256 MiB of decoded payloads. HTTP
request and response bodies are limited to 2 MiB.
Publication itself does not prove indexing or authorize provider disclosure.
RequestEmailDocumentProcessing submits one receipt occurrence to the ordinary
rendition scheduler with a processing profile, execution identity, artifact
policy, principal, scope, and consent classes. Existing consent must authorize
those exact inputs and retained outputs. GrantProcessingConsent grants that
consent. RevokeProcessingConsent advances the principal/scope revocation
fence. Publication and processing requests never grant consent or inherit it
from a parent email. The profile must match a configured provider. Docbank
inspects the child bytes and rejects execution metadata that differs from the
prepared upload before enqueueing work. The embedded method runs the provider
and waits for completion. The HTTP endpoint returns after enqueueing for the
daemon worker. Both paths check consent again before provider access and
publication.
Relation status reports indexed for complete searchable output from ordinary
text extraction or an active rendition of that child version. Partial or
truncated renditions report partial, and empty output reports none.
Text-extraction failures report failed with reason text_extraction_failed.
Completed jobs without serving output report decoded with reason
rendition_not_serving. Other states include pending, unsupported,
encrypted, and unavailable. Ordinary search returns child matches. QueryV1
does not add an email-family traversal predicate.
Trash and restore keep relations and do not cascade to children. A referenced
version cannot be permanently deleted or pruned. A targeted purge of a
referenced MIME inventory conflicts. A vault-wide purge skips it and purges
other eligible derivatives. Conflicts identify the blocking publication
operation. To release those references, call RemoveEmailDocumentPublication
with the operation ID and its request digest. This removes that receipt and
its relations, preserves ordinary children, and gives up the operation's retry
guarantee. Other receipts keep their own references. Ordinary audit and
content-retention rules still apply. Use a fresh operation ID for a new
publication after release, or reuse the existing child versions. A generated
filename collision returns email_document_conflict and leaves the
publication uncommitted.
CLI users can inspect and release blocking receipts
through the daemon with docbank email-documents.
The typed client names the principal/scope consent operations
GrantScopedProcessingConsent and RevokeScopedProcessingConsent to distinguish
them from consent for a configured processing plan.
The authenticated HTTP and typed client surfaces expose the same operations:
| Operation | HTTP route |
|---|---|
| Publish | POST /api/v1/email-document-publications |
| Read receipt | GET /api/v1/email-document-publications/{operation_id} |
| Release receipt | DELETE /api/v1/email-document-publications/{operation_id} with request_digest |
| Read relations | GET /api/v1/email-document-relations with parent_version_id or child_version_id |
| Request processing | POST /api/v1/email-document-processing |
| Grant processing consent | POST /api/v1/processing/consents |
| Revoke processing consent | POST /api/v1/processing/consents/revoke |
These routes require the master API key and are outside the current browser
session capability. All relation and receipt DTOs live in
go.kenn.io/docbank/document. Consumers can use the same identities for
navigation and downloads without inferring parentage from names or hashes.
Read canonical visual previews¶
EnsureVisualPreview synchronously processes one immutable content version
when the current built-in recipe has no recorded result. It returns the active
preview. If the recipe was already recorded, another recipe's active result
stays selected. Ready results identify the preview bytes and dimensions.
Unsupported and failed results carry a stable failure code and no content.
preview, err := vault.EnsureVisualPreview(ctx, versionID)
if err != nil {
return err
}
if preview.State == document.VisualPreviewReady {
content, err := vault.OpenVisualPreview(ctx, versionID)
if err != nil {
return err
}
defer content.Reader.Close()
// Reach EOF or call Verify before trusting the bytes.
}
OpenVisualPreview uses the same verified-reader contract as original
content. It returns ErrVisualPreviewUnavailable for a cataloged unsupported
or failed result and ErrNotFound when no preview result exists.
VisualPreview remains a read-only lookup. Opening an embedded vault does not
start a preview worker. Applications choose when to call the synchronous
producer. The built-in producer supports JPEG, PNG, GIF, still WebP, and
supported embedded JPEG previews in ARW, DNG, CR2, NEF, and RAF camera RAW
files. See Visual previews for format limits,
output size, and recipe selection.
Use VisualPreviewForSize, EnsureVisualPreviewForSize, and
OpenVisualPreviewForSize with VisualPreviewGrid, VisualPreviewFit, or
VisualPreviewLarge to read, produce, or stream one specific retained recipe.
Their maximum edges are 512, 2560, and 4096 pixels. Grid and fit leave the
legacy active head alone. A newly produced large result becomes active.
Reusing a cached result preserves a different head.
docbank.VisualPreviewSupportsMediaType(mediaType) checks normalized media
types without reading or decoding a source. It reports whether a decoder path
exists. Individual files can still return unsupported or failed results.
Inspect and repair stored content¶
Put and Create receipts include Physical. This field describes the raw,
zstd, or packed representation on disk. Use it to inspect storage. Use
ContentIdentity to identify document bytes.
Put is an idempotent content write, not an integrity repair primitive. Kit's
structural dedup can reuse an existing canonical representation without hashing
it, and the catalog can keep reading a packed copy even when a loose copy
exists. When an application has trusted bytes for a known SHA-256 and size,
RepairContent verifies the complete stream before replacing the physical
copy the catalog reads. It preserves every node and historical version
reference to that identity. Repairing packed content makes the catalog read a
verified loose copy. A later repack reclaims the now-dead packed bytes.
Choose loose compression¶
New content remains loose until you call Pack. The standalone daemon selects
zstd when a new loose object is at least 4 KiB and compression saves at least
10%. Embedded vaults keep raw loose storage by default. An owner can match the
daemon policy or choose application-specific thresholds:
vault, err := docbank.New(ctx, docbank.Config{
Root: root,
LooseCompression: docbank.LooseCompressionOptions{
Enabled: true,
MinBytes: 4 << 10,
MinSavingsPercent: 10,
},
})
Docbank keeps zstd only when the logical size meets MinBytes and the completed
encoding saves at least MinSavingsPercent. Otherwise it publishes raw loose
content. Enabling compression does not migrate or rewrite existing objects.
RepairContent preserves an existing loose object's raw or zstd encoding. It
applies this policy only when trusted bytes replace a packed or missing
physical copy. The zero value writes new loose content as raw.
Backup restore applies its own
managed packing and compression policy, independently of these write options.
Mixed raw, zstd, and packed content remains readable through the same
verified API. Receipts report the chosen physical
encoding and stored size without changing the logical SHA-256 or size.
PutReceipt.Created reports a new logical node, while PhysicalCreated
separately reports that the operation published new final loose authority.
An eligible write temporarily needs scratch space for both the raw object and
its compressed candidate before Docbank chooses one to publish.
LooseBacklog reports how much indexed loose content remains eligible for a
pack pass, including both logical and physically stored bytes and a
split between raw and compressed object counts. Applications can therefore
schedule packing from physical storage growth without walking loose objects.
The report does not make packing automatic.
Identify a vault and verify reads¶
vault.ID() returns the archive's UUID. JSONL backup and restore preserve
that identity even when the restored vault has a different filesystem root, so
applications can tell archives apart without treating paths as identity.
Do not trust an OpenContent stream until it reaches terminal io.EOF or
Verify succeeds. Early Close does not drain the stream. Vault.Close
waits for active operations and streams, closes storage, and releases the vault
lock.
OpenContent and OpenVersionContent wrap ErrContentUnavailable when the
catalog-authorized physical content cannot be opened or its physical size
disagrees with metadata. Metadata lookup failures keep their existing
ErrNotFound, ErrNotFile, or ErrClosed classification instead. A canceled
physical open can match both context.Canceled and ErrContentUnavailable, so
callers that distinguish cancellation should check the context error first.
OpenVersionContentRange selects a non-empty decoded logical byte range from
one immutable version. Offsets and lengths address the same logical bytes
described by ContentVersion.BlobHash and Size, whether the bytes are
currently stored raw loose, zstd-compressed loose, packed, or in a secondary
store:
part, err := vault.OpenVersionContentRange(ctx, versionID,
docbank.ContentRangeOptions{Offset: 1 << 20, Length: 256 << 10},
)
if err != nil {
return err
}
defer part.Reader.Close()
_, err = io.Copy(dst, part.Reader)
Range-read costs depend on storage:
- Raw loose content uses filesystem offsets.
- Compressed loose content is decoded to a temporary file first.
- Packed content is decoded in full into memory. Reading a small range can therefore need RAM for the whole decoded object.
A range outside the version's bytes returns ErrInvalidContentRange. The
reader returns exactly the requested length, or io.ErrUnexpectedEOF if the
stream ends early. It keeps the vault open until the caller calls Close.
A successful partial read checks the selected location, decoded size, and
range bounds. It does not verify the whole object. Use OpenVersionContent
to read and verify all bytes, or run maintenance verification.
Traverse and mutate the tree¶
Children reads the live virtual tree one page at a time, so a large
directory is never loaded whole. Resolve a directory with Stat, then advance
through its direct children with Limit and Offset:
manifests, err := vault.Stat(ctx, "/manifests")
if err != nil {
return err
}
for offset := 0; ; {
page, err := vault.Children(ctx, manifests.ID, docbank.ChildrenOptions{
Limit: 500,
Offset: offset,
})
if err != nil {
return err
}
for _, child := range page.Items {
// Inspect this bounded page.
_ = child
}
offset += len(page.Items)
if offset >= page.Total || len(page.Items) == 0 {
break
}
}
Pages contain directories first and files second, name-sorted within each kind.
A zero limit uses DefaultChildrenLimit. One call cannot exceed
MaxChildrenLimit. The total and page come from one metadata snapshot, but a
caller that needs a complete stable traversal must avoid concurrent tree
mutations between page calls.
Use Walk for a complete stable traversal. It pins one SQLite snapshot before
returning and yields the selected root and its descendants in pages:
walker, err := vault.Walk(ctx, "/sessions", docbank.WalkOptions{PageSize: 500})
if err != nil {
return err
}
defer walker.Close()
for {
page, err := walker.Next(ctx)
if err == io.EOF {
break
}
if err != nil {
return err
}
for _, entry := range page {
// entry.Path is canonical within the pinned snapshot.
_ = entry
}
}
A zero page size uses DefaultWalkPageSize. No page can exceed
MaxWalkPageSize. Paths are limited to MaxWalkPathBytes, and absolute tree
depth is limited to MaxWalkDepth. Later tree changes do not enter the snapshot.
The walker loads the tree incrementally. Setup does not load the whole subtree. Each node needs at most two sibling range seeks and one child seek. The second sibling seek applies only when an include-trash walk finishes the node IDs sharing one path and advances to the next name.
Always call Walker.Close, including after io.EOF. Repeated calls are safe.
It releases the read transaction, dedicated connection, and vault lease.
Vault.Close waits for all walkers and content readers to close.
MovePath, TrashPath, and Restore return the resulting node and canonical
path. Their optional positive IfRevision rejects stale mutations.
IfRevision == 0 is unconditional. Restore accepts photo members inside trashed folders and recovers their group, those folders, and their other contents. EmptyTrash uses MaxRoots as a batch target, finishing complete photo groups even when they exceed it. Zero uses DefaultTrashEmptyMaxRoots, and
More asks the owner to schedule another batch. Held counts roots the batch
reached but kept because a connected photo member is live, too new, or retained.
Use BatchMove for an all-or-nothing reorganization of up to
MaxBatchMoves nodes. Each source is either a path resolved inside the
transaction or a node ID with the revision previously inspected. Every
destination is the node's full final path, and its parent resolves in the
planned final tree. An existing directory does not mean “move into.” Docbank
validates the complete final tree before any change, so embedded applications
can express file or directory swaps and nested moves without temporary names
or partial completion.
Back up and restore an embedded vault¶
An application that owns a vault in-process can use the same portable, topology-independent backup format as the daemon:
repository, err := docbank.InitBackupRepository(backupRoot)
if err != nil {
return err
}
snapshot, err := vault.CreateBackup(ctx, repository, docbank.BackupOptions{
Tag: "before-import",
Prepare: func(ctx context.Context) error {
return snapshotApplicationCatalog(ctx, catalogSnapshot)
},
ExtraFiles: []docbank.BackupExtraFile{{
Path: catalogSnapshot, RecordAs: "application/catalog.sqlite",
}},
})
if err != nil {
return err
}
proof, err := repository.Verify(ctx, docbank.BackupVerifyOptions{
SnapshotID: snapshot.ID,
})
if err != nil {
return err
}
if len(proof.Problems) != 0 {
return fmt.Errorf("backup verification found %d problems", len(proof.Problems))
}
_, err = vault.RestoreBackup(ctx, repository, docbank.BackupRestoreOptions{
SnapshotID: snapshot.ID,
Target: restoreRoot,
ProtectedRoots: applicationStorageRoots,
})
return err
Use OpenBackupRepository after process restart. Snapshots lists recovery
points in chronological order. Save the repository's ID() and the returned
snapshot's ID and MinReaderVersion with your recovery record.
MinReaderVersion names the required backup format reader, not a Docbank release.
Pass the saved snapshot ID to verification and restore so a newer backup on a
shared repository cannot change which recovery point you use.
Capture prevents content reclamation for the whole snapshot. Ordinary appends resume once Docbank fixes the SQLite view that the backup will use. Physical maintenance waits until Docbank publishes the manifest.
To include your application's catalog in the same backup:
- Use
Prepareto create an immutable catalog snapshot while writes are paused. - Declare the file in
ExtraFiles. - Keep the file unchanged until
CreateBackupreturns.
RecordAs is the file's relative location beneath the restored vault root.
Your application must coordinate its own writes during Prepare. Docbank's
freeze pauses Docbank mutations, not changes to an unrelated application
database. The callback must not call vault mutations while that lock is held.
Extras larger than 64 MiB stream in chunks, including files over 4 GiB. Those snapshots require backup reader version 6. Once one exists, older readers cannot list snapshots, create backups, prune, or restore or verify the latest snapshot. Verification of all snapshots also fails. They can still restore or verify a supported older snapshot by ID. Upgrade every reader before writing large extras to a shared repository. Existing extras larger than 64 MiB are stored again as chunks on their first capture. Their old whole-file blobs remain until the older snapshots are removed and pruned.
Capture fails if an extra's length changes after it is opened. This does not detect in-place changes, so the immutable-source requirement still applies. Restore stages all extras as complete temporary files before replacing their destinations. Allow disk space for those copies alongside any existing files.
Mark files containing credentials or tokens as Sensitive. Docbank rejects
sensitive files in a plaintext repository unless your application sets
AllowPlaintextSecrets for that backup.
Restore always uses a separate root. It rejects overlap with the live vault
or repository and checks content, SQLite integrity, and manifest statistics
before making the result available. Supply other application storage in
ProtectedRoots. Docbank checks those locations before restore cleanup too.
Embedded restore builds a fresh primary store. It does not recreate secondary
store placement.
If the original vault is unavailable, open the backup repository and restore directly without initializing or opening a source vault:
repository, err := docbank.OpenBackupRepository(backupRoot)
if err != nil {
return err
}
_, err = repository.Restore(ctx, docbank.BackupRestoreOptions{
SnapshotID: snapshotID,
Target: restoreRoot,
ProtectedRoots: applicationStorageRoots,
})
return err
An omitted snapshot ID selects the latest recovery point. Set SQLite in
BackupRestoreOptions to use your application's driver, such as
modernc.Driver{} from go.kenn.io/docbank/sqlite/modernc. Omitting it selects
the build's default driver. An application using modernc in a CGO build should
select it explicitly for restore too, because the default CGO driver needs the
fts5 build tag. Restore includes declared host files along with vault
content. It rejects the repository, declared protected roots, and targets held
by another vault. Include any offline source or application storage you want
to preserve in ProtectedRoots: the repository cannot infer their current
locations. Vault.RestoreBackup also protects its open vault automatically
and keeps that vault's configured SQLite driver.
Remove recovery points and reclaim storage¶
BackupRepository.Forget removes the snapshot records you select.
BackupRepository.Prune reclaims backup storage that retained snapshots no
longer need. Neither operation opens the source vault. Retention schedules and
which recovery points to keep remain the embedding application's policy.
Preview each operation before committing to it:
selection, err := repository.Forget(ctx, docbank.BackupForgetOptions{
SnapshotIDs: snapshotIDs,
DryRun: true,
})
if err != nil {
return err
}
// Present selection.Selected to the user. Call Forget again with DryRun false
// to apply the removal; selection.Forgotten is empty during a dry run.
fmt.Println("Selected recovery points:", selection.Selected)
cleanup, err := repository.Prune(ctx, docbank.BackupPruneOptions{DryRun: true})
if err != nil {
return err
}
// After removing snapshots, preview cleanup again before calling Prune with
// DryRun false. PacksToRemove includes any old packs that will be rewritten.
fmt.Println("Packs selected for cleanup:", cleanup.PacksToRemove)
Forgetting alone does not reclaim packed bytes. It refuses to remove the last
recovery point unless AllowEmpty is set (ErrBackupLastSnapshot), and
refuses parents needed by retained incremental snapshots
(ErrBackupSnapshotRequired). Both cleanup operations use Kit's exclusive
repository lock, including dry runs. Contention returns
ErrBackupRepositoryLocked. ForceUnlock is only for known abandoned locks.
Pruning removes wholly unused packs and rewrites packs with less than half their encoded payload still needed. Mostly-live packs retain unused bytes, so this is not full compaction or secure erasure. Planned byte counts cover old pack files and copied payload, not exact net savings or temporary-space needs. Inspect the returned report even when an error occurs: completed removals and writes may be partial. Interrupted pruning can be retried. These embedded operations do not add automatic retention or standalone CLI cleanup commands.
Maintain physical storage¶
Ordinary Put calls publish loose content. Call Pack when the embedded
owner is ready to move authorized loose blobs into managed immutable packs:
report, err := vault.Pack(ctx, docbank.PackOptions{MaxBytes: 256 << 20})
if err != nil {
return err
}
if report.More {
// Run another bounded pass when scheduling allows.
}
MaxBytes is a soft committed raw-byte budget: the pass finishes the blob that
crosses the budget, seals its pack, and stops. Zero is unlimited. The report
includes packing, reconciliation, missing/corrupt content, and orphan cleanup
outcomes. Embedded applications should surface those fields. A nil error alone
is not a complete health report. Packing changes only physical
representation. OpenContent keeps the same verified read contract.
Embedded GarbageCollect, Verify, and Repack calls are resumable bounded
passes. WorkBudget.MaxObjects == 0 uses DefaultMaintenanceMaxObjects.
Larger budgets must not exceed MaxMaintenanceObjects. A positive MaxBytes
adds a soft byte bound, so one selected object may finish after crossing it. A
zero byte bound is unlimited, but the object bound still limits each pass.
When a report has More, pass a non-empty NextCursor back in the same
operation's next WorkBudget. Treat cursors as opaque and operation-specific.
Malformed cursors and cursors from another operation return
ErrInvalidMaintenanceCursor. A Repack pass can validly report more work with
an empty cursor when completed mutations themselves reduce the candidate set.
In that case, repeat it from an empty cursor. Cursors are continuation
positions, not snapshot tokens: work inserted earlier in canonical order
during a cycle waits for a later cycle started without a cursor.
The embedded calls do less than the standalone full-run commands. Embedded
Verify checks a bounded page of blob bytes but
does not perform whole-catalog metadata validation. Embedded GarbageCollect
handles bounded unreachable catalog authority but does not enumerate untracked
filesystem files. The daemon's verify and gc commands still run those
full-run checks.
Your application decides when to empty trash and prune prior versions. GC can reclaim a blob only after no retained reference needs it. Embedded vaults have no background maintenance scheduler, so your application also chooses when to resume each bounded pass.
Choose SQLite¶
The build default preserves performance where CGO is available:
- CGO builds use
github.com/mattn/go-sqlite3. CGO_ENABLED=0builds usemodernc.org/sqlite.
An application may select either adapter explicitly:
import (
"go.kenn.io/docbank"
"go.kenn.io/docbank/sqlite/modernc"
)
vault, err := docbank.New(ctx, docbank.Config{
Root: root,
SQLite: modernc.Driver{},
})
Use sqlite/mattn.Driver in a CGO build to select the CGO adapter explicitly.
The adapter is selected when the vault opens. Query and transaction operations
then run directly on that driver's database/sql pool. Standalone backup and
restore paths use the same adapter boundary rather than silently switching
SQLite implementations.
Ownership boundary¶
Do not point a daemon and an embedded application at the same root. New holds
the same exclusive hierarchy lock as the daemon for the entire vault lifetime
and fails if the requested root overlaps another active vault or restore target.
The standalone CLI remains daemon-first and never opens storage directly. Embedding is a distinct application ownership mode, not a second privileged path into a daemon-owned vault. External agents should continue to use the authenticated HTTP contract.