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, 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 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:

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:

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:

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:

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:

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:

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 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:

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 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 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:

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

Build one disk per kind of storage:

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:

{: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:

{: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:

{: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.

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:

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 (PHP): the scope, one API across many kinds of storage
  • Req: the API design, with disks as plain values and plugins that attach to them
  • 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.

Summary

Building

Attaches a plugin callback to a disk under a name.

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

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

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

Operations

Copies a file and returns the destination ref.

Copies a file. See cp/2.

Copies a file. See cp/2.

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

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

Whether anything exists at this path.

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

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

Lists a directory. See ls/1.

Lists a directory. See ls/1.

Reads a file.

Reads a file. See read/1.

Reads a file. See read/1.

Moves a file and returns the destination ref.

Deletes a file.

Deletes a file. See rm/1.

Deletes a file. See rm/1.

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

Removes everything under a prefix. See rm_rf/1.

Removes everything under a prefix. See rm_rf/1.

Builds a URL that grants temporary access to a file.

Builds a signed URL. See signed_url/1.

Builds a signed URL. See signed_url/1.

Returns metadata for a file or directory.

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

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

Builds the public URL of a file.

Builds a public URL. See url/1.

Builds a public URL. See url/1.

Writes a file, creating missing parent directories.

Writes a file. See write/2.

Bang variants

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Types

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.

A plugin callback: a function or a {module, function} pair. See the Plugins guide.

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

Building

attach(disk, name, callback, opts \\ [])

@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 explains how to write one.

detach(disk, name)

@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(opts)

@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(disk, path)

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

Operations

cp(src, dest)

@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(disk, src_path, dest)

@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(disk, src_path, dest, opts)

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

Copies a file. See cp/2.

dir?(ref)

@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?(disk, path)

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

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

exists?(ref)

@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?(disk, path)

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

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

ls(disk)

@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 (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(disk, path)

@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(disk, path, opts)

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

Lists a directory. See ls/1.

read(ref)

@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 (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(disk, path)

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

Reads a file. See read/1.

read(disk, path, opts)

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

Reads a file. See read/1.

rename(src, dest)

@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(disk, src_path, dest)

@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(disk, src_path, dest, opts)

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

Moves a file. See rename/2.

rm(ref)

@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(disk, path)

@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(disk, path, opts)

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

Deletes a file. See rm/1.

rm_rf(ref)

@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(disk, path)

@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(disk, path, opts)

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

Removes everything under a prefix. See rm_rf/1.

signed_url(ref)

@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(disk, path)

@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(disk, path, opts)

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

Builds a signed URL. See signed_url/1.

stat(ref)

@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(disk, path)

@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(disk, path, opts)

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

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

url(ref)

@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(disk, path)

@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(disk, path, opts)

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

Builds a public URL. See url/1.

write(ref, content)

@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 (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(disk, path, content)

@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(disk, path, content, opts)

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

Writes a file. See write/2.

Bang variants

cp!(src, dest)

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

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

cp!(a, b, c)

@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!(a, b, c, d)

@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!(target)

@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!(a, b)

@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!(a, b, c)

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

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

read!(ref)

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

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

read!(a, b)

@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!(a, b, c)

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

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

rename!(src, dest)

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

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

rename!(a, b, c)

@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!(a, b, c, d)

@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!(ref)

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

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

rm!(a, b)

@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!(a, b, c)

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

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

rm_rf!(ref)

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

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

rm_rf!(a, b)

@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!(a, b, c)

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

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

signed_url!(ref)

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

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

signed_url!(a, b)

@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!(a, b, c)

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

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

stat!(ref)

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

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

stat!(a, b)

@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!(a, b, c)

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

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

url!(ref)

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

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

url!(a, b)

@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!(a, b, c)

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

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

write!(ref, content)

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

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

write!(a, b, c)

@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!(a, b, c, d)

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

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

Types

error()

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.

plugin_callback()

@type plugin_callback() ::
  (Fil.Op.t(), (Fil.Op.t() -> Fil.Op.t()), keyword() -> Fil.Op.t())
  | {module(), atom()}

A plugin callback: a function or a {module, function} pair. See the Plugins guide.

result(value)

@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 error/0.