Packages

ActiveStorage-like file storage for Phoenix. All things file uploads for your Phoenix app.

Current section

Files

Jump to
phoenix_contrib_storage lib storage controller.ex
Raw

lib/storage/controller.ex

defmodule Storage.Controller do
@moduledoc """
Phoenix controller for serving Storage files.
Add this to your router:
get "/storage/:key", Storage.Controller, :serve
Or for more control:
scope "/storage" do
get "/:key", Storage.Controller, :serve
get "/:key/:filename", Storage.Controller, :serve_with_filename
end
"""
import Plug.Conn
import Phoenix.Controller
@doc """
Serves a file by its storage key.
"""
def serve(conn, %{"key" => key}) do
case Storage.Blob.find_by_key(key) do
nil ->
conn
|> put_status(:not_found)
|> text("File not found")
blob ->
serve_blob(conn, blob)
end
end
@doc """
Serves a file by its storage key with a specific filename in the URL.
This is useful for SEO and user-friendly URLs.
"""
def serve_with_filename(conn, %{"key" => key, "filename" => _filename}) do
# Filename is ignored for security, we use the blob's actual filename
serve(conn, %{"key" => key})
end
@doc """
Serves a blob with appropriate headers.
"""
def serve_blob(conn, %Storage.Blob{} = blob) do
case Storage.get_file(blob) do
{:ok, data} ->
conn
|> put_resp_content_type(blob.content_type)
|> put_resp_header("content-disposition", content_disposition(blob))
|> put_resp_header("content-length", to_string(blob.byte_size))
|> put_resp_header("cache-control", "public, max-age=31536000")
|> put_resp_header("etag", blob.checksum)
|> send_resp(200, data)
{:error, _reason} ->
conn
|> put_status(:not_found)
|> text("File not found")
end
end
@doc """
Serves a blob as an attachment (forces download).
"""
def download(conn, %{"key" => key}) do
case Storage.Blob.find_by_key(key) do
nil ->
conn
|> put_status(:not_found)
|> text("File not found")
blob ->
case Storage.get_file(blob) do
{:ok, data} ->
conn
|> put_resp_content_type(blob.content_type)
|> put_resp_header("content-disposition", "attachment; filename=\"#{blob.filename}\"")
|> put_resp_header("content-length", to_string(blob.byte_size))
|> send_resp(200, data)
{:error, _reason} ->
conn
|> put_status(:not_found)
|> text("File not found")
end
end
end
# Private functions
defp content_disposition(%Storage.Blob{} = blob) do
if inline?(blob) do
"inline; filename=\"#{blob.filename}\""
else
"attachment; filename=\"#{blob.filename}\""
end
end
defp inline?(%Storage.Blob{content_type: content_type}) do
content_type in [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"text/plain",
"application/pdf"
]
end
end