# `Ankusa.BlobStore`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/blob_store.ex#L1)

Object storage: `PUT` whole immutable objects, range `GET` one record,
`DELETE` for retention, `LIST` by prefix. The default is the local
filesystem; S3, GCS, Azure Blob and OCI are drop-in adapters.

This module is both the behaviour and the instance-scoped facade. Every call
names a scope:

  * `:segments` — compacted hook segments (`Ankusa.Storage`): the
    `storage.blob_store`, with every new key under `storage.key_prefix`.
    Nodes that share one bucket give themselves distinct prefixes
    (`"node-a/"`), because a segment key (`seg/<first>-<last>`) is only
    unique per node.
  * `:backup` — store backups (`Ankusa.Store.Backup`): the
    `backup.blob_store` when set, else the segment store, **with**
    `storage.key_prefix`. A backup belongs to one node's store, like its
    segments.

Claim packs do not go through this facade: `Ankusa.ClaimCheck.Stores`
writes and reads them across `claim_check.blob_stores`, never prefixed.

The prefix is applied once, when a new object is named (`object_key/3`);
the full key is what the archive catalogue records, and `put`/`get`/
`get_range`/`delete` take full keys verbatim. Changing `storage.key_prefix`
therefore moves only new segments, never strands written ones.

## Encryption at rest

The object-store adapters take an `:sse` option (`Ankusa.BlobStore.SSE`) that
asks the provider to encrypt every object a `put/4` writes, with a KMS key or
the provider's own. It is per store, so it covers the segment store, the
backup store and every claim store as each is configured.

## Transport

The HTTP adapters share their transport options (`Ankusa.BlobStore.Options`):
one deadline per operation, jittered retries that honour `Retry-After`
(`Ankusa.BlobStore.Retry`), a breaker per bucket (`Ankusa.BlobStore.Breaker`),
multipart uploads above a size, integrity checksums
(`Ankusa.BlobStore.Checksum`) and a connection pool per endpoint
(`Ankusa.BlobStore.Pool`).

# `scope`

```elixir
@type scope() :: :segments | :backup
```

# `delete`

```elixir
@callback delete(instance :: atom(), key :: String.t(), opts :: keyword()) :: :ok
```

# `get`

```elixir
@callback get(instance :: atom(), key :: String.t(), opts :: keyword()) ::
  {:ok, binary()} | {:error, term()}
```

# `get_range`

```elixir
@callback get_range(
  instance :: atom(),
  key :: String.t(),
  offset :: non_neg_integer(),
  length :: pos_integer(),
  opts :: keyword()
) :: {:ok, binary()} | {:error, term()}
```

# `list`

```elixir
@callback list(instance :: atom(), prefix :: String.t(), opts :: keyword()) ::
  {:ok, [String.t()]} | {:error, term()}
```

Every key under `prefix`, sorted, across all of the store's result pages. A
page that fails fails the call: a partial listing is never presented as the
whole.

# `put`

```elixir
@callback put(instance :: atom(), key :: String.t(), data :: iodata(), opts :: keyword()) ::
  :ok | {:error, term()}
```

# `put_if_absent`

```elixir
@callback put_if_absent(
  instance :: atom(),
  key :: String.t(),
  data :: iodata(),
  opts :: keyword()
) ::
  :ok | {:error, :already_exists} | {:error, term()}
```

`put/4`, but only if no object exists under `key`: `{:error, :already_exists}`
when one does, with that object left exactly as it was. The adapters use the
store's own precondition (S3 and Azure `If-None-Match: *`, GCS
`ifGenerationMatch=0`, OCI `if-none-match: *`), so the check and the write are
one atomic request. `Ankusa.ClaimCheck` writes every pack with it: a reused
or colliding pack id then refuses instead of overwriting claims that are
already referenced.

An object that exists with exactly the bytes being written is `:ok`
(`same_object/5`): a retried request whose first attempt landed is not a
collision with another writer.

# `delete`

```elixir
@spec delete(atom(), scope(), String.t()) :: :ok
```

# `get`

```elixir
@spec get(atom(), scope(), String.t()) :: {:ok, binary()} | {:error, term()}
```

# `get_range`

```elixir
@spec get_range(atom(), scope(), String.t(), non_neg_integer(), pos_integer()) ::
  {:ok, binary()} | {:error, term()}
```

# `list`

```elixir
@spec list(atom(), scope(), String.t()) :: {:ok, [String.t()]} | {:error, term()}
```

Every key of this node's objects in `scope` under `prefix` (behind `storage.key_prefix` for `:segments` and `:backup`), as full keys.

# `object_key`

```elixir
@spec object_key(atom(), scope(), String.t()) :: String.t()
```

The full key a new object in `scope` is written under: `key` behind
`storage.key_prefix`. The compactor names each segment with it and records
the result in the archive catalogue, so a later prefix change only moves new
segments; the ones already written are read back under the key they were
written with.

# `put`

```elixir
@spec put(atom(), scope(), String.t(), iodata()) :: :ok | {:error, term()}
```

# `put_if_absent`

```elixir
@spec put_if_absent(atom(), scope(), String.t(), iodata()) ::
  :ok | {:error, :already_exists} | {:error, term()}
```

# `same_object`

```elixir
@spec same_object(module(), atom(), String.t(), iodata(), keyword()) ::
  :ok | {:error, :already_exists}
```

Settle an `:already_exists`: `:ok` when `key` already holds exactly `data` (a
conditional `PUT` that landed, whose answer was lost and which was then sent
again, finds its own object), else `{:error, :already_exists}`. A store that
cannot be read back is `{:error, :already_exists}` too: the caller mints a new
key rather than assume.

# `secondary_config`

```elixir
@spec secondary_config(Ankusa.Config.t()) :: {module(), keyword(), String.t()} | nil
```

The second object store backups are also written to (`backup.secondary`),
under the same `storage.key_prefix`; `nil` when none is configured.

# `store`

```elixir
@spec store(atom(), scope()) :: {module(), keyword()}
```

The `{module, opts}` a scope resolves to on `instance`.

# `validate_config!`

```elixir
@spec validate_config!(Ankusa.Config.t()) :: :ok
```

Check the `:sse` option and the transport options (`Ankusa.BlobStore.Options`)
of every blob store the config names (`storage.blob_store`,
`backup.blob_store`, `backup.secondary`, `claim_check.blob_stores`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
