Psst. It would be super cool if you could try the new Phoenix Boilerplate

Try now →

Tutorial

Fileuploads to S3 with Waffle

waffles3fileupload

Waffle is the file upload library that is forked from Arc and works much in the same way. In this tutorial I will show you how to do file uploads with the Waffle library to Amazon S3.

I already have an app with a User and want to add the possibility to add an avatar to that user.

STEP 1 - Install the Waffle library

First, install the libraries in the mix file. Since I want to upload to Amazon S3, I also need to add some extra libraries:

defp deps do
  [
    {:waffle, "~> 1.1.0"},

    # If using S3:
    {:ex_aws, "~> 2.1.2"},
    {:ex_aws_s3, "~> 2.0"},
    {:hackney, "~> 1.9"},
    {:sweet_xml, "~> 0.6"}
  ]
end

Then install the dependencies with running the command:

mix deps.get

After that, I need to set up some configuration. Waffle comes with a lot of options but for this purpose, I will go with the Amazon S3 option.

config :waffle,
  storage: Waffle.Storage.S3, # or Waffle.Storage.Local
  bucket: System.get_env("AWS_BUCKET_NAME") # if using S3

# If using S3:
config :ex_aws,
  json_codec: Jason,
  access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
  secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
  region: System.get_env("AWS_REGION")

Create or edit an .env file, add your Amazon credentials:

export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=
export AWS_REGION=
export AWS_BUCKET_NAME=

STEP 2 - Add avatar to User

I need to add a the avatar file to the users table so I will start with adding the migration.

mix ecto.gen.migration add_avatar_to_users

Alter the users table and add the avatar field as a string.

  def change do
    alter table(:users) do
      add :avatar, :string
    end
  end

And then run the migration with:

mix eco.migrate

STEP 3 - Generate the Uploader

Waffle comes with a generator that can generate a module with the settings and configuration for a specific uploader. I will then need to reference that uploader in the User schema.

mix waffle.g avatar_uploader

That will give me a an uploader like:

defmodule Tutorial.AvatarUploader do
  use Waffle.Definition
  use Waffle.Ecto.Definition
    ...
end

Since the avatar is an image, I will uncomment the part that says:

  # Whitelist file extensions:
  def validate({file, _}) do
    ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
  end

There are also other settings like the file path on the bucket on S3, generating thumbnails and other transformations.

Next part is to add the uploader to the User module.

defmodule Tutorial.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  use Waffle.Ecto.Schema

  @derive {Inspect, except: [:password]}
  schema "users" do
    ...
    field :avatar, Tutorial.AvatarUploader.Type

    timestamps()
  end

  def avatar_changeset(user, attrs) do
    user
    |> cast(attrs, [])
    |> cast_attachments(attrs, [:avatar])
  end   
end

NOTE That I also add a specific avatar changeset.

STEP 4 - Add fileupload to the frontend

First, I need to add some functions to the Accounts context:

# lib/tutorial/accounts.ex
def update_user_avatar(%User{} = user, attrs) do
  user
  |> User.avatar_changeset(attrs)
  |> Repo.update()
end

def change_user_avatar(%User{} = user) do
  User.avatar_changeset(user, %{})
end

And I need to use these in the UserSettingsController

# user_settings_controller.ex
defmodule TutorialWeb.UserSettingsController do
  ...

  def update_avatar(conn, %{"user" => user_params}) do
    user = conn.assigns.current_user

    case Accounts.update_user_avatar(user, user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Avatar updated successfully.")
        |> redirect(to: Routes.user_settings_path(conn, :edit))

      {:error, changeset} ->
        render(conn, "edit.html", avatar_changeset: changeset)
    end
  end

  defp assign_email_and_password_changesets(conn, _opts) do
    user = conn.assigns.current_user

    conn
    ...
    |> assign(:avatar_changeset, Accounts.change_user_avatar(user))
    |> assign(:user, user)
  end
end

So, we are almost done. I just need to add a route for the form and add the html. First in the routes, add:

put "/users/settings/update_avatar", UserSettingsController, :update_avatar

Last piece of the puzzle, is to add the actual file upload form on the edit page.

<!-- lib/tutorial_web/templates/user_settings/edit.html.eex -->
<h3>Change Avatar</h3>

<%= if @user.avatar do %>
  <%= link @user.avatar.file_name, to: NinjaApp.AvatarUploader.url(@user.avatar, signed: true) %>
<% end %>

<%= form_for @avatar_changeset, Routes.user_settings_path(@conn, :update_avatar), [multipart: true], fn f -> %>
  <%= if @avatar_changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :avatar %>
    <%= file_input f, :avatar %>
    <%= error_tag f, :avatar %>
  </div>

  <div>
    <%= submit "Upload avatar", class: "btn btn-primary" %>
  </div>
<% end %>