Elixir / Phoenix — Uploading images locally (With ARC)

Hello. It’s been a while since my last post.
Did you have a nice summer? I sure did. Me and my better half went on a long motorcycle vacation through the west side of Norway. The nature is really amazing, Highly recommended.

Anyways, back to coding, right?
I figured I’ll show you a way to upload images to your Phoenix application. We will start with uploading to a local folder, and look into uploading to S3/Google in another post.

As always, we’ll start with a new project. I will call mine “Imageer”:

~/$ mix phoenix.new imageer
* creating imageer/config/config.exs
* creating imageer/config/dev.exs
* creating imageer/config/prod.exs
................................
Fetch and install dependencies? [Yn] y

After a stupid long wait, (at least for me. Does fetching dependencies for a new project take several minutes for you?) we have ourself a fresh new application.

We’ll cd into our app and create our new database:

~/$ cd imageer/
~/imageer$ mix ecto.create
==> connection
Compiling 1 file (.ex)
Generated connection app
==> fs (compile)
............................
The database for Imageer.Repo has been created

Sweet. Let’s start with the C and R of a CRUD app. we’ll fire up the model generator:

~/imageer$ mix phoenix.gen.model Image images image:string
.....
~imageer$ mix ecto.migrate
.....
09:49:36.323 [info] == Migrated in 0.0s

Perfect. Next up, we’ll create our controller, routes, views, templates

# web/controllers/image_controller.exdefmodule Imageer.ImageController do
use Imageer.Web, :controller
alias Imageer.Image
alias Imageer.Repo
def index(conn, _) do
render(conn, “index.html”)
end
def new(conn, _) do
changeset = Image.changeset(%Image{})
render(conn, “new.html”, changeset: changeset)
end
def create(conn, %{"image" => image_params}) do
IO.inspect image_params
end
end

This is pretty basic, and we’ve covered this in previous blog posts. Notice that in the create function, we’ll inspect the content of our image_params, which we get once we submit a form from the new action.

Standard view for our images:

# web/view/image_view.exdefmodule Imageer.ImageView do
use Imageer.Web, :view
end

And we’ll just set up a nice resource in our router:

# web/router.ex
......
scope “/”, Imageer do
pipe_through :browser # Use the default browser stack
get “/”, PageController, :index
resources “/images”, ImageController
end

We’ll create a new image folder in our templates folder for our view templates, and two new files. index.html.eex and new.html.eex. If we go to http://localhost:4000/images, we can finally see our empty views.

For the sake of making things easy, let’s populate our index view with a link to uploading new images:

# web/templates/image/index.html.eex<h4><%= link “Upload new image”, to: image_path(@conn, :new) %></h4>

And we’ll start implementing our image form:

# web/templates/image/new.html.eex<h2>Upload a new image</h2><%= form_for @changeset, image_path(@conn, :create), 
[multipart: true], fn f -> %>
<div class=”form-group”>
<label>Image</label>
<%= file_input f, :image %>
<%= error_tag f, :image %>
</div>
<div class=”form-group”>
<%= submit “Submit”, class: “btn btn-default” %>
</div>
<% end %>

Sweet. We got ourself a form. For the sake of it, let’s try and upload a new image. The app is going to blow up, however, if we look in server console, we’ll see what our image_params consist of:

