defmodule Depot do @external_resource "README.md" @moduledoc @external_resource |> File.read!() |> String.split("") |> Enum.fetch!(1) @type adapter :: module() @type filesystem :: {module(), Depot.Adapter.config()} @doc """ Write to a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.write(filesystem, "test.txt", "Hello World") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end LocalFileSystem.write("test.txt", "Hello World") """ @spec write(filesystem, Path.t(), iodata(), keyword()) :: :ok | {:error, term} def write({adapter, config}, path, contents, opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.write(config, path, contents, opts) end end @doc """ Returns a `Stream` for writing to the given `path`. ## Options The following stream options apply to all adapters: * `:chunk_size` - When reading, the amount to read, usually expressed as a number of bytes. ## Examples > Note: The shape of the returned stream will > necessarily depend on the adapter in use. In the > following examples the [`Local`](`Depot.Adapter.Local`) > adapter is invoked, which returns a `File.Stream`. ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") {:ok, %File.Stream{}} = Depot.write_stream(filesystem, "test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end {:ok, %File.Stream{}} = LocalFileSystem.write_stream("test.txt") """ def write_stream({adapter, config}, path, opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.write_stream(config, path, opts) end end @doc """ Read from a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") {:ok, "Hello World"} = Depot.read(filesystem, "test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end {:ok, "Hello World"} = LocalFileSystem.read("test.txt") """ @spec read(filesystem, Path.t(), keyword()) :: {:ok, binary} | {:error, term} def read({adapter, config}, path, _opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.read(config, path) end end @doc """ Returns a `Stream` for reading the given `path`. ## Options The following stream options apply to all adapters: * `:chunk_size` - When reading, the amount to read, usually expressed as a number of bytes. ## Examples > Note: The shape of the returned stream will > necessarily depend on the adapter in use. In the > following examples the [`Local`](`Depot.Adapter.Local`) > adapter is invoked, which returns a `File.Stream`. ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") {:ok, %File.Stream{}} = Depot.read_stream(filesystem, "test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end {:ok, %File.Stream{}} = LocalFileSystem.read_stream("test.txt") """ def read_stream({adapter, config}, path, opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.read_stream(config, path, opts) end end @doc """ Delete a file from a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.delete(filesystem, "test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end :ok = LocalFileSystem.delete("test.txt") """ @spec delete(filesystem, Path.t(), keyword()) :: :ok | {:error, term} def delete({adapter, config}, path, _opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.delete(config, path) end end @doc """ Move a file from source to destination on a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.move(filesystem, "test.txt", "other-test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end :ok = LocalFileSystem.move("test.txt", "other-test.txt") """ @spec move(filesystem, Path.t(), Path.t(), keyword()) :: :ok | {:error, term} def move({adapter, config}, source, destination, opts \\ []) do with {:ok, source} <- Depot.RelativePath.normalize(source), {:ok, destination} <- Depot.RelativePath.normalize(destination) do adapter.move(config, source, destination, opts) end end @doc """ Copy a file from source to destination on a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.copy(filesystem, "test.txt", "other-test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end :ok = LocalFileSystem.copy("test.txt", "other-test.txt") """ @spec copy(filesystem, Path.t(), Path.t(), keyword()) :: :ok | {:error, term} def copy({adapter, config}, source, destination, opts \\ []) do with {:ok, source} <- Depot.RelativePath.normalize(source), {:ok, destination} <- Depot.RelativePath.normalize(destination) do adapter.copy(config, source, destination, opts) end end @doc """ Copy a file from source to destination on a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.copy(filesystem, "test.txt", "other-test.txt") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end :ok = LocalFileSystem.copy("test.txt", "other-test.txt") """ @spec file_exists(filesystem, Path.t(), keyword()) :: {:ok, :exists | :missing} | {:error, term} def file_exists({adapter, config}, path, _opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.file_exists(config, path) end end @doc """ List the contents of a folder on a filesystem ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") {:ok, contents} = Depot.list_contents(filesystem, ".") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end {:ok, contents} = LocalFileSystem.list_contents(".") """ @spec list_contents(filesystem, Path.t(), keyword()) :: {:ok, [%Depot.Stat.Dir{} | %Depot.Stat.File{}]} | {:error, term} def list_contents({adapter, config}, path, _opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.list_contents(config, path) end end @doc """ Create a directory ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.create_directory(filesystem, "test/") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end LocalFileSystem.create_directory("test/") """ @spec create_directory(filesystem, Path.t(), keyword()) :: :ok | {:error, term} def create_directory({adapter, config}, path, opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path), {:ok, path} <- Depot.RelativePath.assert_directory(path) do adapter.create_directory(config, path, opts) end end @doc """ Delete a directory. ## Options * `:recursive` - Recursively delete contents. Defaults to `false`. ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.delete_directory(filesystem, "test/") ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end LocalFileSystem.delete_directory("test/") """ @spec delete_directory(filesystem, Path.t(), keyword()) :: :ok | {:error, term} def delete_directory({adapter, config}, path, opts \\ []) do with {:ok, path} <- Depot.RelativePath.normalize(path), {:ok, path} <- Depot.RelativePath.assert_directory(path) do adapter.delete_directory(config, path, opts) end end @doc """ Clear the filesystem. This is always recursive. ## Examples ### Direct filesystem filesystem = Depot.Adapter.Local.configure(prefix: "/home/user/storage") :ok = Depot.clear(filesystem) ### Module-based filesystem defmodule LocalFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end LocalFileSystem.clear() """ @spec clear(filesystem, keyword()) :: :ok | {:error, term} def clear({adapter, config}, _opts \\ []) do adapter.clear(config) end @spec set_visibility(filesystem, Path.t(), Depot.Visibility.t()) :: :ok | {:error, term} def set_visibility({adapter, config}, path, visibility) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.set_visibility(config, path, visibility) end end @spec visibility(filesystem, Path.t()) :: {:ok, Depot.Visibility.t()} | {:error, term} def visibility({adapter, config}, path) do with {:ok, path} <- Depot.RelativePath.normalize(path) do adapter.visibility(config, path) end end @doc """ Copy a file from one filesystem to the other This can either be done natively if the same adapter is used for both filesystems or by streaming/read-write cycle the file from the source to the local system and back to the destination. ## Examples ### Direct filesystem filesystem_source = Depot.Adapter.Local.configure(prefix: "/home/user/storage") filesystem_destination = Depot.Adapter.Local.configure(prefix: "/home/user/storage2") :ok = Depot.copy_between_filesystem({filesystem_source, "test.txt"}, {filesystem_destination, "copy.txt"}) ### Module-based filesystem defmodule LocalSourceFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage" end defmodule LocalDestinationFileSystem do use Depot.Filesystem, adapter: Depot.Adapter.Local, prefix: "/home/user/storage2" end :ok = Depot.copy_between_filesystem( {LocalSourceFileSystem.__filesystem__(), "test.txt"}, {LocalDestinationFileSystem.__filesystem__(), "copy.txt"} ) """ @spec copy_between_filesystem( source :: {filesystem, Path.t()}, destination :: {filesystem, Path.t()}, keyword() ) :: :ok | {:error, term} def copy_between_filesystem(source, destination, opts \\ []) # Same adapter, same config -> just do a plain copy def copy_between_filesystem({filesystem, source}, {filesystem, destination}, opts) do copy(filesystem, source, destination, opts) end # Same adapter -> try direct copy if supported def copy_between_filesystem( {{adapter, config_source}, path_source} = source, {{adapter, config_destination}, path_destination} = destination, opts ) do with :ok <- adapter.copy(config_source, path_source, config_destination, path_destination, opts) do :ok else {:error, :unsupported} -> copy_via_local_memory(source, destination, opts) error -> error end end # different adapter def copy_between_filesystem(source, destination, opts) do copy_via_local_memory(source, destination, opts) end defp copy_via_local_memory( {{source_adapter, _} = source_filesystem, source_path}, {{destination_adapter, _} = destination_filesystem, destination_path}, opts ) do case {Depot.read_stream(source_filesystem, source_path, opts), Depot.write_stream(destination_filesystem, destination_path, opts)} do # A and B support streaming -> Stream data {{:ok, read_stream}, {:ok, write_stream}} -> read_stream |> Stream.into(write_stream) |> Stream.run() # Only A support streaming -> Stream to memory and write when done {{:ok, read_stream}, {:error, ^destination_adapter}} -> Depot.write(destination_filesystem, destination_path, Enum.into(read_stream, [])) # Only B support streaming -> Load into memory and stream to B {{:error, ^source_adapter}, {:ok, write_stream}} -> with {:ok, contents} <- Depot.read(source_filesystem, source_path) do contents |> chunk(Keyword.get(opts, :chunk_size, 5 * 1024)) |> Enum.into(write_stream) end # Neither support streaming {{:error, ^source_adapter}, {:error, ^destination_adapter}} -> with {:ok, contents} <- Depot.read(source_filesystem, source_path) do Depot.write(destination_filesystem, destination_path, contents) end end rescue e -> {:error, e} end @doc false # Also used by the InMemory adapter and therefore not private def chunk("", _size), do: [] def chunk(binary, size) when byte_size(binary) >= size do {chunk, rest} = :erlang.split_binary(binary, size) [chunk | chunk(rest, size)] end def chunk(binary, _size), do: [binary] end