# `Fil`
[🔗](https://github.com/pehbehbeh/fil/blob/v0.1.0/lib/fil.ex#L1)

Fil is a pluggable file storage abstraction for Elixir.

## Features and goals

- **One API for many kinds of storage.** Local disk, S3 (and S3-compatible stores) and an in-memory disk for async
  tests, with the same behaviour on every adapter. `cp` and `rename` work across disks.
- **Just values.** A disk is a plain value: no application config, no registry, nothing to supervise. It works in a
  script or a Livebook with `Mix.install/1`.
- **Pluggable.** Anything that isn't about where files are stored is a [plugin](https://fil.hexdocs.pm/plugins.html),
  such as setting content types or logging.
- **Few dependencies.** Req, NimbleOptions and MIME, plus Plug if you serve files. Cloud adapters use Req instead of
  their own SDKs.
- **URLs on every disk.** Public and signed GET and PUT URLs, from S3 itself or from `Fil.Plugin.URL` and `Fil.Plug`.
- **Safe by default.** Paths can't climb out of the disk root, `if_exists: :error` never replaces a file, and S3
  verifies checksums.
- **Errors you can act on.** Each error, such as `Fil.NotFoundError` or `Fil.UnavailableError`, says what to do next
  and is the same on every adapter.

## Concepts

`Fil` uses the function names of Elixir's `File` module (`read`, `write`, `stat`, `ls`, `cp`, `rename`, `rm`,
`rm_rf`), but on every adapter they behave like an object store:

- `write` creates missing parent directories
- `rm` on a missing file succeeds
- `ls` on a missing directory returns an empty list
- paths are always relative to the disk root, and `.` is the root
- a path that climbs above the root fails with a `Fil.InvalidRequestError`

