cadrumo.adapters.persistence.storage.bucket.directory_layout module

Filesystem provisioning and path resolution for per-bucket directories.

The per-bucket on-disk model lives at <cadrumo-root>/buckets/<bucket-id>/ and carries exactly two subdirectories:

  • db/ relational state (SQLite database files).

  • blobs/ opaque artefact storage (sealed ciphertext blobs).

This module RESOLVES that layout; it does not create or destroy it. A bucket root comes into existence exactly once, by capsule publication’s atomic no-replace rename, and a second creator here would target the very directory that rename must claim – measured, bucket_paths(...).bucket_dir and the capsule commit marker’s parent are the same path. The test-only provisioner that used to live here now sits in the wheel-excluded test package.

The typed BucketPaths record carries each resolved subpath so callers never compose the layout themselves.

class BucketPaths(**data)[source]

Bases: BaseModel

Typed record carrying the resolved paths for one bucket directory.

Parameters:

data (Any)

bucket_id: BucketId
root: Path
bucket_dir: Path
db_dir: Path
blobs_dir: Path
database_file: Path

The bucket’s database file.

Resolved here rather than composed by callers. Seventeen test modules were hand-building it as storage_root / "buckets" / id / "db" / "cadrumo.db" precisely because this record carried every sibling directory but not the one file inside them.

The composition is easy to get wrong in a way that fails late: BUCKET_DATABASE_FILE’s subpath is db/cadrumo.db – bucket-relative, already carrying its own db/ segment – so it joins onto bucket_dir. Joining it onto db_dir yields <bucket>/db/db/cadrumo.db, a path nothing creates, surfacing as a missing file rather than a wrong join. That is the blobs/blobs shape: the wrong anchor, not a wrong constant.

validate_path_component(value, *, subject)[source]

Refuse value unless it is one path component that stays where it is joined.

Every storage path built from a caller-supplied string needs this same answer, and the rule was previously written out at each join instead. Three copies drifted apart: bucket_paths and keystore_path() each enumerated the empty and separator cases, and the keystore sidecar join validated its bucket_id while never looking at its filename at all.

Four ways a value fails to be a component, each measured against a real join rather than reasoned about:

  • Empty, which joins to the parent itself.

  • Carrying a separator, the only case all three copies caught.

  • A dot segment. ".." carries no separator, so a separator check passes it, and the join then resolves ABOVE the directory being addressed.

  • Drive-qualified. "D:x" resolves onto another drive entirely, and "C:x" – when the root is already on C: – silently becomes the component "x", so the directory name no longer equals the identifier that named it and two distinct ids can land on one directory.

Raises:

BucketValidationError – When value is not a single containable component; subject names the offending parameter.

Return type:

None

Parameters:
bucket_paths(root, bucket_id)[source]

Resolve the typed paths for <root>/buckets/<bucket_id>/ without IO.

The id is checked by validate_path_component() before it is joined, so it must be one containable component. Nothing reaches this with a hostile id today – a restored archive’s bucket_id must equal the custody envelope’s profile_id, which is a UUID, and the system-scoped ids are system, unsecured and diagnostic-probe. But that containment lives upstream in an identity cross-check written for another purpose, while BucketId itself is a 1-128 character string that admits ".." happily. Refusing it HERE puts the guarantee at the boundary that owns the join.

Parameters:
  • root (Path) – Cadrumo storage root (the parent of buckets/).

  • bucket_id (str) – The bucket identifier; must be one path component.

Return type:

BucketPaths

Returns:

A BucketPaths record carrying every resolved subpath.

Raises:

BucketValidationError – When bucket_id is not a single containable path component; see validate_path_component().