cadrumo.adapters.outbound.storage.local module

Local-filesystem adapters.outbound.storage.protocol.StorageProvider implementation.

Stores objects under a configurable root directory. Each namespace is a subdirectory; each object is a single file named <hmac_prefix_8>--<label>.<ext>. Metadata (content_hash, byte_length, written_at, full HMAC, and label) lives in a sibling JSON sidecar so the listing API can return ProviderObjectMetadata without re-hashing the payload.

Bytes-in / bytes-out: encryption + classification stay above this layer. The provider treats every payload as opaque bytes and uses adapters.outbound.storage._integrity.verify_content_hash() to enforce the stored digest on read.

class LocalFileSystemProvider(root)[source]

Bases: object

Bytes-in / bytes-out provider backed by a pathlib.Path tree.

Parameters:

root (Path)

property root: Path

Provider storage root as a pathlib.Path.

put(namespace, object_key_hmac, payload, *, content_hash, label, batch=None)[source]

Atomically write the object and its sidecar, returning ProviderObjectMetadata.

batch opts this write into a DurableWriteBatch, deferring its fsyncs to the batch commit. Sound here specifically because the store is content-addressed: content_hash names the payload, so a crash mid-batch yields a file that fails its own digest check on read and whose source document is still available to re-import. A bulk evidence ingest passes one batch for the whole run; a single put passes nothing and keeps per-file durability.

Atomicity guarantee: the payload file is written through atomic_write_hardened_bytes() (the master-key O_EXCL + mode 0o600 + fsync pattern) rather than the standard tier. The payload here is already-encrypted ciphertext (encryption and classification stay above this provider layer), but the 2026-05-30 security-paths audit flagged the sidecar’s plaintext content_hash/byte_length metadata as a size-channel inference aid against the encrypted blob set, so the object file is hardened defence-in-depth even though its own content is opaque. The sidecar is written afterwards through atomic_write_text() (standard tier), closing the crash window where a torn sidecar write could leave a committed object with missing or corrupted metadata; on sidecar-write failure the payload is removed so no orphaned object lingers without metadata.

Return type:

ProviderObjectMetadata

Parameters:
get(namespace, object_key_hmac)[source]

Read the object payload from disk and return verified metadata.

Locates the .bin file by HMAC prefix, loads the sibling .meta.json sidecar, reads the raw bytes, and compares the core.hashing.sha256_hex() digest against the sidecar’s content_hash field through verify_content_hash(). Both sha256-<hex>-prefixed strings and bare hex digests are accepted.

Parameters:
  • namespace (str) – Logical bucket name; maps to a subdirectory of root.

  • object_key_hmac (str) – Full HMAC string identifying the object.

Return type:

tuple[bytes, ProviderObjectMetadata]

Returns:

A two-tuple containing payload bytes and ProviderObjectMetadata.

Raises:
delete(namespace, object_key_hmac)[source]

Remove the object file and its sidecar from disk, or neither.

Returns False immediately when the object is absent; deleting a non-existent object is idempotent. The sidecar is removed with missing_ok=True so a pre-existing orphaned payload without a sidecar is still cleanly deleted.

A failure leaves the PAIR, never half of it. The payload used to be unlinked first and the sidecar second, so a sidecar cleanup that failed after the payload was already gone raised while leaving nothing to retry: iter_objects no longer reported the object, get could not read it, and the orphaned sidecar path then blocked a re-put under the same key. The object was un-deletable, un-readable and un-writable at once.

Two changes make the failure recoverable. The sidecar goes first, so a refusal there removes nothing at all. And its bytes are read before that removal, so a payload unlink that then fails can put the sidecar back — the sidecar is metadata this method already holds, whereas the payload is not something it could restore. Either way the retry that follows sees the same state the caller started from.

Both unlinks are guarded on OSError rather than PermissionError alone: the ways a filesystem refuses to remove a path are not one errno, and on some platforms an unlink of a directory-shaped sidecar raises IsADirectoryError, which is not a PermissionError. A narrower catch let exactly the corruption shape above escape untranslated.

Parameters:
  • namespace (str) – Logical bucket name.

  • object_key_hmac (str) – Full HMAC string identifying the object.

Return type:

bool

Returns:

True when the object was found and deleted; False when it was already absent.

Raises:
iter_namespaces()[source]

Yield the name of every namespace subdirectory under root.

Returns immediately (yields nothing) when root does not yet exist on disk.

Yields:

Directory names in case-normalised name order.

Return type:

Iterator[str]

iter_objects(namespace)[source]

Yield metadata for every object in namespace.

Only .bin files with a companion .meta.json sidecar are yielded; files without a sidecar are silently skipped (the coordinator surfaces those as integrity issues via its own diff classifier).

Parameters:

namespace (str) – Logical bucket name.

Yields:

ProviderObjectMetadata records in sorted filename order.

Raises:
Return type:

Iterator[ProviderObjectMetadata]

probe(*, read_only=False)[source]

Assess filesystem accessibility and write permissions, returning a ProviderProbeReport.

Attempts to create root if absent. Then, unless read_only=True, performs a sentinel write/delete round-trip in a _probe namespace to confirm write access end-to-end.

The method never raises; every failure mode is encoded in the returned ProviderProbeReport.

Parameters:

read_only (bool) – When True, skip the sentinel write round-trip and report writable=False regardless of actual permissions.

Return type:

ProviderProbeReport

Returns:

A ProviderProbeReport with reachable, writable, and a human-readable detail string describing the outcome.