Image Upload in Phoenix

I’ve been having tons of fun lately learning Elixir and, being a Rails developer, it was only a matter of time before I tried out Phoenix. Phoenix is a MVC style web framework for Elixir that follows the convention over configuration style of Rails. In this article, I will demonstrate a simple example of how to handle image upload, storage, and association in Phoenix.

Arc

For the actual multipart file upload portion of this example, we are going to take advantage of the file upload functionality built into Phoenx, but that will get us only so far. We have to deal with:

  • Copying the uploaded images to some publicly accessible directory
  • Storing the path to those files in our DB
  • Processing or cropping thumbnails with ImageMagick
  • Integrating with S3

That DOES sound fun, but sometimes I just want to get to the point and build what I want to build!

Enter Arc! Arc is an Elixir library oddly reminiscent of Ruby’s Carrierwave gem that handles the majority of what we need to get this task done. Arc will deal with storing and processing our files as well as persisting the association between our model and the uploaded image.

Building the App

For this example, we will do something simple. We will create an app that stores Users who have an avatar, username, and email address.

Create the New Phoenix App

Lets use Mix to create a new Phoenix application. Mix is Elixir’s build-tool that we use for creating files, compiling, testing and a variety of other tasks.

mix phoenix.new my_app

Once this command is done running, we can cd into our new app and start up the phoenix server.

cd my_app && mix phoenix.server

Everything should start right up and we can see the Phoenix welcome screen at http://localhost:4000.

If you run into any problems, please run through Phoenix’s Up and Running guide.

Create the User Model

Now, we will use Mix again to create a User model that has an avatar, username, and email. All these attributes will be strings, including the avatar attribute which is intended to just persist the local path to where our uploaded image is stored. Similar to Rails’ rails g scaffold command, mix phoenix.gen.html will create any Phoenix views, templates, models, controllers, and test files we need.

mix phoenix.gen.html User users avatar:string username:string email:string

Now we need to create the database and run the migration generated by the command above by using the Mix tasks create and migrate provided by our Phoenix database adapter Ecto. If your mix ecto.create command happens to fail, refer to Phoenix’s ecto.create documentation to properly configure your database.

mix ecto.create
mix ecto.migrate

Lastly, you will need to add a users resource to your routes file. This can be a little bit tricky because order does matter in this file.

Add the following line after the get "/", PageController, :index in your web/routes.ex file.

web/routes.ex

scope "/", Roblist do
#...
  resources "/users", UserController
#...
end

Running mix phoenix.routes should now show you all the new routes available within your Phoenix app.

$ mix phoenix.routes
page_path  GET     /                MyApp.PageController :index
user_path  GET     /users           MyApp.UserController :index
user_path  GET     /users/:id/edit  MyApp.UserController :edit
user_path  GET     /users/new       MyApp.UserController :new
user_path  GET     /users/:id       MyApp.UserController :show
user_path  POST    /users           MyApp.UserController :create
user_path  PATCH   /users/:id       MyApp.UserController :update
           PUT     /users/:id       MyApp.UserController :update
user_path  DELETE  /users/:id       MyApp.UserController :delete

Adding Arc

For this example, we will be using v0.3.2 of arc_ecto which is an Elixir library that, as its name implies, provides integration with Ecto. The latest version of arc_ecto (as of 10 May 2016) is v0.4.1, but that version uses Ecto v2, which is not the version of Ecto that Phoenix (currently) ships with. There is no reason why you cannot upgrade to Ecto v2, but that is outside the scope of this blog post. The version arc_ect v0.3.2 will work just fine.

Add arc_ecto and Arc to your deps in your mix.exs file.

mix.exs

defp deps do
#...
  {:arc_ecto, "~> 0.3.1"},
  {:arc, "0.2.0"},
#...
end

Then fetch your new dependencies:

mix deps.get

Creating an Uploader

Now that our dependencies have been updated, we can generate a new uploader called Avatar.

mix arc.g avatar

This will create an Elixir module that can be found at web/uploaders/avatar.ex. This file will also need an additional Elixir using macro Arc.Ecto.Definition added to it so we can use arc_ecto.

defmodule MyApp.Avatar do
  use Arc.Definition
  use Arc.Ecto.Definition

  # ...
end

