In one of our Rails 4 app, we decided to move file and image uploads to another microservice so that the load on server is reduced when a big file is uploaded. We decided to do this in Phoenix.

In Phoenix, we have ex_aws package which makes file uploads to S3 very simple just like Rails. So lets get started.

Add ex_aws

Update mix.exs to include following dependencies.

defp deps do
  [
    ...,
    {:ex_aws, "~> 1.0"},
    {:poison, "~> 2.0"},
    {:hackney, "~> 1.6"},
    {:sweet_xml, "~> 0.6"},
  ]
end

We need :sweet_xml so that we can parse XML response from S3.

Don't forget to update the applications list.

 def application do
   [
     mod: {ApplicationName, []},
     extra_applications: [..., :ex_aws, :hackney, :poison]
   ]
 end

Finally run mix deps.get to install above dependencies.

Set AWS credentials

I prefer to add all important keys specific to a project as environment variables. So we will create .env file in root directory of the project and add following code to it.

export AWS_ACCESS_KEY_ID=<......>
export AWS_SECRET_ACCESS_KEY=<.....>
export BUCKET_NAME=<.....>

Don't forget to run source .env on console to make sure all environment variables are loaded before you compile the project.

Next, update config.exs to include the AWS credentials.

# Configure :ex_aws
config :ex_aws,
  access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
  secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
  region: "us-east-1"

System.get_env() is used to read environment variables.

Write code

When Phoenix receives a file to upload, a Plug.Upload struct is received in the params. Here's an example of what the params would look like for an image upload:

%{
  "upload" => %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. The image is saved till the request is completed i.e. once the conn is returned, the image will be deleted.

Now we create upload service (UploadService) under /lib directory.

defmodule S3Upload.UploadService do
  def upload_to_s3(upload_params) do
    file = upload_params.path
    bucket_name = System.get_env("BUCKET_NAME")
    s3_path = "path/on/s3"
    file
      |> ExAws.S3.Upload.stream_file
      |> ExAws.S3.upload(bucket_name, s3_path)
      |> ExAws.request!

    s3_url = "http://#{bucket_name}.s3.amazonaws.com/#{s3_path}"
    %{
      s3_url: s3_url
    }
  end
end

Next we will add a controller UploadController -

defmodule S3UploadWeb.UploadController do
  use S3UploadWeb, :controller
  alias S3Upload.UploadService

  def upload(conn, %{"upload" => upload_params}) do
    resp = UploadService.upload_to_s3(upload_params)
    render(conn, "s3_response.json", resp: resp)
  end
end

And a view s3_response.ex -

defmodule S3UploadWeb.UploadView do
  use S3UploadWeb, :view

  def render("s3_response.json", %{resp: resp}) do
    resp
  end
end

Lastly, we add route in router.ex to accept files

scope "/", S3UploadWeb do
  ...
  post "/upload", UploadController, :upload
end

Now you're set to start uploading files.

Be sure to checkout the documentation on Plug.Upload and ex_aws.