Fil.Adapter.Memory (Fil v0.1.0)

Copy Markdown View Source

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 (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.

SituationFil error:reason
a missing fileFil.NotFoundError:enoent
an exclusive create finding the file already thereFil.AlreadyExistsError:eexist
verify_checksum: true and content that doesn't matchFil.ChecksumMismatchError:checksum_mismatch

Summary

Functions

Lets allowed use the store of owner.

Creates a store owned by the calling process.

Types

t()

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

Functions

allow(owner, allowed)

@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 Tasks don't need this.

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

checkout()

@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.