Packages

Stein.Storage contains common storage functions for our projects at SmartLogic.

Current section

Files

Jump to
stein_storage lib stein storage.ex
Raw

lib/stein/storage.ex

defmodule Stein.Storage do
@moduledoc """
`Stein.Storage` covers uploading, downloading, and deleting remote files
## Available backends
### FileBackend
The `Stein.Storage.FileBackend` is available for development purposes.
For the file backend, you can configure the folder Stein should use. This
should be a local folder that Elixir has read/write permissions to. It should
end with a trailing slash.
config :stein_storage,
backend: :file,
file_backend_folder: "uploads/"
The default folder is Stein's `priv/files`.
To fully support the file storage, you should also add a new `Plug.Static`
to your endpoint. This will let the URLs the backend returns load.
if Mix.env() == :dev do
plug(Plug.Static, at: "/uploads", from: "uploads/files")
end
### S3Backend
The `Stein.Storage.S3Backend` handles uploading, downloading, and deletes
from Amazon S3.
For the S3 backend, you can also configure the bucket Stein should upload to.
config :stein_storage,
backend: :s3,
bucket: "my-bucket"
### MockBackend
The `Stein.Storage.MockBackend` mocks out all actions for use in tests. Each
action is a no-op.
config :stein_storage,
backend: :test
"""
alias Stein.Storage.FileBackend
alias Stein.Storage.FileUpload
alias Stein.Storage.MockBackend
alias Stein.Storage.S3Backend
@typedoc """
A processed file ready for uploading
"""
@type file :: FileUpload.t()
@typedoc """
Key for where the document will be stored
Must start with a leading `/`
"""
@type key :: String.t()
@typedoc """
Options for a function
"""
@type opts :: Keyword.t()
@typedoc """
A local file path
"""
@type local_path :: Path.t()
@typedoc """
The URL for viewing the remote file
"""
@type url :: String.t()
@doc """
Delete files from remote storage
"""
@callback delete(key()) :: :ok
@doc """
Download files from remote storage
*Note*: this creates a temporary file and must be cleaned up manually
"""
@callback download(key()) :: {:ok, local_path()}
@doc """
Upload files to the remote storage
"""
@callback upload(file(), key(), opts()) :: :ok | :error
@doc """
Get the remote url for viewing an uploaded file
"""
@callback url(key(), opts()) :: url()
@doc false
def config({:system, name}), do: System.get_env(name)
def config(value), do: value
@doc """
Delete files from remote storage
"""
@spec delete(key()) :: :ok
def delete(key) do
backend().delete(key)
end
@doc """
Download files from remote storage
*Note*: this creates a temporary file and must be cleaned up manually
"""
@spec download(key()) :: {:ok, local_path()}
def download(key) do
backend().download(key)
end
@doc """
Upload files to the remote storage
## Limiting extensions
You can limit extensions with the `extensions` option. Only the extensions in the list
will be allowed, any other extension will be rejected with `{:error, :invalid_extension}`.
Each extension should start with a `.`.
Stien.Storage.upload(file, key, extensions: [".jpg", ".png"])
"""
@spec upload(file(), key(), opts()) :: :ok | {:error, :invalid_extension} | {:error, :uploading}
def upload(file, key, opts) do
path = prep_file(file)
with {:ok, :extension} <- check_extensions(path, opts) do
backend().upload(path, key, opts)
end
end
@doc false
def check_extensions(file, opts) do
allowed_extensions = Keyword.get(opts, :extensions)
check_allowed_extensions(file, allowed_extensions)
end
defp check_allowed_extensions(_file, nil), do: {:ok, :extension}
defp check_allowed_extensions(file, allowed_extensions) do
extension = String.downcase(file.extension)
case extension in allowed_extensions do
true ->
{:ok, :extension}
false ->
{:error, :invalid_extension}
end
end
@doc """
Get the remote url for viewing an uploaded file
"""
@spec url(key(), opts()) :: url()
def url(key, opts \\ []) do
backend().url(key, opts)
end
@doc """
Prepare a file for upload to the backend
Must be a `Stein.Storage.FileUpload`, `Plug.Upload`, or a map that
has the `:path` key.
"""
@spec prep_file(file()) :: file()
@spec prep_file(Plug.Upload.t()) :: file()
@spec prep_file(%{path: String.t()}) :: file()
def prep_file(upload = %FileUpload{}), do: upload
def prep_file(upload = %Plug.Upload{}) do
%FileUpload{
filename: upload.filename,
extension: Path.extname(upload.filename),
path: upload.path
}
end
def prep_file(upload) when is_map(upload) do
filename = Path.basename(upload.path)
%FileUpload{
filename: Path.basename(upload.path),
extension: Path.extname(filename),
path: upload.path
}
end
@doc false
def backend() do
case Application.get_env(:stein_storage, :backend) do
:file ->
FileBackend
:s3 ->
S3Backend
:test ->
MockBackend
end
end
end