View Source Direct Uploads with LiveView
This guide shows how to implement direct-to-cloud uploads using Phoenix LiveView, allowing users to upload files directly to your storage provider without going through your server.
Overview
Direct uploads improve performance and reduce server load by:
- Uploading files directly from the browser to cloud storage
- Reducing bandwidth usage on your servers
- Enabling progress tracking and cancellation
- Supporting large file uploads
Basic Setup
1. Configure Your Cloud Module
Enable direct uploads by setting the uploader
option:
config :my_app, MyApp.Cloud,
adapter: Buckets.Adapters.S3,
bucket: "my-uploads",
uploader: "S3", # or "GCS" for Google Cloud Storage
# ... other config
2. Basic LiveView Upload
defmodule MyAppWeb.UploadLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar,
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
max_file_size: 5_000_000,
external: &presign_upload/2
)}
end
@impl true
def render(assigns) do
~H"""
<form id="upload-form" phx-submit="save" phx-change="validate">
<.live_file_input upload={@uploads.avatar} />
<button type="submit">Upload</button>
</form>
<div :for={file <- @uploaded_files}>
Uploaded: <%= file.filename %>
</div>
"""
end
defp presign_upload(entry, socket) do
{:ok, config} = MyApp.Cloud.live_upload(entry)
{:ok, config, socket}
end
@impl true
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
@impl true
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{key: key}, entry ->
object = Buckets.Object.from_upload({entry, %{key: key}})
{:ok, object}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end
end
Multiple File Uploads
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:documents,
accept: ~w(.pdf .doc .docx),
max_entries: 10,
max_file_size: 10_000_000,
external: &presign_upload/2,
auto_upload: true
)}
end
Error Handling
def render(assigns) do
~H"""
<div :for={err <- upload_errors(@uploads.avatar)} class="error">
<%= error_to_string(err) %>
</div>
"""
end
defp error_to_string(:too_large), do: "File is too large"
defp error_to_string(:not_accepted), do: "File type not accepted"
defp error_to_string(:too_many_files), do: "Too many files"
defp error_to_string(:external_client_failure), do: "Upload failed"
defp presign_upload(entry, socket) do
case MyApp.Cloud.live_upload(entry) do
{:ok, config} ->
{:ok, config, socket}
{:error, reason} ->
{:error, "Failed to generate upload URL: #{inspect(reason)}", socket}
end
end
CORS Configuration
For direct uploads to work, configure CORS on your bucket:
S3 CORS
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://myapp.com</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
<MaxAgeSeconds>3000</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>
GCS CORS
[
{
"origin": ["https://myapp.com"],
"method": ["PUT", "POST"],
"responseHeader": ["*"],
"maxAgeSeconds": 3600
}
]