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:

Submission form
The simplest form

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.

#phoenix   •   #elixir