Backpex.Fields.Upload (Backpex v0.12.0)
View SourceA field for handling uploads.
Warning
This field does not currently support using a custom Phoenix.LiveView.UploadWriter
.
Field-specific options
See Backpex.Field
for general field options.
The upload_key
, accept
, max_entries
and max_file_size
options are forwarded to
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3. See the documentation for more information.
:upload_key
(atom/0
) - Required. Required identifier for the upload field (the name of the upload).:accept
- List of filetypes that will be accepted or:any
. The default value is:any
.:max_entries
(non_neg_integer/0
) - Number of max files that can be uploaded. The default value is1
.:max_file_size
(pos_integer/0
) - Optional maximum file size in bytes to be allowed to uploaded. The default value is8000000
.:list_existing_files
(function of arity 1) - Required. A function being used to display existing uploads. It has to return a list of all uploaded files as strings. Removed files during an edit of an item are automatically removed from the list.Parameters
:item
(struct) - The item without its changes.
Example
def list_existing_files(item), do: item.files
:file_label
(function of arity 1) - A function to be used to modify a file label of a single file. In the following example each file will have an_upload
suffix.Parameters
:file
(string) - The file.
Example
def file_label(file), do: file <> "_upload"
:consume_upload
(function of arity 4) - Required. Required function to consume file uploads. A function to consume uploads. It is called after the item has been saved and is used to copy the files to a specific destination. Backpex will use this function as a callback forconsume_uploaded_entries
. See https://hexdocs.pm/phoenix_live_view/uploads.html#consume-uploaded-entries for more details.Parameters
:socket
- The socket.:item
(struct) - The saved item (with its changes).:meta
- The upload meta.:entry
- The upload entry.
Example
defp consume_upload(_socket, _item, %{path: path} = _meta, entry) do file_name = ... file_url = ... static_dir = ... dest = Path.join([:code.priv_dir(:demo), "static", static_dir, file_name]) File.cp!(path, dest) {:ok, file_url} end
:put_upload_change
(function of arity 6) - Required. A function to modify the params based on certain parameters. It is important because it ensures that file paths are added to the item change and therefore persisted in the database.Parameters
:socket
- The socket.:params
(map) - The current params that will be passed to the changeset function.:item
(struct) - The item without its changes. On create will this will be an empty map.uploaded_entries
(tuple) - The completed and in progress entries for the upload.removed_entries
(list) - A list of removed uploads during edit.action
(atom) - The action (:validate
or:insert
)
Example
def put_upload_change(_socket, params, item, uploaded_entries, removed_entries, action) do existing_files = item.files -- removed_entries new_entries = case action do :validate -> elem(uploaded_entries, 1) :insert -> elem(uploaded_entries, 0) end files = existing_files ++ Enum.map(new_entries, fn entry -> file_name(entry) end) Map.put(params, "images", files) end
:remove_uploads
(function of arity 3) - Required. A function that is being called after saving an item to be able to delete removed files.Parameters
:socket
- The socket.:item
(struct) - The item without its changes.removed_entries
(list) - A list of removed uploads during edit.
Example
defp remove_uploads(_socket, _item, removed_entries) do for file <- removed_entries do file_path = ... File.rm!(file_path) end end
:external
(function of arity 2) - A 2-arity function that allows the server to generate metadata for each upload entry.Parameters
:entry
- The upload entry.:socket
- The socket.
Examples
This is an example for S3-Compatible object storage, for more examples check the Phoenix LiveView documentation for External Uploads.
defp presign_upload(entry, socket) do config = ExAws.Config.new(:s3) key = "uploads/example/" <> entry.client_name {:ok, url} = ExAws.S3.presigned_url(config, :put, @bucket, key, expires_in: 3600, query_params: [{"Content-Type", entry.client_type}] ) meta = %{uploader: "S3", key: key, url: url} {:ok, meta, socket} end
Info
The first two examples copy uploads to a static folder in the application. In a production environment, you should consider uploading files to an appropriate object store.
Full Single File Example
In this example we are adding an avatar upload for a user. We implement it so that exactly one avatar must exist.
defmodule Demo.Repo.Migrations.AddAvatarToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add(:avatar, :string, null: false, default: "")
end
end
end
defmodule Demo.User do
use Ecto.Schema
schema "users" do
field(:avatar, :string, default: "")
...
end
def changeset(user, attrs, _metadata \ []) do
user
|> cast(attrs, [:avatar])
|> validate_required([:avatar])
|> validate_change(:avatar, fn
:avatar, "too_many_files" ->
[avatar: "has to be exactly one"]
:avatar, "" ->
[avatar: "can't be blank"]
:avatar, _avatar ->
[]
end)
end
end
defmodule DemoWeb.UserLive do
use Backpex.LiveResource,
...
@impl Backpex.LiveResource
def fields do
[
avatar: %{
module: Backpex.Fields.Upload,
label: "Avatar",
upload_key: :avatar,
accept: ~w(.jpg .jpeg .png),
max_file_size: 512_000,
put_upload_change: &put_upload_change/6,
consume_upload: &consume_upload/4,
remove_uploads: &remove_uploads/3,
list_existing_files: &list_existing_files/1,
render: fn
%{value: value} = assigns when value == "" or is_nil(value) ->
~H"<p><%= Backpex.HTML.pretty_value(@value) %></p>"
assigns ->
~H'<img class="h-10 w-auto" src={file_url(@value)} />'
end
},
...
]
end
defp list_existing_files(%{avatar: avatar} = _item) when avatar != "" and not is_nil(avatar), do: [avatar]
defp list_existing_files(_item), do: []
def put_upload_change(_socket, params, item, uploaded_entries, removed_entries, action) do
existing_files = list_existing_files(item) -- removed_entries
new_entries =
case action do
:validate ->
elem(uploaded_entries, 1)
:insert ->
elem(uploaded_entries, 0)
end
files = existing_files ++ Enum.map(new_entries, fn entry -> file_name(entry) end)
case files do
[file] ->
Map.put(params, "avatar", file)
[_file | _other_files] ->
Map.put(params, "avatar", "too_many_files")
[] ->
Map.put(params, "avatar", "")
end
end
defp consume_upload(_socket, _item, %{path: path} = _meta, entry) do
file_name = file_name(entry)
dest = Path.join([:code.priv_dir(:demo), "static", upload_dir(), file_name])
File.cp!(path, dest)
{:ok, file_url(file_name)}
end
defp remove_uploads(_socket, _item, removed_entries) do
for file <- removed_entries do
path = Path.join([:code.priv_dir(:demo), "static", upload_dir(), file])
File.rm!(path)
end
end
defp file_url(file_name) do
static_path = Path.join([upload_dir(), file_name])
Phoenix.VerifiedRoutes.static_url(DemoWeb.Endpoint, "/" <> static_path)
end
defp file_name(entry) do
[ext | _] = MIME.extensions(entry.client_type)
entry.uuid <> "." <> ext
end
defp upload_dir, do: Path.join(["uploads", "user", "avatar"])
end
Full Multi File Example
In this example, we are adding images to a product resource. We limit the images to a maximum of 2.
defmodule Demo.Repo.Migrations.AddImagesToProducts do
use Ecto.Migration
def change do
alter table(:products) do
add(:images, {:array, :string})
end
end
end
defmodule Demo.Product do
use Ecto.Schema
schema "products" do
field(:images, {:array, :string})
...
end
def changeset(user, attrs, _metadata \ []) do
user
|> cast(attrs, [:images])
|> validate_length(:images, max: 2)
end
end
defmodule DemoWeb.ProductLive do
use Backpex.LiveResource,
...
@impl Backpex.LiveResource
def fields do
[
images: %{
module: Backpex.Fields.Upload,
label: "Images",
upload_key: :images,
accept: ~w(.jpg .jpeg .png),
max_entries: 2,
max_file_size: 512_000,
put_upload_change: &put_upload_change/6,
consume_upload: &consume_upload/4,
remove_uploads: &remove_uploads/3,
list_existing_files: &list_existing_files/1,
render: fn
%{value: value} = assigns when is_list(value) ->
~H'''
<div>
<img :for={img <- @value} class="h-10 w-auto" src={file_url(img)} />
</div>
'''
assigns ->
~H'<p><%= Backpex.HTML.pretty_value(@value) %></p>'
end,
except: [:index, :resource_action],
align: :center
},
...
]
end
defp list_existing_files(%{images: images} = _item) when is_list(images), do: images
defp list_existing_files(_item), do: []
defp put_upload_change(_socket, params, item, uploaded_entries, removed_entries, action) do
existing_files = list_existing_files(item) -- removed_entries
new_entries =
case action do
:validate ->
elem(uploaded_entries, 1)
:insert ->
elem(uploaded_entries, 0)
end
files = existing_files ++ Enum.map(new_entries, fn entry -> file_name(entry) end)
Map.put(params, "images", files)
end
defp consume_upload(_socket, _item, %{path: path} = _meta, entry) do
file_name = file_name(entry)
dest = Path.join([:code.priv_dir(:demo), "static", upload_dir(), file_name])
File.cp!(path, dest)
{:ok, file_url(file_name)}
end
defp remove_uploads(_socket, _item, removed_entries) do
for file <- removed_entries do
path = Path.join([:code.priv_dir(:demo), "static", upload_dir(), file])
File.rm!(path)
end
end
defp file_url(file_name) do
static_path = Path.join([upload_dir(), file_name])
Phoenix.VerifiedRoutes.static_url(DemoWeb.Endpoint, "/" <> static_path)
end
defp file_name(entry) do
[ext | _] = MIME.extensions(entry.client_type)
entry.uuid <> "." <> ext
end
defp upload_dir, do: Path.join(["uploads", "product", "images"])
end
Full External File Example
In this example we are adding an avatar upload for a user and storing it in an external object storage like S3 or R2
This example works with Cloudflare R2 and assumes that you configured ExAws
and ExAws.S3
correctly and that you're
serving the images from a CDN in front of your object storage.
For more details check the Phoenix LiveView documentation for External Uploads.
defmodule Demo.Repo.Migrations.AddAvatarToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add(:avatar, :string)
end
end
end
defmodule Demo.User do
use Ecto.Schema
schema "users" do
field(:avatar, :string)
...
end
def changeset(user, attrs, _metadata \ []) do
user
|> cast(attrs, [:avatar])
|> validate_change(:avatar, fn
:avatar, "too_many_files" ->
[avatar: "has to be exactly one"]
:avatar, "" ->
[avatar: "can't be blank"]
:avatar, _avatar ->
[]
end)
end
end
defmodule DemoWeb.UserLive do
use Backpex.LiveResource,
...
@base_cdn_path "https://cdn.example.com/"
@upload_path "uploads/backpex/"
@bucket "example"
@base_r2_host "https://my_host.r2.cloudflarestorage.com/"
@impl Backpex.LiveResource
def fields do
[
avatar: %{
module: Backpex.Fields.Upload,
label: "Avatar",
upload_key: :avatar,
accept: ~w(.jpg .jpeg .png),
max_file_size: 512_000,
put_upload_change: &put_upload_change/6,
consume_upload: &consume_upload/4,
remove_uploads: &remove_uploads/3,
list_existing_files: &list_existing_files/1,
external: &presign_upload/2,
render: fn
%{value: value} = assigns when value == "" or is_nil(value) ->
~H"<p>{Backpex.HTML.pretty_value(@value)}</p>"
assigns ->
~H'<img class="h-10 w-auto" src={@value} />'
end
},
...
]
end
defp list_existing_files(%{avatar: avatar} = _item) when avatar != "" and not is_nil(avatar), do: [avatar]
defp list_existing_files(_item), do: []
defp presign_upload(entry, socket) do
config = ExAws.Config.new(:s3)
key = @upload_path <> entry.client_name
{:ok, url} =
ExAws.S3.presigned_url(config, :put, @bucket, key,
expires_in: 3600,
query_params: [{"Content-Type", entry.client_type}]
)
meta = %{uploader: "S3", key: key, url: url}
{:ok, meta, socket}
end
def put_upload_change(_socket, params, item, uploaded_entries, removed_entries, action) do
existing_files = list_existing_files(item) -- removed_entries
new_entries =
case action do
:validate ->
elem(uploaded_entries, 1)
:insert ->
elem(uploaded_entries, 0)
end
files = existing_files ++ Enum.map(new_entries, fn entry -> entry.client_name end)
case files do
[file] ->
file_path = @base_cdn_path <> @upload_path <> file
Map.put(params, "avatar", file_path)
[_file | _other_files] ->
Map.put(params, "avatar", "too_many_files")
[] ->
Map.put(params, "avatar", "")
end
end
defp consume_upload(_socket, _item, _meta, _entry) do
{:ok, :external}
end
defp remove_uploads(_socket, _item, removed_entries) do
for file <- removed_entries do
object = String.replace_prefix(file, @base_cdn_path, "")
ExAws.S3.delete_object(@bucket, object) |> ExAws.request!()
end
end
end
You also need to create an Uploader
in the app.js
file to handle the actual upload
let Uploaders = {}
Uploaders.S3 = function (entries, onViewError) {
entries.forEach(entry => {
let xhr = new XMLHttpRequest()
onViewError(() => xhr.abort())
xhr.onload = () => xhr.status === 200 ? 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) }
}
})
let url = entry.meta.url
xhr.open("PUT", url, true)
xhr.send(entry.file)
})
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
uploaders: Uploaders,
...
})
Summary
Functions
Returns a list of existing files mapped to a label.
Calls field option function to get label from filename. Defaults to filename.
Lists existing files based on item and list of removed files.
Maps uploaded files to keyword list with identifier and label.
Functions
Returns a list of existing files mapped to a label.
Calls field option function to get label from filename. Defaults to filename.
Examples
iex> Backpex.Fields.Upload.label_from_file(%{file_label: fn file -> file <> "xyz" end}, "file")
"filexyz"
iex> Backpex.Fields.Upload.label_from_file(%{}, "file")
"file"
Lists existing files based on item and list of removed files.
Maps uploaded files to keyword list with identifier and label.