Fil.Disk (Fil v0.2.0)

Copy Markdown View Source

A disk is a plain value that describes where files are stored.

Build one with Fil.disk/1 (short for new/1) and pass it around. There's no global configuration, no registry and no process behind it:

disk = Fil.disk(adapter: Fil.Adapter.Local, root: "priv/storage")

The struct holds the adapter module together with the state its Fil.Adapter.init/1 returned, and the plugins attached with Fil.attach/4 or the :plugins option of new/1. The adapter state may contain credentials, so %Fil.Disk{} has a custom Inspect implementation that prints only the adapter label:

iex> inspect(Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil"))
"#Fil.Disk<local>"

Refs and errors contain the disk too. Logging them is safe, because logs use inspect, but anything that stores the raw term (:erlang.term_to_binary/1, a crash dump) stores the credentials with it.

Summary

Types

A disk, or a 0-arity function or {module, function, args} that returns one. See resolve/1.

t()

Functions

Returns the adapter module backing disk.

A short, human readable label for the disk's adapter.

Builds a disk. Fil.disk/1 is the same function.

Returns the disk for a source/0: the disk itself, the result of a 0-arity function, or the result of {module, function, args}.

Returns whether two disks are the same storage, so a path names the same file on both.

Types

source()

@type source() :: t() | (-> t()) | {module(), atom(), [term()]}

A disk, or a 0-arity function or {module, function, args} that returns one. See resolve/1.

t()

@type t() :: %Fil.Disk{
  adapter: {module(), term()},
  plugins: [{atom(), Fil.plugin_callback(), keyword()}],
  signed_url: keyword()
}

Functions

adapter(disk)

@spec adapter(t()) :: module()

Returns the adapter module backing disk.

iex> Fil.Disk.adapter(Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil"))
Fil.Adapter.Local

label(module)

@spec label(t()) :: String.t()
@spec label(module()) :: String.t()

A short, human readable label for the disk's adapter.

Used by the Inspect implementations of Fil.Disk and Fil.Ref.

iex> Fil.Disk.label(Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil"))
"local"

new(opts)

@spec new(keyword()) :: t()

Builds a disk. Fil.disk/1 is the same function.

iex> Fil.Disk.new(adapter: Fil.Adapter.Local, root: "/tmp/fil")
#Fil.Disk<local>

The adapter validates its options here, once. Invalid options raise ArgumentError when the disk is built instead of failing on first use.

Options

  • :adapter (atom/0) - Required. The adapter module. Every option other than :adapter, :plugins and :signed_url is passed to the adapter, which validates it against its own schema.

  • :plugins (list of tuple of atom/0, atom/0, keyword/0 values) - Plugins to attach, in order, so the first one is the outermost. Each is a plugin callback as {module, function, opts}, attached under the name module (see Fil.attach/4). They're plain data, so the plugins can come from config along with the adapter options: plugins: [{Fil.Plugin.ContentType, :call, default: "text/plain"}]. The default value is [].

  • :signed_url (keyword/0) - Default options of Fil.signed_url/3 on this disk. The options of a call replace them one by one, so signed_url: [expires_in: 86_400, disposition: :attachment] gives every signed URL a day and every download the :attachment disposition, and Fil.signed_url(disk, path, disposition: :inline) still shows a file inline. A default that doesn't fit the method is left out instead of raising, so a default :disposition doesn't break method: :put. Fil.LiveView.external/2 passes its own :expires_in and :if_exists, so these defaults don't apply to its upload URLs. The default value is [].

    • :expires_in - How long the URL stays valid, in seconds. At most 7 days (604800), the cap of S3.

    • :disposition - The content-disposition of downloads. Uploads (method: :put) leave it out.

    • :if_exists - What an upload does if a file is already at the path. Downloads leave it out.

resolve(disk)

@spec resolve(source()) :: t()

Returns the disk for a source/0: the disk itself, the result of a 0-arity function, or the result of {module, function, args}.

Integrations such as Fil.Plug take their disk in these forms and call this each time they need it. A function or an MFA can build the disk from runtime config, where a disk in the options would be fixed at compile time. Where the options are compiled, as a plug's are, the function has to be a capture with the module name, such as &MyApp.Storage.uploads/0:

iex> disk = Fil.disk(adapter: Fil.Adapter.Memory)
iex> Fil.Disk.resolve(disk) == disk
true
iex> Fil.Disk.resolve(fn -> disk end) == disk
true
iex> Fil.Disk.resolve({Fil, :disk, [[adapter: Fil.Adapter.Memory]]})
#Fil.Disk<memory>

Raises ArgumentError when the function returns something other than a disk, or source is none of the three.

same_storage?(disk1, disk2)

@spec same_storage?(t(), t()) :: boolean()

Returns whether two disks are the same storage, so a path names the same file on both.

They are when they have the same adapter and the same address (Fil.Adapter.address/1): the same root on a local disk, the same bucket, region, endpoint and prefix on S3. Plugins don't count, and neither do credentials or other options that only change how the storage is reached:

iex> uploads = Fil.disk(adapter: Fil.Adapter.S3, bucket: "uploads", region: "eu-central-1")
iex> rotated = Fil.disk(adapter: Fil.Adapter.S3, bucket: "uploads", region: "eu-central-1", session_token: "new")
iex> Fil.Disk.same_storage?(uploads, rotated)
true
iex> Fil.Disk.same_storage?(uploads, Fil.disk(adapter: Fil.Adapter.S3, bucket: "backups", region: "eu-central-1"))
false

Fil.Ecto.Ref uses it to check that a ref fits a field.