elixir-plug / plug

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

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

Closed ndrean closed 11 months ago

ndrean commented 11 months 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 11 months ago

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

ndrean commented 11 months ago

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