# `Slap.Files`
[🔗](https://github.com/mjrusso/slap/blob/slap_files-v0.4.0/slap_files/lib/slap/files.ex#L1)

Files stored with `Slap.KV` and an object store. A file lives at a ref,
`{partition, id}`: a partition is a stable group of files (a document's,
for example), listed together, and on one `Slap.KV` shard. Each file has
one record in `Slap.KV`, which exists exactly while the file does, and a
body stored in the record (inline) or as an object.

Files can be replaced; bodies never are. Each `put/3` writes a new body,
switches the file's record to it with a conditional write, and gives the
file a new version. The old body is deleted after `retention_ms`, so a
reader that already has it can finish.

Start it with the object store for bodies, after `Slap.KV.Cluster`:

    {Slap.Files, store: {:url, "s3://bucket/prefix"}, path: "files"}

Options:

  * `:store` - required, as for `Slap.SlateDB.open/2`; `:path` - where the
    objects go in it (default `"files"`).
  * `:inline_max_bytes` - bodies up to this size are stored inline with
    `storage: :auto` (default 16 KiB).
  * `:inline_limit` - the largest inline body, even with `storage:
    :inline` (default 1 MiB).
  * `:retention_ms` - how long an old body is kept (default 5 minutes).
  * `:upload_timeout_ms` - how long an upload may take before its object
    may be deleted (default 1 hour).
  * `:sweep_interval_ms` - see `Slap.Files.Sweeper` (default 10 s).
  * `:max_clock_skew_ms` - how far apart the nodes' clocks may be (default
    30 s): see `Slap.Files.Sweeper`.
  * `:reconcile_interval_ms` - see `Slap.Files.Sweeper` (default 1 hour).
  * `:name` - instance name (default `Slap.Files`). Pass it as `files:`
    to file operations.
  * `:cluster` - the instance's KV cluster (default `Slap.KV.Cluster`);
    define an application-owned one with `use Slap.KV.Cluster, otp_app: :my_app`.
  * `:namespace` - stable data namespace within the KV cluster and object
    store (default `"default"`). Named instances require an explicit
    namespace so renaming an instance does not change its stored layout.
    Changing the namespace leaves existing files at the old layout.
    Instances with the same namespace share records, intents, and object
    registrations.
  * `:timeout` - timeout for each KV or object-store call (default no
    timeout for object-store calls); operations can override it.

`:clock` is an internal test hook.

## Errors

Invalid file data (`ref`, body, metadata, content type, expected SHA-256,
`:if_version` and `:range`) returns an error tuple. Invalid control options (`:storage`,
`:files`, `:timeout`) and unknown option names raise `ArgumentError`.

  * `{:error, {:conflict, version}}` - an `if_version:` condition does not
    hold; `version` is the file's current one, or nil.
  * `{:error, :expired}` - an upload took longer than
    `upload_timeout_ms`; its object is deleted.
  * `{:error, {:range_not_satisfiable, size}}` - a read's `:range`
    contains no byte of the file; `size` is the file's size.
  * `{:error, :checksum_mismatch}`, `{:error, :too_large_for_inline}`,
    `{:error, {:bad_request, reason}}`.
  * `{:error, :unavailable | :timeout}` - from `Slap.KV` or the store. A
    write may still have been applied; `get/1` shows whether it was.

## Telemetry

`put/3`, `delete/2`, `get/2`, `read/2`, `stream/2`, and `list/2` emit
Telemetry spans under `[:slap, :files, operation]`, with `:start`, `:stop`,
and `:exception` events. Metadata includes `:files`, the instance name;
a stop event also has `:outcome` (`:ok` or `:error`). A read or stream
with a `:range` has it in its metadata. The `stream/2` span
measures opening the body stream; consuming it happens after that span ends.

# `error`

```elixir
@type error() ::
  Slap.KV.error()
  | :checksum_mismatch
  | :expired
  | :too_large_for_inline
  | {:range_not_satisfiable, non_neg_integer()}
```

# `range`

```elixir
@type range() ::
  {non_neg_integer(), non_neg_integer() | :eof} | {:last, non_neg_integer()}
```

