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")
endOptions
: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:andlength:ask for. - Reading a directory returns a
Fil.NotFoundError. verify_checksum: truecompares the content with the checksum the write stored.
Fil.stream/3
- Looks up the content (or the part that
offset:andlength:ask for) each time the stream is read, and returns it in chunks of 64 KiB. verify_checksum: truecompares 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/xwhenreport.txtis a file writes a second file and leaves the first alone.
Fil.rm/3
- Removing a directory succeeds and removes nothing.
Fil.stat/3
:etagis the MD5 of the content in hex (the ETag S3 returns for a single-part upload).:content_typeis thecontent_type:the write stored.checksum:returns the stored checksum if the write used the same algorithm, andnilotherwise.- A stat on a directory returns
%Fil.Stat{type: :directory}with every other fieldnil.
Fil.cp/4
- The copy keeps the content type and the checksum.
- With
if_exists: :error, uses:ets.insert_new/2, like a write. - Copying a directory returns a
Fil.NotFoundError. - A copy onto itself leaves the file as it is. With
if_exists: :error, it returns aFil.AlreadyExistsError.
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 aFil.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 aFil.AlreadyExistsError.
Fil.url/3 and Fil.signed_url/3
- A memory store has no URLs. Attach
Fil.Plugin.URLto build them, andFil.Plugserves them, in a test throughPhoenix.ConnTesttoo.
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.Tasksets$callers, and so doesPhoenix.LiveViewTestfor the LiveView processes it starts, so both find the test's store withoutallow/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.
| Situation | Fil error | :reason |
|---|---|---|
| a missing file | Fil.NotFoundError | :enoent |
| an exclusive create finding the file already there | Fil.AlreadyExistsError | :eexist |
a source that a write replaced during a move with if_exists: :error | Fil.ConflictError | :source_changed |
verify_checksum: true and content that doesn't match | Fil.ChecksumMismatchError | :checksum_mismatch |
Summary
Types
@type t() :: %Fil.Adapter.Memory{prefix: String.t()}
Functions
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
@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.