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.
Terminal
Let’s create a branch for today’s work:
Terminal
And then let’s get our dependencies and database set-up.
Terminal
Terminal
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
Terminal
With all that out of the way… let’s see where we are starting from.
Terminal
If we navigate to http://localhost:4000/ we’ll see our application.
The application we’re starting with is essentially the default application you get when running mix phx.new
… just with the addition of Pow for authentication and Bodyguard for authorization.
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:
Terminal
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.
/priv/repo/migrations/timestamp
_create_photos.exs
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 user_id
.
Let’s run the migration.
Terminal
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.
Terminal
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 Arc
.
Adding and configuring Arc
We’ll be making use of the file transformation features of Arc
so the first thing to do is install ImageMagick as this is what Arc
uses to perform the transformations.
If on a Mac and using Homebrew this is a snap!
Terminal
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.
Now onto Arc
. We need two new dependencies in mix.exs
; arc
and arc_ecto
. arc is the primary dependency and arc_ecto provides the database tie in with arc.
/mix.exs
Let’s update our dependencies.
Terminal
We’ll see that arc
and arc-ecto
along with a number of other packages have been added.
That’s it for installation, let’s configure things up!
Configuration
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.
/.gitignore
And that’s it for basic installation and configuration, let’s get into using Arc
!
Using Arc
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 mix
task.
Terminal
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.
/lib/photo_gallery_web/uploaders/photo.ex
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 uuid
.
We now need to update our photo
schema to use Arc.
/lib/photo_gallery/gallery/photo.ex
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 Arc
.
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.
Terminal
/lib/
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:
/lib/photo_gallery/gallery/gallery.ex
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.
Controller updates
With our controller code, we can remove the edit
and update
actions as we are not making use of them. Other changes we need are:
- Add an action_fallback to handle unauthorized deletes.
- Includ
conn.assigns.current_user
in the calls to our context functions in thecreate
anddelete
actions. - Redirect to the
index
action instead of theshow
action when an image is created. - Use a
with
statement in thedelete
action so that our fallback controller is triggered when an invalid delete occurs.
The above results in the following code:
/lib/photo_gallery_web/controllers/photo_controller.ex
The final changes we need to make before trying things out is to update our template files.
Template updates
The first thing we can do is remove the edit
template since we are no longer using it.
Terminal
Next let’s update form.html.eex
to allow us to upload photos:
/lib/images_web/templates/image/form.html.eex
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 submit
button.
Next let’s update index.html.eex
.
/lib/images_web/templates/image/index.html.eex
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).
/lib/images_web/templates/image/show.html.eex
And that’s it for our templates, we’re finally ready to try things out!
Terminal
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 image_controller
.
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.
Summary
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!