File Uploads with Phoenix Live View, Waffle and Caddy

A short tutorial how to configure uploads with Phoenix Framework.

The problem

It's confusing because there are different system which work with different paths:

  • Browser: https://inhji.de/uploads/files/images/cat.jpg
    • This is the public path
  • Caddy: /var/www/hajur/uploads/files/images/cat.png
    • This is the physical path of the image on disk
  • Waffle Config: /var/www/hajur
    • This is the base path that all image files should be placed in
  • Image Uploader: uploads/files/images
    • This is the specific path for this kind of image

Configuration

Waffle

In our config.exs, we define the default path for uploads. This is only used for dev and test environments.

config :waffle,
storage: Waffle.Storage.Local,
storage_dir_prefix: "priv/waffle/public"

For production, we place a UPLOAD_DIR variable in our .env file which will then replace our storage_dir_prefix option:

upload_dir =
System.get_env("UPLOAD_DIR") ||
raise """
environment variable UPLOAD_DIR is missing.
It should look like: /var/www/hajur/files
"""
config :waffle,
storage_dir_prefix: upload_dir

Image Uploader

I'm dithering all images and also create a smaller version as well as a very small thumbnail. The final filename is created by hashing the filename of the uploaded file. The storage_dir is the thing that needs to match the caddy configuration.

defmodule Hajur.Content.ImageUploader do
use Waffle.Definition
use Waffle.Ecto.Definition
@versions [:original, :full, :thumb]
# Whitelist file extensions:
def validate({_file, _post}) do
true
end
def transform(:original, _) do
{:magick, "-auto-orient -colors 24 -dither FloydSteinberg -format png", :png}
end
def transform(:full, _) do
{:magick,
"-auto-orient -resize 1280x1024> -background #ffffff00 -gravity center -extent 1280x1024 -colors 24 -dither FloydSteinberg -format png",
:png}
end
def transform(:thumb, _) do
{:magick,
"-auto-orient -resize 250x250> -background #ffffff00 -gravity center -extent 250x250 -colors 24 -dither FloydSteinberg -format png",
:png}
end
# Override the persisted filenames:
def filename(version, {file, _post_image} = _scope) do
hash = :crypto.hash(:sha, file.file_name) |> Base.encode16()
"#{hash}-#{version}"
end
# Override the storage directory:
def storage_dir(_version, {_file, _post}) do
"uploads/files/images"
end
end

Caddy

I tried using rewrite without the wrapping handle block which did not work. In the end, I added the file_server directive. I have a feeling that that might have been the problem all along.

We are matching for requests which begin with /uploads and rewrite them to the actual directory where uploads are placed by the server. This will rewrite a url like:

https://inhji.de/uploads/files/images/somehash-full.png

to

/var/www/hajur/uploads/files/images/somehash-full.png
inhji.de {
@uploads path /uploads/*
handle @uploads {
file_server
rewrite * /var/www/hajur/{path}
}
reverse_proxy * localhost:9000
}