Here’s a minimal but complete working example for you.
# Utility module to deal with S3 operations
defmodule MyAppWeb.S3 do
def presigned_put(opts) do
key = Keyword.fetch!(opts, :key)
max_file_size = opts[:max_file_size] || 10_000_000
expires_in = opts[:expires_in] || 7200
uri = "https://s3.us-east-005.backblazeb2.com/bucket-name/#{key}"
url =
:aws_signature.sign_v4_query_params(
"secret",
"secret2",
"us-east-005",
"S3",
:calendar.universal_time(),
"PUT",
uri,
ttl: expires_in,
uri_encode_path: false,
body_digest: "UNSIGNED-PAYLOAD"
)
{:ok, url}
end
end
# Phoenix live upload
defmodule MyAppWeb.UploadLive do
use MyAppWeb, :live_view
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar,
max_file_size: 50_000_000,
accept: ~w(.jpg .jpeg .png .gif .svg .webp),
max_entries: 1,
external: &presign_upload/2
)}
end
defp presign_upload(entry, socket) do
uploads = socket.assigns.uploads
key = "public/#{URI.encode(entry.client_name)}"
{:ok, presigned_url} = MyAppWeb.S3.presigned_put(key: key)
meta = %{uploader: "S3", key: key, url: presigned_url}
{:ok, meta, socket}
end
end
// assets/js/app.js
...
...
const Uploaders = {}
Uploaders.S3 = function(entries, onViewError){
entries.forEach(entry => {
let {url} = entry.meta
let xhr = new XMLHttpRequest()
onViewError(() => xhr.abort())
xhr.onload = () => {
xhr.status >= 200 && xhr.status < 300 ? entry.progress(100) : entry.error()
}
xhr.onerror = () => entry.error()
xhr.upload.addEventListener("progress", (event) => {
if(event.lengthComputable){
let percent = Math.round((event.loaded / event.total) * 100)
if(percent < 100){ entry.progress(percent) }
}
})
xhr.open("PUT", url, true)
xhr.send(entry.file)
})
}
...
...
let liveSocket = new LiveSocket("/live", Socket, {
uploaders: Uploaders,
params: {_csrf_token: csrfToken}
})
...