File uploads are a common requirement of many applications. Today we’ll look at how we can accomplish file uploads in Phoenix with the help of Arc… a flexible file upload and attachment library for Elixir.
What we’ll build
To demonstrate file uploads we’re going to build a very simple photo gallery, at the end of this post we’ll be able to upload and display images, and our application will look like:
If you’d rather grab the completed source code directly rather than follow along, it’s available on GitHub, otherwise let’s get started!
Creating the app
We need to set-up a bit of plumbing as a starting point, so let’s grab a bare-bones skeleton of the application from GitHub.
Clone the Repo
We’ll start by cloning the repo.
Let’s create a branch for today’s work:
And then let’s get our dependencies and database set-up.
Now we’ll run
ecto.setup to create our database. Note: before running
ecto.setup you may need to update the username / password database settings in
dev.config to match your local postgres settings, i.e.
/config/dev.config …line 69
With all that out of the way… let’s see where we are starting from.
If we navigate to http://localhost:4000/ we’ll see our application.
Throwing up some scaffolding for our gallery pages
We’ll use some scaffolding to quickly put together the pages for our gallery functionality. So run the below:
We’re creating a
Gallery context and a
photos table in the database. We won’t be storing our images in the database, but we will keep track of some general information about them in the database. Specifically we want a unique id associated with each photo and we also want a photo to be linked to the user who uploaded the photo.
Therefore we need to make a few changes to the migration file.
We’ve added a reference to our
users table; indicating on user deletion we should also clean any associated
photo records (via the
:delete_all option). We’ve also created an index on
Let’s run the migration.
We now need to update our router to include our photo routes.
/lib/images_web/router.ex …line 30
We only want authenticated users to access the photo functionality, thus we are utilizing the protected scope in our router file. It also doesn’t make much sense for a photo to be edited (i.e. replaced), so we’re excluding edit and update from the routing. We’ll be removing the update / edit functionality from the controller and context later on.
Now we’ll add a navigation link to the main Photos page in our layout.
/lib/images_web/templates/layout/app.html.eex …line 13
This would be a good time to fire up the application and create a user. Since we’re going to restrict our functionality to authenticated users we’ll need to sign up a user for testing out our code… and it would also be good to do a quick spot check to make sure the changes we’ve made so far haven’t broken anything.
Note we’ll need to grab the registration confirmation link from our console:
Once we paste the confirmation link into our browser, we can log in.
If we follow the
Photos link we’ll see the default
index page created by our scaffolding.
We’ll be changing these pages a fair bit; now that we’ve confirmed our application is running we can look at how to integrate
Adding and configuring Arc
If on a Mac and using Homebrew this is a snap!
If not on a Mac I suggest checking out the ImageMagick download page or using a package manager compatible with your OS. For Windows I like chocolatey.
Arc. We need two new dependencies in
arc_ecto. arc is the primary dependency and arc_ecto provides the database tie in with arc.
Let’s update our dependencies.
We’ll see that
arc-ecto along with a number of other packages have been added.
That’s it for installation, let’s configure things up!
Configuration is pretty straight forward. We need to add an
Arc specific configuration entry in
config.exs. For now we’ll be storing images locally, so the initial configuration is very simple.
/config/config.exs …line 39
We also need to add a second static plug in
endpoint.ex to indicate that we will be serving static resources from the
/uploads directory. This is the directory we will be saving and serving our uploaded files from.
/lib/photo_gallery_web/endpoint.ex …line 12
We also likely want to add the
./uploads folder to our
.gitignore file as we don’t want to be checking our uploads into source control.
And that’s it for basic installation and configuration, let’s get into using
The first step in getting started with
Arc is to generate an uploader module. This module is what determines the basic behaviour we want from
Arc when an upload occurs.
The uploader module can be generated via a
The generated file contains pretty good commenting so it should be pretty obvious what the various components of the file do, feel free to have a read through the generated comments.
We’ll replace the contents of the file with the below.
Pretty self explanatory.
We start off by defining a white-list of allowed file types. We then indicate we’ll be uploading both the original image and a thumbnail. After this we have a few functions; one to validate against our whitelist; another to perform the thumbnail transform; and then two functions which override the default directory and file name of our images. Note in our
filename function we are making use of a
uuid value. This references the
uuid column we added in the database. In this way we will have an easy way to associate an uploaded file with a database record as they will both have the same
We now need to update our
photo schema to use Arc.
We’ve added a
use statement for
Arc.Ecto.Changeset and have updated the type of the
photo field to be of type
PhotoGallery.Photo (i.e. the uploader module we just created).
In the changeset, we are generating a
uuid value if one does not already exist. We then call
cast_attachments on the photo, which is what handles the upload via
Updating the context and adding a Bodyguard policy
I know we’re vomiting out a lot of code here without seeing anything in action… we’re getting close thou, so hang on!
Before updating our context we want to create a simple
Bodyguard policy around image deletion. We want only the user who uploaded an image to be able to delete the image. So let’s create the policy.
Pretty simple, we’re just checking that the
user ids match.
Now onto the
gallery context changes, we need to do the following:
- Update the delete function to delete the stored image in addition to the database record.
- Make use of the policy we created when it comes to image deletion.
- Update the create and delete functions to include the current user id.
- Remove the edit function.
So given the above, our context code becomes:
Nothing complicated, we’ve added a bunch of
alias statements, and a
defdelegate statement that delegates to our policy file.
Other than that, we’ve updated the function signatures to include the current user, and updated the delete code to include a call to our
Bodyguard.permit policy function. In the delete function we also added a call to
Photo.delete so the actual image gets deleted as well as the database record. Note we’ve also added a
put_assoc call in the
create_photo function so that our user gets associated with the photo record when it is inserted in the database.
Now we need to make some controller updates to accomodate the above context changes.
With our controller code, we can remove the
update actions as we are not making use of them. Other changes we need are:
- Add an action_fallback to handle unauthorized deletes.
conn.assigns.current_userin the calls to our context functions in the
- Redirect to the
indexaction instead of the
showaction when an image is created.
- Use a
withstatement in the
deleteaction so that our fallback controller is triggered when an invalid delete occurs.
The above results in the following code:
The final changes we need to make before trying things out is to update our template files.
The first thing we can do is remove the
edit template since we are no longer using it.
Next let’s update
form.html.eex to allow us to upload photos:
Note that we needed to add
multipart: true to allow for files to be processed. Other than that, everything is straight forward, we have a single
file_input control for selecting a file and then a
Next let’s update
Super simple, we’re just iterating thru any photo records and displaying each photo. We’re enclosing each individual photo in a link tag so that selecting the image navigates to the
show template. Note that the
PhotoGallery.Photo.url function is an
Arc specific function for retrieving the
url of an upload. We’re taking the result of this function and piping it into
img_tag in order to display each photo.
Finally lets update the
show template. All we are going to do is display the
uuid of the photo, the thumbnail and the original image. We also include a
delete button if the user has authorization to delete the image (i.e. if they are the one who uploaded the image).
And that’s it for our templates, we’re finally ready to try things out!
We can now upload photos:
After the photo is uploaded, we see it is present in the directory we specified in our uploader module. Both the original and a thumbnail version have been uploaded.
We also have a record in the database:
We can view the images on our index page:
Selecting a photo takes us to the details for that photo where we can also delete the photo if we have permission to do so.
So that pretty much wraps things up!
One final thing
One final thing before we stop for the day however. Currently if we click the upload button without selecting a file, we get a nasty error.
To remedy this we just need another
create function in the
NOTE: this second function needs to be placed after the original create method otherwise it will always be what gets matched on and we won’t be able to upload anything!
/lib/images_web/controllers/image_controller.ex …line 31
This second create method will match on any value for
params and thus we can check if the map passed into
params contains the
photo key. If not we know a photo was not selected so can indicate that in the error message. Otherwise we just spit out a generic message.
With that we get a much better result when clicking
Upload with no file selected.
A quick word about tests
I haven’t been sure how to handle testing for the purposes of this post. Testing is obviously very important but at the same time can require a fair bit of explanation which can add significantly to the length of a post and distract from the core subject.
Initially I was going to completely ignore testing, and indeed if you run
mix test you’ll see a number of tests will fail with the updates we’ve made to the code base.
This felt a little janky… so as a compromise I’ve updated the existing tests in the master branch of the GitHub repo so they pass; but there is no discussion of the tests and I haven’t added any new tests. Needless to say, with a real application this is not the strategy you would want to follow! I might post an addendum at one point to discuss the tests as I found it a little tricky getting things set up with
Arc… but for now we won’t be discussing testing.
I try to keep these posts a reasonable length, I think I failed this time out, but I just couldn’t see a good way of splitting things up 🙁, hopefully it wasn’t too painful!
In any case we now have a decent idea about
Arc’s general functionality. Next time we’ll look at how we can upload multiple photos at a time and also look at how to upload to AWS S3 instead of a local directory.
Thanks for reading and I hope you enjoyed the post!