Now, for the final step, we need to connect our User module and Avatar uploader together. Let’s add a Arc.Ecto.Model using statement to the top of our User model’s code and change the type of our :avatar field to MyApp.Avatar.Type in the model’s schema. We also need to make some adjustments to our changeset function to properly handle uploaded files.

defmodule MyApp.User do
  use MyApp.Web, :model
  use Arc.Ecto.Model

  schema "users" do
    field :avatar, MyApp.Avatar.Type
    field :username, :string
    field :email, :string

    timestamps
  end

  @required_fields ~w()
  @optional_fields ~w(username email)

  @required_file_fields ~w()
  @optional_file_fields ~w(avatar)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_attachments(params, @required_file_fields, @optional_file_fields)
  end
end

Updating Your Controller

Now for our controller. No need for changes here! We can save our attachments like we usually do in our controller.

web/controllers/user_controller.ex

#...
def create(conn, %{"user" => user_params}) do
  changeset = User.changeset(%User{}, user_params)

  case Repo.insert(changeset) do
    {:ok, _user} ->
      conn
      |> put_flash(:info, "User created successfully.")
      |> redirect(to: user_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end
#...

Updating Your User Form

We will need to modify our user form to support multipart uploads by adding [multipart: true] to the form_for function at the top of our user/form.html.eex file. We will also be replacing text_input with file_input for our :avatar field.

web/templates/user/form.html.eex

<%= form_for @changeset, @action, [multipart: true], fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :avatar, class: "control-label" %>
    <%= file_input f, :avatar, class: "form-control" %>
    <%= error_tag f, :avatar %>
  </div>

  <div class="form-group">
    <%= label f, :username, class: "control-label" %>
    <%= text_input f, :username, class: "form-control" %>
    <%= error_tag f, :username %>
  </div>

  <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

Obtaining the URLs for an Uploaded Image

This is one of the primary reasons why I chose to use Arc. Arc provides us with URL helpers for obtaining serialized URLs to our images. These URLs link directly to our locally-stored images and even include timestamps for cache-busting purposes. This is all provided by Arc and are called on our Avatar uploader directly.

Example from the Arc_Ecto Source Repo:

user = Repo.get(User, 1)

# To receive a single rendition:
MyApp.Avatar.url({user.avatar, user}, :thumb)
  #=> "https://bucket.s3.amazonaws.com/uploads/avatars/1/thumb.png?v=63601457477"

# To receive all renditions:
MyApp.Avatar.urls({user.avatar, user})
  #=> %{original: "https://.../original.png?v=1234", thumb: "https://.../thumb.png?v=1234"}

# To receive a signed url:
MyApp.Avatar.url({user.avatar, user}, signed: true)
MyApp.Avatar.url({user.avatar, user}, :thumb, signed: true)

Now, let’s use these URL helpers to display our images in our index and show templates.

web/templates/user/index.html.eex

<!-- ... -->
<%= for user <- @users do %>
    <tr>
      <td><img src="<%= MyApp.Avatar.url({user.avatar, user}) %>"/></td>
      <td><%= user.username %></td>
      <td><%= user.email %></td>

      <td class="text-right">
        <%= link "Show", to: user_path(@conn, :show, user), class: "btn btn-default btn-xs" %>
        <%= link "Edit", to: user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %>
        <%= link "Delete", to: user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
      </td>
    </tr>
<% end %>
<!-- ... -->

web/templates/user/show.html.eex

<!-- ... -->
<li>
  <strong>Avatar:</strong>
  <img src="<%= MyApp.Avatar.url({@user.avatar, @user}) %>"/>
</li>
<!-- ... -->

However, when we visit these pages, our images are still not being served up! That is because we need to tell Phoenix to serve static assets from our newly created uploads/ directory. Add the following line to your lib/my_app/endpoint.ex file.

lib/my_app/endpoint.ex

plug Plug.Static,
  at: "/uploads", from: Path.expand('./uploads'), gzip: false

Now, restart your Phoenix server and you should be able to view any images that you upload via the user creation form at http://localhost:4000/users/new!

In Summary

We created ourselves a brand new Phoenix application, generated a User and then leveraged Arc to handle avatar uploading and association. I would also like to take the time to connect S3 and do some image processing but I will talk about that on a late date. Granted, things are a bit more laborious to set up in Phoenix than in Rails, but I think that is only a hallmark of Rails’ maturity. All this code took me about three hours of Googling and debugging to get up and running, and I consider that a win! Now, download Phoenix and give it a try!