# `Backpex.Fields.Upload`
[🔗](https://github.com/naymspace/backpex/blob/0.18.3/lib/backpex/fields/upload.ex#L2)

A field for handling uploads.

> #### Warning {: .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` (`t: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` (`t:non_neg_integer/0`) - Number of max files that can be uploaded. The default value is `1`.

* `:max_file_size` (`t:pos_integer/0`) - Optional maximum file size in bytes to be allowed to uploaded. The default value is `8000000`.

* `: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 for `consume_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 editing an item to be able to delete removed files.

  Note that this function is not invoked when an item is deleted. Therefore, you must implement file deletion logic in the `c:Backpex.LiveResource.on_item_deleted/2` callback.

  **Parameters**

  * `:socket` - The socket.
  * `:item` (struct) - The item without its changes.
  * `removed_entries` (list) - A list of removed uploads during edit. The list only contains files that existed before the 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](https://hexdocs.pm/phoenix_live_view/external-uploads.html).

      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 {: .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_label: :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](https://hexdocs.pm/phoenix_live_view/external-uploads.html).

    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,
      ...
    })

# `config_schema`

Returns the schema of configurable options for this field.

This can be useful for reuse in other field modules.

# `existing_file_paths`

Returns a list of existing files mapped to a label.

# `label_from_file`

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"

# `list_existing_files`

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

# `map_file_paths`

Maps uploaded files to keyword list with identifier and label.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
