Skip to main content

lib/attached/storage_backends/behaviour.ex

defmodule Attached.StorageBackends.Behaviour do
  @moduledoc """
  Behaviour contract for storage backends.

  A backend is a named instance in the registry — a `{module, config}` pair
  under `config :attached, :storage_backends` (see `Attached.StorageBackends`).
  Every callback receives that instance's config keyword list as its first
  argument: backend modules hold no global state, so the same module can back
  several named instances (e.g. two S3 buckets).

  Callers should not use backend modules directly — go through
  `Attached.StorageBackends`, which resolves the default instance and
  dispatches with its config.
  """

  @type key :: String.t()
  @type config :: keyword()

  @doc "Upload a file from `source_path` to the given `key`."
  @callback upload(config, key, source_path :: String.t(), opts :: keyword()) ::
              :ok | {:error, term()}

  @doc "Download the file at `key` and return its binary content."
  @callback download(config, key) :: {:ok, binary()} | {:error, term()}

  @doc "Return the partial content in the byte `range` of the file at `key`."
  @callback download_chunk(config, key, Range.t()) :: {:ok, binary()} | {:error, term()}

  @doc "Concatenate files at `source_keys` into a single file at `destination_key`."
  @callback compose(config, source_keys :: [key], destination_key :: key) ::
              :ok | {:error, term()}

  @doc "Delete the file at `key`."
  @callback delete(config, key) :: :ok | {:error, term()}

  @doc "Delete files at keys starting with the `prefix`."
  @callback delete_prefixed(config, prefix :: String.t()) :: :ok | {:error, term()}

  @doc "Return `true` if a file exists at `key`."
  @callback exists?(config, key) :: boolean()

  @doc "Return a URL for the file at `key`."
  @callback url(config, key, opts :: keyword()) :: String.t()

  @doc """
  Return a URL (plus the headers the client must send) for uploading the file
  at `key` directly from the browser via HTTP PUT.

  Options: `:content_type`, `:checksum` (base64 MD5, pinned via
  `Content-MD5`), `:byte_size` (pinned via `Content-Length`), `:expires_in`.

  Optional — backends that cannot offer direct uploads simply don't implement
  it; `Attached.StorageBackends.direct_upload_url/2` then returns
  `{:error, :not_supported}`.
  """
  @callback direct_upload_url(config, key, opts :: keyword()) ::
              {:ok, %{url: String.t(), headers: [{String.t(), String.t()}]}} | {:error, term()}

  @optional_callbacks direct_upload_url: 3
end