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...

Posted by Radzishevskyi Sergey

Sergey is a tech-lead at 2amigOS. Has been working on the IT industry for more than 12 years. Possesses an extraordinary knowledge of OOD and OOP programming and design patterns and refactoring techniques. Self-motivated and highly responsible person.