How to upload a file in Elixir with Waffle
The ability to upload files is a key requirement for many todays web and mobile applications. In this tutorial, we will look at how we can accomplish file uploads to local storage and S3 server in Phoenix with the help of Waffle library.
Background
Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3 and ImageMagick. This library is forked from Arc and works much in the same way. To illustrate how to upload files we will start with a simple demo application, which let us upload pictures to local storage and viewing pictures on page. We will also use Waffle.Ecto library to integrate Waffle with Ecto and save file data in the database. At the end, we will check how to upload files to S3 server.
Demo app
It's time to create our app:
mix phx.new file_uploader
Let's take a few shortcuts and create everything that we need with one command in the project folder:
mix phx.gen.html Gallery Photo photos picture:string
The above command mix phx.gen.html generates controller, views, and context for an HTML resource. We will have a table named "photos" in the database with id, picture (for storing data about the file) and timestamps columns.
We also need to provide an appropriate route for our new controller in router.ex
so update the existing scope just like below:
scope "/", FileUploaderWeb do
pipe_through :browser
get "/", PageController, :index
resources "/photos", PhotoController
end
Now provide your database credentials in your config and run
mix ecto.create && mix ecto.migrate
to set up a database. You can run mix phx.server
to start an application and go to http://localhost:4000/photos just to take a look at what we have created at the moment. In the next chapter, we are going to add Waffle
and Waffle.Ecto
to our project.
Setup Waffle
Add the latest stable release of Waffle and Waffle.Ecto to your mix.exs
file:
defp deps do
[
...
{:waffle, "~> 1.1.5"},
{:waffle_ecto, "~> 0.0.11"}
...
]
end
and after that:
mix deps.get
Next, we should generate a definition module with mix waffle.g :
mix waffle.g file_image
With above command we generated file in lib/[APP_NAME]_web/uploaders/file_image.ex
. For now, everything in this file is commented out but we will provide some changes there in next section.
Local Storage
To store files locally in our project file system we will start with the setup of the storage provider in dev
config:
config :waffle, storage: Waffle.Storage.Local
Now let's provide some changes to lib/file_uploader_web/uploaders/file_image.ex
. Add use Waffle.Ecto.Definition
at the top of the file which includes ecto support. We can also provide some file validation to our application. Let's assume we want uploaded files to have the following extensions: .jpg .jpeg .gif .png
so let's add validation function to file_image.ex
:
defmodule FileUploader.FileImage do
use Waffle.Definition
use Waffle.Ecto.Definition
# Whitelist file extensions:
def validate({file, _}) do
file_extension = file.file_name |> Path.extname() |> String.downcase()
case Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do
true -> :ok
false -> {:error, "invalid file type"}
end
end
end
Next part is to add the uploader to the Photo
module in lib/file_uploader/gallery/photo.ex
:
defmodule FileUploader.Gallery.Photo do
use Ecto.Schema
use Waffle.Ecto.Schema
import Ecto.Changeset
schema "photos" do
field :picture, FileUploader.FileImage.Type
timestamps()
end
@doc false
def changeset(photo, attrs) do
photo
|> cast_attachments(attrs, [:picture])
|> validate_required([:picture])
end
end
By default, our files are saved in /uploads
folder. So now we need to configure the endpoint.ex
to indicate that we will be serving static resources from the /uploads
directory. Go to the lib/file_uploader_web/endpoint.ex
and add a second static plug:
plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false
We managed to configure everything on the backend side. Now we need to add some changes to the templates we generated earlier. We will start from lib/file_uploader_web/templates/photo/index.html.eex
. We should change: <td><%= photo.picture %></td>
to:
<td>
<%= img_tag FileUploader.FileImage.url({photo.picture, photo}, signed: true)
%>
</td>
The same change we need to provide in our show template in lib/file_uploader_web/templates/photo/show.html.eex
. Let's change
<%= @photo.picture %>
to:
<%= img_tag FileUploader.FileImage.url({@photo.picture, @photo}, signed: true) %>
This change will allow us to display the photos correctly. We use the url/4 function which is injected into our FileImage
module through use Waffle.Definition
macro.
Okay, so now we can display our pictures, but we also need to make some changes when it comes to adding new images. Let's go to our form.html.eex
template in lib/file_uploader_web/templates/photo/form.html.eex
and change text input
<%= text_input f, :picture %>
to file input:
<%= file_input f, :picture %>
We also need to change our form into a multipart form. The form_for/4
function accepts a keyword list of options where we can specify this:
<%= form_for @changeset, @action, [multipart: true], fn f -> %>
Now let's check our changes and how what we've done works. You can run your local server and go to http://localhost:4000/photos, try to add a new photo and check if everything works fine.
Upload files to S3
If we want to upload files to Amazon S3 we need to add some extra libraries from ExAws to what we already have:
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
After that, it's time to set up some configurations. In this case, we are going to use Waffle.Storage.S3
:
config :waffle,
storage: Waffle.Storage.S3,
bucket: "your_bucket"
and we must add ex_aws configuration:
config :ex_aws,
json_codec: Jason
# any configurations provided by https://github.com/ex-aws/ex_aws
FAQ
What is Waffle in Elixir, and why use it for file uploads?
Waffle is an Elixir library for file uploads, offering straightforward integration with Amazon S3 and ImageMagick. It's used for efficiently handling file uploads in web applications.
How can I start a new Phoenix project for file uploading using Waffle?
Create a new Phoenix project using mix phx.new
and set up the necessary configurations for file uploading with the Waffle library.
How do you integrate Waffle with a Phoenix application?
Add Waffle and Waffle.Ecto to your mix.exs
dependencies, configure your application for file uploads, and update your Phoenix project settings accordingly.
What are the steps to configure local storage for file uploads in Phoenix with Waffle?
Set up Waffle for local storage in your project's configuration and modify the necessary Elixir modules to handle file storage and validation.
How do you integrate Amazon S3 with Waffle for file uploads in Elixir?
Include additional dependencies for S3 in your mix.exs
file, and configure your application to use Waffle.Storage.S3, along with the necessary S3 bucket settings.
How can you validate file types and extensions in a Phoenix application using Waffle?
Implement file validation logic in your uploader module to ensure that only files with specified extensions are accepted.
What changes are needed in Phoenix templates to support file uploading with Waffle?
Modify form templates to include file input fields and update them to handle multipart data for file uploads.