Phoenix Framework — Direct Uploading to Amazon S3.

Sergio Tapia
sergiotapia
Published in
3 min readAug 5, 2017

Today I want to teach you guys how to upload files to Amazon S3 for your Phoenix application.

We’ll set up uploads to go like this:

  • User tells the backend, “I want to upload picture.jpeg!”
  • Backend tells the user, “Alright you have my permission but ONLY for that filename with that extension. Here’s a token, enjoy.”
  • User uses that signed token and pushes the file to our S3 bucket.

The benefit of using this approach is that your user sees a snappier upload. The file doesn’t need to be passed through our backend. Your resources don’t get bogged down when a lot of people want to upload things. It’s a win-win.

Route, controller, and json view.

Let’s create a route people can hit to receive a signed token.

post "/upload-signature", UploadSignatureController, :create

Users will need to pass in two params:

  • filename
  • mimetype

Our controller action looks like this:

def create(conn, %{"filename" => filename, "mimetype" => mimetype}) do
conn
|> put_status(:created)
|> render("create.json", signature: sign(filename, mimetype))
end

In our upload_signature_view.ex file, we need to create our create.json render. This is just a simple return.

def render("create.json", %{signature: signature}) do
signature
end

Signing filename and mimetype for secure uploads.

At this point we’re ready to dive into the meat of the signature generation: the sign function that’s being invoked in the controller.

These functions can be placed in our UploadSignatureController file.

defp sign(filename, mimetype) do
policy = policy(filename, mimetype)

%{
key: filename,
'Content-Type': mimetype,
acl: "public-read",
success_action_status: "201",
action: "https://s3.amazonaws.com/#{System.get_env("S3_BUCKET_NAME")}",
'AWSAccessKeyId': System.get_env("AWS_ACCESS_KEY_ID"),
policy: policy,
signature: hmac_sha1(System.get_env("AWS_SECRET_ACCESS_KEY"), policy)
}
end

defp now_plus(minutes) do
import Timex
now
|> shift(minutes: minutes)
|> format!("{ISO:Extended:Z}")
end

defp hmac_sha1(secret, msg) do
:crypto.hmac(:sha, secret, msg)
|> Base.encode64
end

defp policy(key, mimetype, expiration_window \\ 60) do
%{
# This policy is valid for an hour by default.
expiration: now_plus(expiration_window),
conditions: [
# You can only upload to the bucket we specify.
%{bucket: System.get_env("S3_BUCKET_NAME")},
# The uploaded file must be publicly readable.
%{acl: "public-read"},
# You have to upload the mime type you said you would upload.
["starts-with", "$Content-Type", mimetype],
# You have to upload the file name you said you would upload.
["starts-with", "$key", key],
# When things work out ok, AWS should send a 201 response.
%{success_action_status: "201"}
]
}
# Let's make this into JSON.
|> Poison.encode!
# We also need to base64 encode it.
|> Base.encode64
end

Looks crazy, but take a minute to digest what’s happening at each line, there’s some helpful comments.

Now you can use the returned token to upload your file to your S3 bucket. You can use something like Dropzone, jQuery Uploader, or whatever you need.

Many thanks to ElixirSips for this terrific article, I used it as inspiration to simplify it a little bit and apply it to my project.

Enjoy fast uploads and an even faster Phoenix API!

--

--