ExStorageService.BlobStore.LocalCAS (ex_storage_service v0.6.4)

Copy Markdown View Source

Durable local content-addressed blob storage.

Staging and ready files default to directories below the same cas root. Commit syncs staged bytes, closes the file, atomically renames it, and syncs both directories affected by the rename where the platform supports directory handles. The configured staging and ready roots must share a filesystem; a cross-device rename is rejected without copying or publishing partial content.

Delete unlinks a loose blob and syncs its containing directory where supported. A directory-sync error after unlink is ambiguous but safely retryable: deleting an already absent blob syncs the directory again.

Filesystem operations and boundary faults are injectable per call through :fs_module and :faults; no mutable global test state is used.

Summary

Functions

Publishes a checksum-verified repair, atomically replacing a corrupt path.

Durably deletes a loose CAS blob.

Ensures a hash is ready in the global CAS.

Recovers a completed caller-owned staging file without copying it.

Stages bytes from a state-threaded reader without buffering the full body.

Validates a bare hexadecimal SHA-256 digest without raising.

Types

reader_result(state)

@type reader_result(state) ::
  {:more, binary(), state}
  | {:ok, binary(), state}
  | {:done, state}
  | {:error, term()}
  | {:error, term(), state}

Functions

commit_repair(staged, opts \\ [])

@spec commit_repair(
  ExStorageService.BlobStore.StagedBlob.t(),
  keyword()
) :: {:ok, ExStorageService.BlobStore.ReadyBlob.t()} | {:error, term()}

Publishes a checksum-verified repair, atomically replacing a corrupt path.

The staged source is independently verified before the final same-filesystem rename. POSIX rename replacement keeps readers on either the old inode or the complete repaired inode; no partial destination is exposed.

delete(hash, opts \\ [])

Durably deletes a loose CAS blob.

After unlinking, the containing directory is synced where supported. If that sync fails, the blob is already absent but an error is returned; retrying is safe and syncs the directory again. Packed content is not modified.

ensure_ready(hash, opts \\ [])

@spec ensure_ready(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Ensures a hash is ready in the global CAS.

Existing loose or packed blobs are returned unchanged. With :bucket, legacy content is streamed for checksum verification, synced, and renamed into the CAS using the normal durable commit path.

recover_stage(path, expected_hash, expected_size, opts \\ [])

@spec recover_stage(Path.t(), String.t(), non_neg_integer(), keyword()) ::
  {:ok, ExStorageService.BlobStore.StagedBlob.t()} | {:error, term()}

Recovers a completed caller-owned staging file without copying it.

The path must name a regular file on the same filesystem as the destination CAS root. Its size and SHA-256 digest are checked against the caller's durable metadata before a StagedBlob is returned. Symlinks, non-regular files, changed files, mismatches, and cross-filesystem publication fail closed.

The recovered value may be passed to commit/2, which performs the normal file sync, atomic rename, and directory sync. The caller must retain exclusive ownership of the path until commit or discard completes.

stage_from_reader(reader, initial_state, opts \\ [])

@spec stage_from_reader((state -> reader_result(state)), state, keyword()) ::
  {:ok, ExStorageService.BlobStore.StagedBlob.t(), state}
  | {:error, term(), state}
when state: term()

Stages bytes from a state-threaded reader without buffering the full body.

The reader receives its current state and must return {:more, chunk, state} while data remains, or {:ok, final_chunk, state} / {:done, state} when complete. The final reader state is returned to callers so adapters retain updates such as a consumed Plug.Conn.

:max_size defaults to the configured maximum object size. Reader, write, and size-limit failures close and remove the partial staging file.

validate_hash(hash)

@spec validate_hash(term()) :: :ok | {:error, :invalid_hash}

Validates a bare hexadecimal SHA-256 digest without raising.