Upload Files to S3 With Phoenix and Ex_Aws
A common feature many web applications need is the ability to take a file and upload it to Amazon S3. Luckily for us ex_aws along with Phoenix makes it very simple to add S3 uploads. I'll show you how to start uploading files with just a few lines of code.
Add the Dependency
Update your Mix configuration to include :ex_aws
as a dependency:
def deps do
[
...,
{:ex_aws, "~> 1.0"}
]
end
Set the Credentials
Update your config.exs
to include the AWS credentials for a role that can upload to S3.
config :ex_aws,
access_key_id: ["ACCESS_KEY", :instance_role]
secret_access_key: ["SECRET_ACCESS_KEY", :instance_role]
Creating the Feature
Create a controller and a template responsible for handling the file uploads.
defmodule FileUpload.UploadController do
use FileUpload.Web, :controller
def upload_form(conn, _params) do
render conn, "upload.html"
end
def upload(conn, params) do
# We'll fill this in later
render conn, "upload.html"
end
end
In the upload.html.eex, create a form for uploading a file. In my example, I'm only going to check for images. Make sure to include the multipart: :true
option required for uploading a file to your server.
<%= form_for @conn, upload_path(@conn, :upload), [as: "upload", multipart: :true], fn f -> %>
<%= file_input f, :file, accept: "image/*" %>
<button class="btn btn-primary" type="submit">Upload</button>
<% end %>
Now let's update our router.ex to have our upload routes:
scope "/", FileUpload do
# ...
get "/upload", UploadController, :upload_form
post "/upload", UploadController, :upload
end
If we hit our route and you've got a new Phoenix project, you should see something like this:
When our Phoenix receives a file upload, we receive a Plug.Upload
struct as in the params. Here's an example of what the params would look like for an image upload:
%{
"upload" => %{
"file" => %Plug.Upload{
content_type: "image/png",
filename: "some_image.png",
path: "/var/folders/nd/snztgzpd6t92bdkfm6lnm9l00000gn/T//plug-1484/multipart-890782-844137-2"
}
}
}
Phoenix will put the image in a temporary location for us so we can manipulate the image during the request. It is import to note that the image will only be on disk for the lifecycle of the request. Once the conn
is returned, the image will be deleted. Make sure to account for this and to do something with the file.
Back to our controller, let's update it to handle our file and finally upload it to S3. We are going to give a unique id to each file we upload. I will use UUID to generate my ids.
def upload(conn, %{"upload" => %{"file" => file}}) do
# Get the file's extension
file_extension = Path.extname(file.filename)
# Generate the UUID
file_uuid = UUID.uuid4(:hex)
# Set the S3 filename
s3_filename = "#{file_uuid}.#{file_extension}"
# The S3 bucket to upload to
s3_bucket = "somebucket"
# Load the file into memory
{:ok, file_binary} = File.read(file.path)
# Upload the file to S3
{:ok, _} =
ExAws.S3.put_object(s3_bucket, s3_filename, file_binary)
|> ExAws.request()
put_flash(:success, "File uploaded successfully!")
|> render("upload.html")
end
Now you're set to start uploading files. Try it out in your project and watch the files come in to your S3 instance.
Wrap Up
Adding S3 file uploads is very easy with :ex_aws
and Phoenix. Be sure to checkout the documentation on Plug.Upload and ex_aws. As always, any comments or questions are welcome.