In this post, we'll add file upload capabilities to a Phoenix LiveView application and directly upload files to Amazon S3.
Without further ado, let's get started!
Setting Up Our Phoenix LiveView Project
Our example will upload behavior to an existing application with a “create puppy” feature. We're going to go through the Phoenix LiveView Uploads guide, making small adjustments as we go.
To begin, we need to set up our project. You can skip this step if you already have an existing Phoenix LiveView application. Otherwise, you can create a new project by running the following command in your terminal:
Change into the project directory:
And create a new Phoenix LiveView:
This will generate the necessary files and migrations for our live view and model.
Allowing Uploads
The very first thing we need to do is enable an upload on mount. Open the form component file at lib/puppies_web/live/puppy_live/form_component.ex
. You will see an update/2
function where we will enable file uploads. Add the following line to your callback:
With the update/2
callback ultimately looking like this:
This line enables file uploads for the :photo
attribute, accepting only JPEG, WEBP, and PNG file formats and only one upload at a time via max_entries: 1
. With auto_upload: true
, a photo begins uploading as soon as a user selects it in our form. You can modify these options based on your requirements by looking at the allow_upload
function docs.
If you’re not using a LiveComponent, you can add this line to your mount/2
callback.
Adding the Upload Markup
Next, we’re going to render the HTML elements in our form. This is where we will put our file input, a preview of our picture, and the upload percentage status. We'll also add drag-and-drop support for images.
To render the file upload form in our LiveView, we need to add the necessary HTML components. In the puppy form HTML, add all of these file upload goodies:
In this code, we render a file input with drag-and-drop support via the phx-drop-target
form binding and live_file_input component provided by Phoenix LiveView. We also display a live file preview of the uploaded photo with <.live_img_preview entry={entry} width="75" />
.
Live updates to the preview, progress, and errors will occur as the end-user interacts with the file input.
We render an image preview for each uploaded image stored in @uploads.photo.entries
. The @uploads
variable becomes available to us when we pipe into the allow_upload/3
function in our form component (remember, you can do this in the mount/2
callback for a LiveView if you aren’t using a LiveComponent).
for {_ref, msg} <- @uploads.photo.errors
will display any errors, like uploading the wrong file type, to the user.
Finally, notice this:
These appear in the form because after we upload the file to Amazon S3, we’re going to persist the url of the picture to the database.
Important: Your LiveView upload form must implement both phx-submit
and phx-change
callbacks, even if you’re not using them.
The LiveView upload feature set will not work without these callbacks and subsequent handle_event
callbacks. Read more about this.
Configuring Amazon S3 to Phoenix LiveView
Before we can test our file upload feature, we need to set up our Amazon S3 bucket and configuration. Setting up your Amazon S3 bucket and configuring access is outside the scope of this guide, but this video should help.
Consuming the File Upload on the Back-end
We’re going to crack open the Direct to S3 guide to bring all of this home.
In our allow_upload
function call, add “external” support so that we can get it on Amazon S3. It should look like this:
external: &presign_entry/2
sends the file to our presign_entry/2
function. This function dedicates work to a SimpleS3Upload
module that generates a “presigned_url”. We need to upload files directly to Amazon S3 via our front-end safely, without exposing our Amazon S3 credentials.
So, we generate a pre-authenticated short-term URL on the back-end with our Amazon credentials. We then send this URL back to the front-end, where it can safely upload our file to Amazon S3, already completely safely authenticated.
You can find the content of the SimpleS3Upload file here. It’s 170+ lines of code purely dedicated to creating a pre-signed url for pre-authenticated file uploading.
Inside the SimpleS3Upload
module, you’ll see references to the configuration you need to add.
In your app, you’ll need to specify your secret keys/credentials in your config/config.exs
.
Now let's pull this through to the front-end!
Uploading the File to S3 on the Front-end
We now need to upload the file from our front-end JavaScript in assets/js/app.js
:
This will upload each one of our files to Amazon S3 via AJAX.
Notice how we have an event listener for a “progress” event, always keeping the upload status available via entry.progress(percent)
. Remember, we’re displaying the progress percentage in our HTML. And if there’s an error in the AJAX response, entry.error()
has us covered. We render any file upload errors back to the user in the HTML, too.
The URL it’s uploading to is the pre-signed one that we generated on the back-end.
Saving the Amazon URL to the Database
In our form, there's a form submit binding that looks like phx_submit: "save"
, meaning we have a matching handle_event
in our LiveView/form_component
.
Make the following change:
We’re not adding the Amazon S3 photo URL to puppy_params
. The consume_uploaded_entries
function call is where this magic is happening.
SimpleS3Upload.entry_url(entry)
is the dynamically generated URL where our image lives in our S3 bucket.
Notably, observe my pattern matching in the add_photo_url_to_params
function. This guards us from overwriting the puppy with a blank URL if we don’t upload a file when updating the puppy’s attributes.
Here's the final result:
Wrapping Up
You now know how to upload from Phoenix LiveView directly to Amazon S3.
We began by enabling file uploads in our LiveView and adding the necessary components to our HTML template. We then created a pre-signed (pre-authenticated) URL to upload the file to with the SimpleS3Upload
module. Next, we actually performed the file upload via AJAX calls by adding the uploader to our application’s JavaScript. Finally, we consumed the files we uploaded, persisting the URL to the database.
Have fun experimenting with this feature and enhancing your application!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!