Uploading and serving files from Phoenix and fly volumes

Learn how to upload and serve files from phoenix using fly volumes

Published on · 4 min read · 1 view · 1 reading right now

FLYIO
PHOENIX
ELIXIR

Introduction

I have been working on a personal project where I needed to store and serve files. The files are meant to be stored temporarily so I didn't want to over-engineer and use S3 or any other blob storage.

My project is hosted on fly.io, so I started looking for storage solutions that fly.io provides. Luckily, fly.io has volumes.

A volume is a slice of disk on your fly machine. Volumes are resilient to machine restarts(if you store files directly on your machine, they will be wiped out on machine restart or on new deployment).

Volumes prefectly fits my need. Fly.io also provides free 3GB storage(at the time this blog was written) which is more than enough.

Configuring file upload directory

My project is a Phoenix(with LiveView) application. Phoenix provides functions to handle uploads. Let's see how to upload and server file using fly volume.

Following the official guide, I add a volume to my fly machine.

I set the mount point to /data, so we need to configure our file uploader to save files to this directory.

A basic file upload handler in phoenix looks like this:

storage_directory = "/data" # <- we will configure this for different enviroments later

uploaded_files =
  consume_uploaded_entries(socket, :files, fn %{path: path}, entry ->
    # TIP: make sure to sanitize filename before saving them
    %{client_name: file_name, client_type: file_type} = entry

    dest = Path.join([storage_directory, file_name])
    File.cp!(path, dest)

    {:ok, ~p"/uploads/#{file_name}"}
  end)

# save the list of uploaded files in db or somewhere else.

Now on upload, the files will be saved inside /data directory(which is the mount point to our volume storage).

Serving files

Now that the files are being saved to /data directory, we need to serve it as well.

I want the file to be accessible through /uploads endpoint. For example, for a file stored at /data/video.mp4, I should be able to access it through /uploads/video.mp4 endpoint.

Fortunately, that's pretty easy to configure with Plug.Static plug. Plug.Static is used for serving static assets(like images, videos or any files).

In the endpoint.ex file, add:

endpoint.ex
defmodule MyApp.Endpoint do
  # other stuff

  # Add this line
  plug Plug.Static,
    at: "/uploads", # <- from the /uploads endpoint
    from: "/data", # <- serves file from this directory
    gzip: Mix.env() == :prod

  #other stuff
end

This tells Phoenix that any link to /uploads endpoint will result in serving files from /data directory.

That's it! You can now upload files to /data directory and access them at /uploads endpoint.

Environment based config

The current setup has hardcoded values as to where to upload files and where to serve them from. Local environment won't have /data directory and you generally want to save files to priv/ directory of your application(or some other place) during development.

The most straight forward way is to store the upload directory path inside your config file. We can then use Application.compile_env and Application.fetch_env to read this config.

For local development, I defined this in dev.exs file:

# File upload config
config :my_app,
  upload_dir: Path.join(Path.expand("."), "priv/uploads")

and for prod, inside prod.exs:

# File upload config
config :my_app,
  upload_dir: "/data"

Now use this config during upload and inside our Plug.Static plug.

During upload:

storage_directory = Application.fetch_env!(:my_app, :upload_dir) # <- The change

uploaded_files =
  consume_uploaded_entries(socket, :files, fn %{path: path}, entry ->
    # TIP: make sure to sanitize filename before uploading them
    %{client_name: file_name, client_type: file_type} = entry

    dest = Path.join([storage_directory, file_name])
    File.cp!(path, dest)

    {:ok, ~p"/uploads/#{file_name}"}
  end)

# save the list of uploaded files in db or somewhere else.

I used fetch_env! to get the :upload_dir value defined in the config.

During serve:

plug Plug.Static,
  at: "/uploads",
  from: Application.compile_env(:my_app, :upload_dir), # <- The change
  gzip: Mix.env() == :prod

I used compile_env here because the plug is defined at module level. We cannot use fetch_env here. compile_env is a compile time configuration, so during production deployment, the from value will be set to "/data".

Now we can test our application locally and run it on prod without any change.

That covers file uploading and serving using phoenix and fly volumes. With the given approach, we can store and server our files from anywhere in the disk.

0 likes

Other articles