The bytes a read returns: `{first, last}` (inclusive, as in HTTP),
`{first, :eof}`, or `{:last, n}` (the last `n` bytes).

# `ref`

```elixir
@type ref() :: {partition :: binary(), id :: binary()}
```

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `delete`

```elixir
@spec delete(ref(), keyword()) :: :ok | {:error, error()}
```

Deletes a file. Returns `:ok` once it is gone (also if there was none,
without `:if_version`); its body is deleted after `retention_ms`.
`if_version:` makes it conditional, as for `put/3`.

# `get`

```elixir
@spec get(ref(), keyword()) :: {:ok, Slap.Files.File.t() | nil} | {:error, error()}
```

The file's metadata, or `{:ok, nil}`.

# `list`

```elixir
@spec list(binary(), keyword()) ::
  {:ok, %{files: [Slap.Files.File.t()], cursor: binary() | nil}}
  | {:error, error()}
```

Lists a partition's files in id order, including their versions. Takes the
`:prefix`, `:gte`, `:lt`, `:limit` and
`:cursor` of `Slap.KV.scan/2`, and returns `{:ok, %{files: files, cursor:
cursor}}`. An input `cursor: nil` starts at the beginning of the selected
range; a returned `cursor: nil` means there are no more pages. A binary
cursor remains valid after the file it follows is deleted. Supplied
`:prefix`, `:gte`, and `:lt` values must be binaries.

# `new_id`

```elixir
@spec new_id() :: binary()
```

A new random id, for a ref.

# `put`

```elixir
@spec put(ref(), binary() | Enumerable.t(), keyword()) ::
  {:ok, Slap.Files.File.t()} | {:error, error()}
```

Writes a file: creates it, or replaces its body and metadata. `body` is a
binary or an enumerable of binaries (read once). Returns the file's new
`Slap.Files.File` once it is durable.

Options:

  * `:if_version` - `:absent` to create only, or the version the file
    must be at.
  * `:storage` - `:auto` (default), `:inline` or `:object`.
  * `:content_type` - default `"application/octet-stream"`.
  * `:metadata` - a map of strings to strings (default `%{}`).
  * `:expected_sha256` - the body's expected SHA-256 (32 bytes).
  * `:files` - the instance name (default `Slap.Files`).
  * `:timeout` - overrides the instance's timeout for each KV or
    object-store call.

Writing the body, content type, metadata and storage the file already has
without a condition returns the file as it is. With `:if_version`, the
condition must hold even when the content is unchanged.

# `read`

```elixir
@spec read(ref(), keyword()) :: {:ok, binary() | nil} | {:error, error()}
```

The file's body, or `{:ok, nil}`. See `stream/2` for large bodies, and
for the `:range` and `:if_version` options.

# `start_link`

```elixir
@spec start_link(keyword()) :: Supervisor.on_start()
```

# `stream`

```elixir
@spec stream(ref(), keyword()) ::
  {:ok,
   {Slap.Files.File.t(), Enumerable.t()}
   | {Slap.Files.File.t(), {non_neg_integer(), non_neg_integer()},
      Enumerable.t()}
   | nil}
  | {:error, error()}
```

The file and its body as a lazy stream of binaries, or `{:ok, nil}`. An
object body stays readable for `retention_ms` after the file is replaced
or deleted. A store failure while reading the returned stream raises
`Slap.SlateDB.Error`; failure to open the download returns
`{:error, :unavailable | :timeout}`.

Options:

  * `:range` - read only part of the body (see `t:range/0`). Returns
    `{:ok, {file, {first, last}, chunks}}`, where `{first, last}` is the
    range clamped to the file's size, as HTTP clamps it: a `last` past the
    end becomes the last byte. A range that starts past the end,
    `{:last, 0}`, and every range of an empty file return `{:error,
    {:range_not_satisfiable, size}}`. For an object body, only the
    requested bytes are downloaded.
  * `:if_version` - the version the file must have; otherwise the read
    returns `{:error, {:conflict, version}}`. To read a file in several
    ranges from the same body, pass the version from the first read to
    the rest.
  * `:files`, `:timeout` - as for `put/3`.

---

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