Elixir: Work with Phoenix models and images
Today I’m going to talk about elixir and phoenix framework. It’s my first post about them and today I would like to tell you how did I solve pretty common problem for web development – serve images for your domain models.
Problem:
We develop web application. We have users and companies and users have avatars, companies have logos and any other model can have referring image. I need to have ability to save images for my models. I want to have ability to save them to AWS S3 and locally(in future I want to add new providers).
I want to get:
model refers to image (user.image_id => image.id company.image_id => image.id). See image below. image_refs
I need to implement AWS S3 and local adapters to serve images.
I want to have ability to get image url from model user.image_url(:avatar_id) App.User.image_url(user, :image_id)
I want to have ability to get resized thumbs App.user.image_url user, :image_id, width: 200, height: 250 (this will be in part #2)
Let’s go.
Let’s started from upload manager. I need a component that will allow to manage my images. As I mentioned below I want to have ability to upload my images to different storages. So I need some interface for such components. In elixir we don’t have pure interfaces but we have @behaviour-s. Here is what I need – save!, exists?, delete!, url.
defmodule App.UploadManager.Behavior do
@doc """
Check if file exists
"""
@callback exists?(String.t) :: :ok | {:error, String.t}
@doc """
Save file
"""
@callback save!(String.t, String.t, Map) :: :ok | {:error, String.t}
@doc """
Delete file
"""
@callback delete!(String.t) :: :ok | {:error, String.t}
@doc """
Return URL to file
"""
@callback url(String.t) :: String.t
end
Local upload manager.
Ok next step is real implementation of upload manager and lets start from local file storage.
defmodule App.UploadManager.Local do
@behaviour App.UploadManager.Behavior
# config :myapp, file_manager: [
# local: [
# base_path: Path.dirname(__DIR__) <> "/web/static/assets/media",
# base_url: "http://localhost:4000/media"
# ]
#]
@config Application.get_env(:myapp, :file_manager)[:local]
@doc """
Check if file exists
"""
def exists?(destination) do
destination = build_path(destination)
File.exists?(destination)
end
@doc """
Save file to disk
"""
def save!(source, destination, _opts \\ %{}) do
destination = build_path(destination)
dirname = Path.dirname(destination)
case File.mkdir_p(dirname) do
:ok -> save_file(source, destination)
{:error, reason} -> {:error, reason}
end
end
defp save_file(source, destination) when is_binary(source), do: File.write(destination, source)
defp save_file(source, destination) when is_bitstring(source), do: File.copy(source, destination)
@doc """
Delete file from disk
"""
def delete!(destination) do
destination = build_path(destination)
File.rm(destination)
end
@doc """
return URL for file
"""
def url(key) do
base_url = @config[:base_url]
"#{base_url}/#{key}"
end
# build full file path including base path from config
defp build_path(path) do
@config[:base_path] <> "/" <> path
end
end
We use elixir File to manage file locally we need to specify base path for our uploads that will be available for web server.
AWS S3 upload manager.
For AWS we need to add some deps to our code.
{:ex_aws, “~> 1.0.0-beta0”},
{:sweet_xml, “~> 0.6.0”},
defmodule App.UploadManager.S3 do
@behaviour App.UploadManager.Behavior
# config example:
#
# config :ex_aws,
# access_key_id: "key",
# secret_access_key: "secret",
# region: "eu-central-1",
# s3: [
# scheme: "https://",
# host: "s3.eu-central-1.amazonaws.com",
# region: "eu-central-1"
# ]
@config Application.get_env(:myapp, :file_manager)[:s3]
alias ExAws.S3
@doc """
Check if file exists on S3 bucket
"""
def exists?(key) do
case S3.head_object(@config[:bucket], key) |> ExAws.request do
{:ok, _body} -> :ok
{:error, error} -> {:error, error}
end
end
@doc """
Save file to s3 bucket
"""
def save!(source_file, key, opts \\ %{}) do
opts = if (Map.get(opts, :acl) == nil), do: Map.put(opts, :acl, :public_read)
source_file = if is_binary(source_file), do: source_file, else: File.read!(source_file)
{:ok, response} = S3.put_object(@config[:bucket], key, source_file, opts) |> ExAws.request
case response.status_code do
200 -> :ok
_ -> {:error, response.body}
end
end
@doc """
Delete file from S3
"""
def delete!(key) do
{:ok, response} = S3.delete_object(@config[:bucket], key) |> ExAws.request
case response.status_code do
204 -> :ok
_ -> {:error, response.body}
end
end
@doc """
return URL for file
"""
def url(key) do
# todo add ability to return URL with expired param
bucket = @config[:bucket]
bucket_host = Application.get_env(:ex_aws, :s3)
bucket_host = bucket_host[:host]
"https://#{bucket_host}/#{bucket}/#{key}"
end
end
Now we ready to attach images to app models. Read more...