Tutorial
Fileuploads to S3 with Waffle
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 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 an uploader like:
defmodule Tutorial.AvatarUploader do
use Waffle.Definition
use Waffle.Ecto.Definition
...
# Whitelist file extensions:
def validate({file, _}) do
~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
end
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 file upload 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 %>