View Source Backpex.Fields.Upload (Backpex v0.8.2)

A field for handling uploads.

Warning

This field does not currently support Phoenix.LiveView.UploadWriter and direct / external uploads.

Options

  • :upload_key (atom) - Required identifier for the upload field (the name of the upload).
  • :accept (list) - Required filetypes that will be accepted.
  • :max_entries (integer) - Required number of max files that can be uploaded.
  • :max_file_size (integer) - Optional maximum file size in bytes to be allowed to uploaded. Defaults 8 MB (8_000_000).
  • :list_existing_files (function) - Required function that returns a list of all uploaded files based on an item.
  • :file_label (function) - Optional function to get the label of a single file.
  • :consume_upload (function) - Required function to consume file uploads.
  • :put_upload_change (function) - Required function to add file paths to the params.
  • :remove_uploads (function) - Required function that is being called after saving an item to be able to delete removed files

Info

The following examples copy uploads to a static folder in the application. In a production environment, you should consider uploading files to an appropriate object store.

Options in detail

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.

list_existing_files

Parameters

  • :socket - The socket.
  • :item (struct) - The item without its changes.

The function is being used to display existing uploads. The function receives the socket and the item and has to return a list of strings. Removed files during an edit of an item are automatically removed from the list. This option is required.

Example

def list_existing_files(_socket, item), do: item.files

file_label

Parameters

  • :file (string) - The file.

The function can be used to modify a file label based on a file. In the following example each file will have an "_upload" suffix. This option is optional.

Example

def file_label(file), do: file <> "_upload"

consume_upload

Parameters

  • :socket - The socket.
  • :item (struct) - The saved item (with its changes).
  • :meta - The upload meta.
  • :entry - The upload entry.

The function is used 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 for consume_uploaded_entries. See https://hexdocs.pm/phoenix_live_view/uploads.html#consume-uploaded-entries for more details. This option is required.

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

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)

This function is used 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. This option is required.

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

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

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_entries: 1,
        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

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

Link to this function

existing_file_paths(field, item, removed_files)

View Source

Returns a list of existing files mapped to a label.

Link to this function

label_from_file(field_options, file)

View Source

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"
Link to this function

list_existing_files(field, item, removed_files)

View Source

Lists existing files based on item and list of removed files.

Link to this function

map_file_paths(field, files)

View Source

Maps uploaded files to keyword list with identifier and label.