lib/depot/filesystem.ex

defmodule Depot.Filesystem do
  @moduledoc """
  Behaviour of a `Depot` filesystem.
  """
  @callback write(path :: Path.t(), contents :: binary, opts :: keyword()) :: :ok | {:error, term}
  @callback read(path :: Path.t(), opts :: keyword()) :: {:ok, binary} | {:error, term}
  @callback read_stream(path :: Path.t(), opts :: keyword()) ::
              {:ok, Enumerable.t()} | {:error, term}
  @callback delete(path :: Path.t(), opts :: keyword()) :: :ok | {:error, term}
  @callback move(source :: Path.t(), destination :: Path.t(), opts :: keyword()) ::
              :ok | {:error, term}
  @callback copy(source :: Path.t(), destination :: Path.t(), opts :: keyword()) ::
              :ok | {:error, term}
  @callback file_exists(path :: Path.t(), opts :: keyword()) ::
              {:ok, :exists | :missing} | {:error, term}
  @callback list_contents(path :: Path.t(), opts :: keyword()) ::
              {:ok, [%Depot.Stat.Dir{} | %Depot.Stat.File{}]} | {:error, term}

  @doc false
  @spec __using__(Macro.t()) :: Macro.t()
  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      @behaviour Depot.Filesystem
      {adapter, opts} = Depot.Filesystem.parse_opts(__MODULE__, opts)
      @adapter adapter
      @opts opts
      @key {Depot.Filesystem, __MODULE__}

      def init do
        filesystem =
          @opts
          |> Depot.Filesystem.merge_app_env(__MODULE__)
          |> @adapter.configure()

        :persistent_term.put(@key, filesystem)

        filesystem
      end

      def __filesystem__ do
        :persistent_term.get(@key, init())
      end

      if adapter.starts_processes() do
        def child_spec(_) do
          Supervisor.child_spec(__filesystem__(), %{})
        end
      end

      @impl true
      def write(path, contents, opts \\ []),
        do: Depot.write(__filesystem__(), path, contents, opts)

      @impl true
      def read(path, opts \\ []),
        do: Depot.read(__filesystem__(), path, opts)

      @impl true
      def read_stream(path, opts \\ []),
        do: Depot.read_stream(__filesystem__(), path, opts)

      @impl true
      def delete(path, opts \\ []),
        do: Depot.delete(__filesystem__(), path, opts)

      @impl true
      def move(source, destination, opts \\ []),
        do: Depot.move(__filesystem__(), source, destination, opts)

      @impl true
      def copy(source, destination, opts \\ []),
        do: Depot.copy(__filesystem__(), source, destination, opts)

      @impl true
      def file_exists(path, opts \\ []),
        do: Depot.file_exists(__filesystem__(), path, opts)

      @impl true
      def list_contents(path, opts \\ []),
        do: Depot.list_contents(__filesystem__(), path, opts)
    end
  end

  def parse_opts(module, opts) do
    opts
    |> merge_app_env(module)
    |> Keyword.put_new(:name, module)
    |> Keyword.pop!(:adapter)
  end

  def merge_app_env(opts, module) do
    case Keyword.fetch(opts, :otp_app) do
      {:ok, otp_app} ->
        config = Application.get_env(otp_app, module, [])
        Keyword.merge(opts, config)

      :error ->
        opts
    end
  end
end