%{“image” => %Plug.Upload{content_type: “image/png”, filename: “image.png”, 
path: “/var/folders/jx/r8fqzstd19v3_jf62256gvt00000gn/T//plug-1474/multipart-190927–216841–2”}}

Cool. So we can see what type the file we’re posting, the filename, and a temporary file path.

Now, let’s do so we actually save the file. We need to add a bit more to our create action:

#web/controllers/image_controller.ex
......
def create(conn, %{“image” => image_params}) do
IO.inspect image_params
changeset = Image.changeset(%Image{}, image_params)
case Repo.insert(changeset) do
{:ok, image} ->
conn
|> put_flash(:info, “Image was added”)
|> redirect(to: image_path(conn, :index))
{:error, changeset} ->
conn
|> put_flash(:error, “Something went wrong”)
|> render(“new.html”, changeset: changeset)
end
end

And we’ll finally bring in Arc. We’ll add it to our dependencies function. In our mix.exs file, in deps:

# mix.exs
........
defp deps do
[{:phoenix, “~> 1.2.1”},
{:phoenix_pubsub, “~> 1.0”},
{:phoenix_ecto, “~> 3.0”},
{:postgrex, “>= 0.0.0”},
{:phoenix_html, “~> 2.6”},
{:phoenix_live_reload, “~> 1.0”, only: :dev},
{:gettext, “~> 0.11”},
{:cowboy, “~> 1.0”},
{:arc, “~> 0.5.2”}, #add this
{:arc_ecto, “~> 0.4.4”}] #and this
end
..........

We also need to add :arc_ecto to our application function, so that it gets run when we start our app:

# mix.exs
......
def application do
[mod: {ImageUpload, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy,
:logger, :gettext, :phoenix_ecto, :postgrex,
:arc_ecto]]
end
......

We’ll then run mix deps.get, and restart our application. Now, we need to generate a new uploader with arc:

imageer$ mix arc.g image_uploader
* creating web/uploaders/image_uploader.ex

Cool. So the generator created a new file for us. Let’s do some minor changes to it. first, according to the Arc documentation, we need to import another using macro, which will provide a set of functions to ease integration with Arc and Ecto. We also define local storage:

# web/uploaders/image_uploader.exdefmodule Imageer.ImageUploader do
use Arc.Definition
use Arc.Ecto.Definition
def __storage, do: Arc.Storage.Local
.......

Now, in our schema (You’ll find it in our model file), we’ll add another using macro, and we’ll specify the type of the column in our schema with Imageer.ImageUploader.Type, and use Arc’s own cast function, cast_attachments/3:

# web/models/image.exdefmodule Imageer.Image do
use Imageer.Web, :model
use Arc.Ecto.Schema
schema “images” do
field :image, Imageer.ImageUploader.Type
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:image])
|> cast_attachments(params, [:image])
end
end

Cool. If we now try and upload an image, we’ll get a nice flash:

Now, let’s open up iEX and verify that it actually got stored in our database:

~/imageer$ iex -S mix
iex(1)> alias Imageer.Image
iex(2)> alias Imageer.Repo
iex(3)> Repo.all(Image)
[debug] QUERY OK source=”images” db=1.0ms decode=3.4ms
SELECT i0.”id”, i0.”image”, i0.”inserted_at”, i0.”updated_at” FROM “images” AS i0 []
[%Imageer.Image{__meta__: #Ecto.Schema.Metadata<:loaded, “images”>, id: 1,image: %{file_name: “image.jpg”,
updated_at: #Ecto.DateTime<2016–09–18 10:01:34>},
inserted_at: #Ecto.DateTime<2016–09–18 10:01:34>,
updated_at: #Ecto.DateTime<2016–09–18 10:01:34>}]

Would you look at that! The image file_name is stored in our database. If you have a look in imageer/uploads, you’ll find our newly uploaded file. Now, let’s figure out how to actually display the image.

First of all, we need to gather all images from the repo, and make them available in our index view:

# web/controllers/image_controller.ex
......
def index(conn, _) do
images = Repo.all(Image)
render(conn, “index.html”, images: images)
end
......

In our index template, we’ll iterate over all images made available for us. We’ll use Arc’s image_uploader function:

# web/templates/image/index.html.eex
<h4><%= link "Upload new image", to: image_path(@conn, :new) %></h4>
<%= for image <- @images do %>
<img src="<%= Imageer.ImageUploader.url({image.image, image})%>"><br>
<% end %>

What? Shouldn’t that show us pictures? Well. Phoenix isn’t exactly serving our uploads folder by default, so we need to explicitly tell it to. In endpoint.ex, add a new Plug.Static, underneath the one that’s already there:

# lib/imageer/endpoint.ex
......
plug Plug.Static, #Leave this as it is.
at: “/”, from: :image_upload, gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
plug Plug.Static,
at: “/uploads”, from: Path.expand(‘./uploads’), gzip: false
......

And refresh your application:

Tada! We’ve got cats!

Next time, we’ll expand this app, discussing how to upload files to cloud storages like AWS S3. You can find the next post here

That’s all for now, thanks for reading!

Until next time
Stephan Bakkelund Valois

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store