Fil.Adapter.Memory (Fil v0.2.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

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

Operations

For each Fil function, this section says how it works on the store and where it departs from the contract that every adapter follows. Anything it doesn't mention follows the contract. Directories exist only as prefixes, like on an object store, so there are no empty directories.

Fil.read/3

  • Reads the content from the store, or only the part that offset: and length: ask for.
  • Reading a directory returns a Fil.NotFoundError.
  • verify_checksum: true compares the content with the checksum the write stored.

Fil.stream/3

  • Looks up the content (or the part that offset: and length: ask for) each time the stream is read, and returns it in chunks of 64 KiB.
  • verify_checksum: true compares the content before the first chunk.

Fil.write/4

  • Stores the content with one :ets.insert/2, so writes are atomic. Collects a stream first.
  • With if_exists: :error, uses :ets.insert_new/2, so the check is atomic too.
  • checksum: stores the checksum of the content.
  • Writing to report.txt/x when report.txt is a file writes a second file and leaves the first alone.

Fil.rm/3

  • Removing a directory succeeds and removes nothing.

Fil.stat/3

  • :etag is the MD5 of the content in hex (the ETag S3 returns for a single-part upload).
  • :content_type is the content_type: the write stored.
  • checksum: returns the stored checksum if the write used the same algorithm, and nil otherwise.
  • A stat on a directory returns %Fil.Stat{type: :directory} with every other field nil.

Fil.cp/4

Fil.rename/4

  • Copies the file, then removes the source, but only while it's still the file that was copied (checked and removed in one step with :ets.select_delete/2). A write that replaced the source in the meantime stays.
  • With if_exists: :error, such a write fails the move with a Fil.ConflictError, and the destination is left as it was.
  • A move onto itself leaves the file as it is. With if_exists: :error, it returns a Fil.AlreadyExistsError.

Fil.url/3 and Fil.signed_url/3

Fil.Disk.same_storage?/2

  • Compares the normalized :root.

Details

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.

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
a source that a write replaced during a move with if_exists: :errorFil.ConflictError:source_changed
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.