elixir-plug / plug

Compose web applications with functions
https://hex.pm/packages/plug
Other
2.88k stars 587 forks source link

Interest in an extension for Plug.Parsers.MULTIPART for FormData? #1168

Closed ndrean closed 1 year ago

ndrean commented 1 year ago

This could be a useful addition for parsing a FormData with multiple files and accepting thme, as an alternative to :multipart.

Why? You receive always the same key on a file input when one submits a formData. The idea is to parse the "parts" and change the key to a unique key, just indexed in fact.

defmodule Plug.Parsers.FD_MULTIPART do
  @multipart Plug.Parsers.MULTIPART

  @moduledoc """
  Custom multipart parser to enable multiple files upload.

  It generates a unique key for each file input.
  """
  @spec init(any) :: any
  def init(opts) do
    opts
  end

  def parse(conn, "multipart", subtype, headers, opts) do
    length = System.fetch_env!("UPLOAD_LIMIT") |> String.to_integer()
    opts = @multipart.init([length: length] ++ opts)
    @multipart.parse(conn, "multipart", subtype, headers, opts)
  end

  def parse(conn, _type, _subtype, _headers, _opts) do
    {:next, conn}
  end

  def multipart_to_params(parts, conn) do
    case filter_content_type(parts) do
      nil ->
        # do nothing if no files
        {:ok, %{}, conn}

      new_parts ->
        acc =
          for {name, _headers, body} <- Enum.reverse(new_parts),
              reduce: Plug.Conn.Query.decode_init() do
            acc -> Plug.Conn.Query.decode_each({name, body}, acc)
          end

        {:ok, Plug.Conn.Query.decode_done(acc, []), conn}
    end
  end

  @doc """
  Rebuilds the "content-type" sublist of "parts" by indexing the key.

  When it captures an entry with header "content-type", a new key will be assigned
  """
  def filter_content_type(parts) when is_list(parts) do
    filtered =
      parts
      |> Enum.filter(fn
        {_, [{"content-type", _}, {"content-disposition", _}], %Plug.Upload{}} = part ->
          part

        {_, [_], _} ->
          nil
      end)

    l = length(filtered)

    case l do
      0 ->
        nil

      _ ->
        others = Enum.filter(parts, fn elt -> !Enum.member?(filtered, elt) end)

        # get the initial key
        key = filtered |> hd() |> elem(0)
        # build a list of new keys, indexed.
        new_keys = Enum.map(1..l, fn i -> key <> "#{i}" end)

        # exchange the key to a new key
        f =
          Enum.zip_reduce([filtered, new_keys], [], fn elts, acc ->
            [{_, headers, content}, new_key] = elts
            [{new_key, headers, content} | acc]
          end)

        f ++ others
    end
  end
end
josevalim commented 1 year ago

Folks are welcome to explore this, but we don’t plan to make it part of Plug itself. Thanks!

ndrean commented 1 year ago

I would have just put this - or equivalent - in the docs, as an example on who to use it.