The [contract in `Fil.Adapter`](https://fil.hexdocs.pm/Fil.Adapter.html#module-contract) lists every difference from
`File`. Every function that can fail returns `{:ok, result}` or `{:error, error}`, with an error from the Errors
section below.

### Disks

A disk says where files are stored: an adapter and its options. It's a plain value, so there's no application config
and nothing to add to your supervision tree (`Fil` starts one process of its own, for the Memory adapter). Every
function takes a disk as its first argument and a path relative to the disk's root:

```elixir
disk = Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")

{:ok, _} = Fil.write(disk, "hello.txt", "World")

Fil.read(disk, "hello.txt")
#=> {:ok, "World"}
```

### Refs

A `Fil.Ref` is a single value for a file on a disk, built with `Fil.ref/2`. Every function that takes a disk and a
path as two arguments also takes a ref as one argument in their place:

```elixir
hello = Fil.ref(disk, "hello.txt")
#=> #Fil.Ref<local:hello.txt>

{:ok, _} = Fil.write(hello, "World")

Fil.read(hello)
#=> {:ok, "World"}
```

Actions on files return the ref they acted on, which keeps cross-disk code short:

```elixir
with {:ok, report} <- Fil.write(s3, "reports/q3.pdf", pdf),
     {:ok, _backup} <- Fil.cp(report, Fil.ref(local, "backups/q3.pdf")) do
  {:ok, report}
end
```

The bang variants return the ref itself instead of `{:ok, ref}`, so you can pipe one call into the next:

```elixir
disk
|> Fil.write!("hello.txt", "World")
|> Fil.cp!("backup/hello.txt")
|> Fil.read!()
#=> "World"
```

### Plugins

Plugins attach to a disk and see every operation on it. `Fil` ships one that sets the content type from the file
extension, so S3 serves `reports/q3.pdf` as `application/pdf`:

```elixir
s3 =
  Fil.disk(adapter: Fil.Adapter.S3, bucket: "my-bucket", region: "eu-central-1")
  |> Fil.Plugin.ContentType.attach()

{:ok, report} = Fil.write(s3, "reports/q3.pdf", pdf)
```

For a disk built from config, `Fil.disk/1` takes plugins as data: `plugins: [{Fil.Plugin.ContentType, :call, []}]`.

Your own plugin is a function. It gets the operation, calls `next` to run the rest, and returns the result:

```elixir
local =
  Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")
  |> Fil.attach(:log, fn op, next, _opts ->
    IO.puts("#{op.name} #{op.path}")
    next.(op)
  end)
```

The [plugins guide](https://fil.hexdocs.pm/plugins.html) explains how to write plugins: matching on operations,
changing content, handling errors and answering without the adapter.

### Errors

Every error is an exception struct that says what you can do about it, and means the same on every disk:

```elixir
case Fil.read(disk, "report.txt") do
  {:ok, content} -> content
  {:error, %Fil.NotFoundError{}} -> nil
  {:error, %Fil.UnavailableError{}} -> :retry_later
end
```

The structs contain the operation, the path, the disk and what the storage reported (`:reason`), so a log line says
which file failed and why. Each error has its own page under Errors in the docs, and
[Errors in `Fil.Adapter`](https://fil.hexdocs.pm/Fil.Adapter.html#module-errors) explains how adapters use them.

Every function that can fail has a bang variant that returns the bare result and raises the same struct instead.

## Usage

The [installation guide](https://fil.hexdocs.pm/installation.html) is the full setup for an application: a module
for your disks, the config for each environment (local disks in development, memory disks in tests and S3 in
production), signed URLs and tests. This section is only a quick tour of the API.

Add `fil` to your dependencies:

```elixir
def deps do
  [
    {:fil, "~> 0.1"}
  ]
end
```

Build one disk per kind of storage:

```elixir
local = Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")

s3 =
  Fil.disk(
    adapter: Fil.Adapter.S3,
    bucket: "my-bucket",
    region: "eu-central-1",
    access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
    secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
  )
```

Every operation works the same on every disk:

```elixir
{:ok, report} = Fil.write(s3, "reports/q3.pdf", pdf)
{:ok, pdf} = Fil.read(report)
{:ok, stat} = Fil.stat(report)
{:ok, reports} = Fil.ls(s3, "reports/", recursive: true)
{:ok, backup} = Fil.cp(report, Fil.ref(local, "backups/q3.pdf"))
{:ok, _} = Fil.rm(report)
```

`url` returns the public URL of a file, and `signed_url` an expiring one, so clients can download or upload a file
directly instead of going through your application code. S3 serves its URLs itself. For local and in-memory disks,
`Fil.Plugin.URL` builds them and `Fil.Plug` serves them from your application:

```elixir
{:ok, logo_url} = Fil.url(s3, "logo.png")
{:ok, url} = Fil.signed_url(report, expires_in: 900)
{:ok, upload_url} = Fil.signed_url(s3, "inbox/new.bin", method: :put)
```

With `checksum:`, a write sends a checksum of the content. S3 rejects the upload if what it received doesn't match,
and stores the checksum with the object, so later reads can check it:

```elixir
{:ok, report} = Fil.write(s3, "reports/q3.pdf", pdf, checksum: :sha256)
{:ok, pdf} = Fil.read(report, verify_checksum: true)
{:ok, %Fil.Stat{checksum: {:sha256, checksum}}} = Fil.stat(report, checksum: :sha256)
```

To create a file only if it doesn't exist yet, pass `if_exists: :error`. If the file is already there, nothing is
overwritten and the write returns a `Fil.AlreadyExistsError`. That makes a simple lock: whoever creates the file
first runs the job.

```elixir
case Fil.write(s3, "jobs/today.lock", "started", if_exists: :error) do
  {:ok, _lock} -> :run_the_job
  {:error, %Fil.AlreadyExistsError{}} -> :someone_else_won
end
```

## Development

`mix test` runs the unit tests, which need no network. The integration tests run the conformance suite against
SeaweedFS, started with Docker Compose:

```bash
docker compose up -d
mix test.integration
```

`FIL_S3_ENDPOINT`, `FIL_S3_ACCESS_KEY_ID`, `FIL_S3_SECRET_ACCESS_KEY` and `FIL_S3_REGION` point the integration tests
at another S3 endpoint.

## Acknowledgments

`Fil` builds on ideas from these projects:

- [Flysystem](https://flysystem.thephpleague.com) (PHP): the scope, one API across many kinds of storage
- [Req](https://github.com/wojtekmach/req): the API design, with disks as plain values and plugins that attach to them
- [Plug](https://github.com/elixir-plug/plug): plugins as small units you add to a disk, each doing one thing to every
  operation that passes through

Thanks to their authors and contributors.

# `attach`

```elixir
@spec attach(Fil.Disk.t(), atom(), plugin_callback(), keyword()) :: Fil.Disk.t()
```

Attaches a plugin callback to a disk under a name.

    iex> disk =
    ...>   Fil.disk(adapter: Fil.Adapter.Memory)
    ...>   |> Fil.attach(:shout, fn op, next, _opts ->
    ...>     op |> Fil.Op.update_content(binary: &String.upcase/1) |> next.()
    ...>   end)
    iex> Fil.write!(disk, "hello.txt", "world")
    iex> Fil.read(disk, "hello.txt")
    {:ok, "WORLD"}

The callback is a function of arity 3 or a `{module, function}` pair naming a public function of arity 3. `opts` are
passed to it on every call. Attaching a name that's already attached replaces the callback and its options in the
same position. The [Plugins guide](plugins.md) explains how to write one.

# `detach`

```elixir
@spec detach(Fil.Disk.t(), atom()) :: Fil.Disk.t()
```

Removes a plugin from a disk. Removing a name that isn't attached returns the disk unchanged.

# `disk`

```elixir
@spec disk(keyword()) :: Fil.Disk.t()
```

Builds a disk. Shorthand for `Fil.Disk.new/1`, which lists the options.

    iex> Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    #Fil.Disk<local>

# `ref`

```elixir
@spec ref(Fil.Disk.t(), Path.t()) :: Fil.Ref.t()
```

Builds a ref. Shorthand for `Fil.Ref.new/2`.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    iex> Fil.ref(disk, "uploads/a.txt")
    #Fil.Ref<local:uploads/a.txt>

# `cp`

```elixir
@spec cp(Fil.Ref.t(), Fil.Ref.t() | Path.t()) :: result(Fil.Ref.t())
```

Copies a file and returns the destination ref.

Within one disk, `Fil` uses the adapter's native copy. Across disks, it reads the file and writes it to the
destination. The destination may be a ref, or a bare path on the source's disk.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> other = Fil.disk(adapter: Fil.Adapter.Memory, root: "other")
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> {:ok, backup} = Fil.cp(disk, "hello.txt", "backup/hello.txt")
    iex> backup
    #Fil.Ref<memory:backup/hello.txt>
    iex> {:ok, copy} = Fil.cp(disk, "hello.txt", Fil.ref(other, "hello.txt"))
    iex> Fil.read(copy)
    {:ok, "World"}

# `cp`

```elixir
@spec cp(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t()) :: result(Fil.Ref.t())
@spec cp(Fil.Ref.t(), Fil.Ref.t() | Path.t(), keyword()) :: result(Fil.Ref.t())
```

Copies a file. See `cp/2`.

# `cp`

```elixir
@spec cp(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t(), keyword()) ::
  result(Fil.Ref.t())
```

Copies a file. See `cp/2`.

# `dir?`

```elixir
@spec dir?(Fil.Ref.t()) :: boolean()
```

Whether this path is a directory. On object stores, that means a non-empty prefix.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "reports/q3.pdf", "%PDF")
    iex> Fil.dir?(disk, "reports")
    true
    iex> Fil.dir?(disk, "reports/q3.pdf")
    false

# `dir?`

```elixir
@spec dir?(Fil.Disk.t(), Path.t()) :: boolean()
```

Whether this path is a directory. See `dir?/1`.

# `exists?`

```elixir
@spec exists?(Fil.Ref.t()) :: boolean()
```

Whether anything exists at this path.

Predicates return a plain boolean, so unreachable storage or an invalid path is `false`. Use `stat/1` when you need
to tell those cases apart.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> Fil.exists?(disk, "hello.txt")
    true
    iex> Fil.exists?(disk, "nope.txt")
    false

# `exists?`

```elixir
@spec exists?(Fil.Disk.t(), Path.t()) :: boolean()
```

Whether anything exists at this path. See `exists?/1`.

# `ls`

```elixir
@spec ls(Fil.Disk.t()) :: result([Fil.Ref.t()])
@spec ls(Fil.Ref.t()) :: result([Fil.Ref.t()])
```

Lists a directory. The whole listing is loaded into memory.

One level deep by default; pass `recursive: true` to walk the whole subtree. Paths are relative to the disk root, and
a missing prefix returns an empty list.

A one-level listing includes directories. A recursive listing returns files only, because object stores only have
directories implicitly and the result should be the same on every adapter.

## Options

* `:recursive` (`t:boolean/0`) - Walk the whole subtree instead of one level. The default value is `false`.

## Examples

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> Fil.write!(disk, "reports/q3.pdf", "%PDF")
    iex> {:ok, [hello, reports]} = Fil.ls(disk)
    iex> hello
    #Fil.Ref<memory:hello.txt>
    iex> reports.stat.type
    :directory
    iex> {:ok, [_hello, report]} = Fil.ls(disk, ".", recursive: true)
    iex> report
    #Fil.Ref<memory:reports/q3.pdf>

The results are ordinary refs with `:stat` filled in from the listing, so you can pass them to any other
function:

    {:ok, reports} = Fil.ls(disk, "reports", recursive: true)

    reports
    |> Enum.filter(&(&1.stat.size == 0))
    |> Enum.each(&Fil.rm/1)

# `ls`

```elixir
@spec ls(Fil.Disk.t(), Path.t()) :: result([Fil.Ref.t()])
@spec ls(
  Fil.Ref.t(),
  keyword()
) :: result([Fil.Ref.t()])
```

Lists a directory. See `ls/1`.

# `ls`

```elixir
@spec ls(Fil.Disk.t(), Path.t(), keyword()) :: result([Fil.Ref.t()])
```

Lists a directory. See `ls/1`.

# `read`

```elixir
@spec read(Fil.Ref.t()) :: result(binary())
```

Reads a file.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> Fil.read(disk, "hello.txt")
    {:ok, "World"}
    iex> {:error, %Fil.NotFoundError{path: "nope.txt", reason: :enoent}} = Fil.read(disk, "nope.txt")

## Options

* `:verify_checksum` (`t:boolean/0`) - Checks the content against the checksum stored with it and returns `Fil.ChecksumMismatchError` if
  they differ. Only S3 stores checksums (see the `:checksum` option of `write/4`). Content without a
  stored checksum is returned unchecked. The default value is `false`.

# `read`

```elixir
@spec read(Fil.Disk.t(), Path.t()) :: result(binary())
@spec read(
  Fil.Ref.t(),
  keyword()
) :: result(binary())
```

Reads a file. See `read/1`.

# `read`

```elixir
@spec read(Fil.Disk.t(), Path.t(), keyword()) :: result(binary())
```

Reads a file. See `read/1`.

# `rename`

```elixir
@spec rename(Fil.Ref.t(), Fil.Ref.t() | Path.t()) :: result(Fil.Ref.t())
```

Moves a file and returns the destination ref.

Within one disk, `Fil` uses the adapter's native rename. Across disks, it copies the file and then deletes the source.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "draft.txt", "World")
    iex> {:ok, final} = Fil.rename(disk, "draft.txt", "final.txt")
    iex> final
    #Fil.Ref<memory:final.txt>
    iex> Fil.exists?(disk, "draft.txt")
    false

# `rename`

```elixir
@spec rename(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t()) :: result(Fil.Ref.t())
@spec rename(Fil.Ref.t(), Fil.Ref.t() | Path.t(), keyword()) :: result(Fil.Ref.t())
```

Moves a file. See `rename/2`.

# `rename`

```elixir
@spec rename(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t(), keyword()) ::
  result(Fil.Ref.t())
```

Moves a file. See `rename/2`.

# `rm`

```elixir
@spec rm(Fil.Ref.t()) :: result(Fil.Ref.t())
```

Deletes a file.

Deleting is idempotent: a missing file still returns `{:ok, ref}`. The returned ref is meant for a restore
function in a future release.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> {:ok, deleted} = Fil.rm(disk, "hello.txt")
    iex> deleted
    #Fil.Ref<memory:hello.txt>
    iex> {:ok, ^deleted} = Fil.rm(disk, "hello.txt")
    iex> Fil.exists?(disk, "hello.txt")
    false

# `rm`

```elixir
@spec rm(Fil.Disk.t(), Path.t()) :: result(Fil.Ref.t())
@spec rm(
  Fil.Ref.t(),
  keyword()
) :: result(Fil.Ref.t())
```

Deletes a file. See `rm/1`.

# `rm`

```elixir
@spec rm(Fil.Disk.t(), Path.t(), keyword()) :: result(Fil.Ref.t())
```

Deletes a file. See `rm/1`.

# `rm_rf`

```elixir
@spec rm_rf(Fil.Ref.t()) :: result(non_neg_integer())
```

Removes everything under a prefix and returns the number of deleted files.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "reports/2026/q3.pdf", "%PDF")
    iex> Fil.write!(disk, "reports/2026/q4.pdf", "%PDF")
    iex> Fil.rm_rf(disk, "reports/2026")
    {:ok, 2}

On object stores the prefix is matched as a directory: `"reports"` removes `"reports"` and everything under
`"reports/"`, but not `"reports.txt"`.

# `rm_rf`

```elixir
@spec rm_rf(Fil.Disk.t(), Path.t()) :: result(non_neg_integer())
@spec rm_rf(
  Fil.Ref.t(),
  keyword()
) :: result(non_neg_integer())
```

Removes everything under a prefix. See `rm_rf/1`.

# `rm_rf`

```elixir
@spec rm_rf(Fil.Disk.t(), Path.t(), keyword()) :: result(non_neg_integer())
```

Removes everything under a prefix. See `rm_rf/1`.

# `signed_url`

```elixir
@spec signed_url(Fil.Ref.t()) :: result(String.t())
```

Builds a URL that grants temporary access to a file.

## Options

* `:method` - `:get` for a download URL, `:put` for a direct upload. The default value is `:get`.

* `:expires_in` - How long the URL stays valid, in seconds. At most 7 days (`604800`), the cap of S3. The default value is `900`.

## Examples

    Fil.signed_url(s3, "cv.pdf", expires_in: 300)
    #=> {:ok, "https://bucket.s3.eu-central-1.amazonaws.com/cv.pdf?X-Amz-Algorithm=..."}

S3 signs its own URLs. Local and memory disks can't, so they need `Fil.Plugin.URL` with a `:secret`, and `Fil.Plug`
serves the URLs from your application:

    iex> disk =
    ...>   Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    ...>   |> Fil.Plugin.URL.attach(base_url: "http://localhost:4000/storage", secret: "secret")
    iex> {:ok, url} = Fil.signed_url(disk, "cv.pdf")
    iex> url =~ ~r"^http://localhost:4000/storage/cv.pdf[?]expires=[0-9]+&signature="
    true

Without the plugin, a local or memory disk returns an error:

    iex> disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    iex> {:error, %Fil.UnsupportedError{op: :signed_url, reason: :no_callback}} = Fil.signed_url(disk, "cv.pdf")

# `signed_url`

```elixir
@spec signed_url(Fil.Disk.t(), Path.t()) :: result(String.t())
@spec signed_url(
  Fil.Ref.t(),
  keyword()
) :: result(String.t())
```

Builds a signed URL. See `signed_url/1`.

# `signed_url`

```elixir
@spec signed_url(Fil.Disk.t(), Path.t(), keyword()) :: result(String.t())
```

Builds a signed URL. See `signed_url/1`.

# `stat`

```elixir
@spec stat(Fil.Ref.t()) :: result(Fil.Stat.t())
```

Returns metadata for a file or directory.

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> Fil.write!(disk, "hello.txt", "World")
    iex> {:ok, stat} = Fil.stat(disk, "hello.txt")
    iex> {stat.size, stat.type}
    {5, :regular}
    iex> {:error, %Fil.NotFoundError{path: "nope.txt", reason: :enoent}} = Fil.stat(disk, "nope.txt")

## Options

* `:checksum` - Fills in `Fil.Stat`'s `:checksum` for this algorithm. S3 returns the checksum stored with the object,
  or `nil` if it was written without one. The local filesystem computes it by reading the file.

# `stat`

```elixir
@spec stat(Fil.Disk.t(), Path.t()) :: result(Fil.Stat.t())
@spec stat(
  Fil.Ref.t(),
  keyword()
) :: result(Fil.Stat.t())
```

Returns metadata for a file or directory. See `stat/1`.

# `stat`

```elixir
@spec stat(Fil.Disk.t(), Path.t(), keyword()) :: result(Fil.Stat.t())
```

Returns metadata for a file or directory. See `stat/1`.

# `url`

```elixir
@spec url(Fil.Ref.t()) :: result(String.t())
```

Builds the public URL of a file.

The URL has no signature and doesn't expire, so it only works where the file can be downloaded by anyone: a public
bucket, a CDN, or `Fil.Plug` with `public: true`.

## Examples

    Fil.url(s3, "logo.png")
    #=> {:ok, "https://bucket.s3.eu-central-1.amazonaws.com/logo.png"}

S3 builds the URL from the bucket. Local and memory disks have no URL of their own, so they get one from
`Fil.Plugin.URL`, and `Fil.Plug` serves it from your application:

    iex> disk =
    ...>   Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    ...>   |> Fil.Plugin.URL.attach(base_url: "http://localhost:4000/avatars")
    iex> Fil.url(disk, "1.png")
    {:ok, "http://localhost:4000/avatars/1.png"}

Without the plugin, a local or memory disk returns an error:

    iex> disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    iex> {:error, %Fil.UnsupportedError{op: :url, reason: :no_callback}} = Fil.url(disk, "1.png")

# `url`

```elixir
@spec url(Fil.Disk.t(), Path.t()) :: result(String.t())
@spec url(
  Fil.Ref.t(),
  keyword()
) :: result(String.t())
```

Builds a public URL. See `url/1`.

# `url`

```elixir
@spec url(Fil.Disk.t(), Path.t(), keyword()) :: result(String.t())
```

Builds a public URL. See `url/1`.

# `write`

```elixir
@spec write(Fil.Ref.t(), iodata()) :: result(Fil.Ref.t())
```

Writes a file, creating missing parent directories.

`content` is any iodata.

## Options

* `:if_exists` - What to do if the file already exists. `:overwrite` replaces it. `:error` writes nothing and returns
  a `Fil.AlreadyExistsError`, like `File.write/3` with `[:exclusive]`. That check is atomic on local
  disk, in memory and on AWS S3, so two processes can't both create the file. Some S3-compatible
  servers ignore it. The default value is `:overwrite`.

* `:content_type` (`t:String.t/0`) - Stored as the object's content type where the storage keeps one.

* `:checksum` - Computes a checksum of the content with this algorithm (`:sha256`, `:sha1` or `:crc32`) and sends it
  along, where the storage supports it. S3 rejects the write with `Fil.ChecksumMismatchError` if the
  content it received doesn't match, and stores the checksum with the object. The local filesystem
  stores nothing.

## Examples

    iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
    iex> {:ok, report} = Fil.write(disk, "reports/q3.pdf", ["%PDF", "-1.7"])
    iex> report
    #Fil.Ref<memory:reports/q3.pdf>
    iex> {:error, %Fil.AlreadyExistsError{reason: :eexist}} =
    ...>   Fil.write(disk, "reports/q3.pdf", "again", if_exists: :error)

# `write`

```elixir
@spec write(Fil.Disk.t(), Path.t(), iodata()) :: result(Fil.Ref.t())
@spec write(Fil.Ref.t(), iodata(), keyword()) :: result(Fil.Ref.t())
```

Writes a file. See `write/2`.

# `write`

```elixir
@spec write(Fil.Disk.t(), Path.t(), iodata(), keyword()) :: result(Fil.Ref.t())
```

Writes a file. See `write/2`.

# `cp!`

```elixir
@spec cp!(Fil.Ref.t(), Fil.Ref.t() | Path.t()) :: Fil.Ref.t()
```

Same as `cp/2`, raising the error on failure.

# `cp!`

```elixir
@spec cp!(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t()) :: Fil.Ref.t()
@spec cp!(Fil.Ref.t(), Fil.Ref.t() | Path.t(), keyword()) :: Fil.Ref.t()
```

Same as `cp/3`, raising the error on failure.

# `cp!`

```elixir
@spec cp!(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t(), keyword()) :: Fil.Ref.t()
```

Same as `cp/4`, raising the error on failure.

# `ls!`

```elixir
@spec ls!(Fil.Disk.t()) :: [Fil.Ref.t()]
@spec ls!(Fil.Ref.t()) :: [Fil.Ref.t()]
```

Same as `ls/1`, raising the error on failure.

# `ls!`

```elixir
@spec ls!(Fil.Disk.t(), Path.t()) :: [Fil.Ref.t()]
@spec ls!(
  Fil.Ref.t(),
  keyword()
) :: [Fil.Ref.t()]
```

Same as `ls/2`, raising the error on failure.

# `ls!`

```elixir
@spec ls!(Fil.Disk.t(), Path.t(), keyword()) :: [Fil.Ref.t()]
```

Same as `ls/3`, raising the error on failure.

# `read!`

```elixir
@spec read!(Fil.Ref.t()) :: binary()
```

Same as `read/1`, raising the error on failure.

# `read!`

```elixir
@spec read!(Fil.Disk.t(), Path.t()) :: binary()
@spec read!(
  Fil.Ref.t(),
  keyword()
) :: binary()
```

Same as `read/2`, raising the error on failure.

# `read!`

```elixir
@spec read!(Fil.Disk.t(), Path.t(), keyword()) :: binary()
```

Same as `read/3`, raising the error on failure.

# `rename!`

```elixir
@spec rename!(Fil.Ref.t(), Fil.Ref.t() | Path.t()) :: Fil.Ref.t()
```

Same as `rename/2`, raising the error on failure.

# `rename!`

```elixir
@spec rename!(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t()) :: Fil.Ref.t()
@spec rename!(Fil.Ref.t(), Fil.Ref.t() | Path.t(), keyword()) :: Fil.Ref.t()
```

Same as `rename/3`, raising the error on failure.

# `rename!`

```elixir
@spec rename!(Fil.Disk.t(), Path.t(), Fil.Ref.t() | Path.t(), keyword()) ::
  Fil.Ref.t()
```

Same as `rename/4`, raising the error on failure.

# `rm!`

```elixir
@spec rm!(Fil.Ref.t()) :: Fil.Ref.t()
```

Same as `rm/1`, raising the error on failure.

# `rm!`

```elixir
@spec rm!(Fil.Disk.t(), Path.t()) :: Fil.Ref.t()
@spec rm!(
  Fil.Ref.t(),
  keyword()
) :: Fil.Ref.t()
```

Same as `rm/2`, raising the error on failure.

# `rm!`

```elixir
@spec rm!(Fil.Disk.t(), Path.t(), keyword()) :: Fil.Ref.t()
```

Same as `rm/3`, raising the error on failure.

# `rm_rf!`

```elixir
@spec rm_rf!(Fil.Ref.t()) :: non_neg_integer()
```

Same as `rm_rf/1`, raising the error on failure.

# `rm_rf!`

```elixir
@spec rm_rf!(Fil.Disk.t(), Path.t()) :: non_neg_integer()
@spec rm_rf!(
  Fil.Ref.t(),
  keyword()
) :: non_neg_integer()
```

Same as `rm_rf/2`, raising the error on failure.

# `rm_rf!`

```elixir
@spec rm_rf!(Fil.Disk.t(), Path.t(), keyword()) :: non_neg_integer()
```

Same as `rm_rf/3`, raising the error on failure.

# `signed_url!`

```elixir
@spec signed_url!(Fil.Ref.t()) :: String.t()
```

Same as `signed_url/1`, raising the error on failure.

# `signed_url!`

```elixir
@spec signed_url!(Fil.Disk.t(), Path.t()) :: String.t()
@spec signed_url!(
  Fil.Ref.t(),
  keyword()
) :: String.t()
```

Same as `signed_url/2`, raising the error on failure.

# `signed_url!`

```elixir
@spec signed_url!(Fil.Disk.t(), Path.t(), keyword()) :: String.t()
```

Same as `signed_url/3`, raising the error on failure.

# `stat!`

```elixir
@spec stat!(Fil.Ref.t()) :: Fil.Stat.t()
```

Same as `stat/1`, raising the error on failure.

# `stat!`

```elixir
@spec stat!(Fil.Disk.t(), Path.t()) :: Fil.Stat.t()
@spec stat!(
  Fil.Ref.t(),
  keyword()
) :: Fil.Stat.t()
```

Same as `stat/2`, raising the error on failure.

# `stat!`

```elixir
@spec stat!(Fil.Disk.t(), Path.t(), keyword()) :: Fil.Stat.t()
```

Same as `stat/3`, raising the error on failure.

# `url!`

```elixir
@spec url!(Fil.Ref.t()) :: String.t()
```

Same as `url/1`, raising the error on failure.

# `url!`

```elixir
@spec url!(Fil.Disk.t(), Path.t()) :: String.t()
@spec url!(
  Fil.Ref.t(),
  keyword()
) :: String.t()
```

Same as `url/2`, raising the error on failure.

# `url!`

```elixir
@spec url!(Fil.Disk.t(), Path.t(), keyword()) :: String.t()
```

Same as `url/3`, raising the error on failure.

# `write!`

```elixir
@spec write!(Fil.Ref.t(), iodata()) :: Fil.Ref.t()
```

Same as `write/2`, raising the error on failure.

# `write!`

```elixir
@spec write!(Fil.Disk.t(), Path.t(), iodata()) :: Fil.Ref.t()
@spec write!(Fil.Ref.t(), iodata(), keyword()) :: Fil.Ref.t()
```

Same as `write/3`, raising the error on failure.

# `write!`

```elixir
@spec write!(Fil.Disk.t(), Path.t(), iodata(), keyword()) :: Fil.Ref.t()
```

Same as `write/4`, raising the error on failure.

# `error`

```elixir
@type error() ::
  Fil.NotFoundError.t()
  | Fil.AccessDeniedError.t()
  | Fil.InvalidRequestError.t()
  | Fil.AlreadyExistsError.t()
  | Fil.ChecksumMismatchError.t()
  | Fil.StorageFullError.t()
  | Fil.UnsupportedError.t()
  | Fil.ConfigurationError.t()
  | Fil.UnavailableError.t()
  | Fil.UnknownError.t()
```

An error from an adapter or from `Fil` itself. Each struct stands for what the caller can do about it, and means the
same on every adapter; see [Errors in `Fil.Adapter`](Fil.Adapter.html#module-errors).

# `plugin_callback`

```elixir
@type plugin_callback() ::
  (Fil.Op.t(), (Fil.Op.t() -&gt; Fil.Op.t()), keyword() -&gt; Fil.Op.t())
  | {module(), atom()}
```

A plugin callback: a function or a `{module, function}` pair. See the [Plugins guide](plugins.md).

# `result`

```elixir
@type result(value) :: {:ok, value} | {:error, error() | Exception.t()}
```

A result. Plugins may return their own exceptions, so an error isn't always one of `t:error/0`.
