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.

Table of contents

    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.

    Mateusz Tatarski
    Mateusz Tatarski Elixir Developer

    Read more
    on #curiosum blog