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

Files in memory, in a store that belongs to a process. It's meant for tests.

    setup do
      Fil.Adapter.Memory.checkout()
    end

    test "stores the avatar" do
      disk = Fil.disk(adapter: Fil.Adapter.Memory, root: "uploads")

      assert {:ok, _} = Fil.write(disk, "avatars/1.png", "png")
    end

## Stores

`checkout/0` creates a store owned by the calling process, usually the test. The store is an ETS table, so it's gone
when the owner exits. Every test starts with an empty store and nothing needs cleaning up, with `async: true` too.

Every operation uses the store of the process that runs it:

  * the store the process checked out itself
  * a store it was allowed into with `allow/2`
  * the store of a process in its `$callers`. `Task` sets `$callers`, and so does `Phoenix.LiveViewTest` for the
    LiveView processes it starts, so both find the test's store without `allow/2`

An operation in a process without a store raises, because that's a missing `checkout/0` or `allow/2` in the test and
not a storage condition.

The disk itself only holds the root, so it can be built anywhere (in `config/test.exs`, for example), and every disk
built in the test sees the same store.

## Behaviour

  * directories exist only as prefixes, like on an object store. There are no empty directories, and a stat on a
    directory returns `%Fil.Stat{type: :directory}` with every other field `nil`.
  * writes are atomic, and `if_exists: :error` uses `:ets.insert_new/2`, so its check is atomic too.
  * `stat/3` sets `:etag` to the MD5 of the content in hex (the ETag S3 returns for a single-part upload), and
    `:content_type` to the `content_type:` the write stored.
  * checksums work as on S3: a write with `checksum:` stores the checksum of the content, `stat/3` with the same
    algorithm returns it (and `nil` for another algorithm or a file written without one), and `verify_checksum: true`
    on a read compares it with the content. A copy keeps the checksum.
  * a memory store has no URLs. Attach `Fil.Plugin.URL` for public and signed URLs, and `Fil.Plug` serves them, in
    a test through `Phoenix.ConnTest` too.

## Options

* `:root` (`t:String.t/0`) - The prefix every path is resolved against, inside the store. Two disks on the same store see each other's
  files where their roots overlap, the same as two local disks on one filesystem. The default value is `"."`.

## Errors

`:reason` is the POSIX atom `Fil.Adapter.Local` would return in the same situation.

| Situation | `Fil` error | `:reason` |
| --- | --- | --- |
| a missing file | `Fil.NotFoundError` | `:enoent` |
| an exclusive create finding the file already there | `Fil.AlreadyExistsError` | `:eexist` |
| `verify_checksum: true` and content that doesn't match | `Fil.ChecksumMismatchError` | `:checksum_mismatch` |

# `t`

```elixir
@type t() :: %Fil.Adapter.Memory{prefix: String.t()}
```

# `allow`

```elixir
@spec allow(pid() | atom(), pid() | atom()) :: :ok
```

Lets `allowed` use the store of `owner`.

Both can be a pid or a registered name. `owner` must have a store, from `checkout/0` or from an earlier `allow/2`.
Processes that `owner` starts as `Task`s don't need this.

    setup do
      Fil.Adapter.Memory.checkout()
      Fil.Adapter.Memory.allow(self(), MyApp.Thumbnailer)
    end

# `checkout`

```elixir
@spec checkout() :: :ok
```

Creates a store owned by the calling process.

Returns `:ok`, so it can be the whole `setup` block. Calling it again in the same process keeps the existing store.

