phoenix_live_view icon indicating copy to clipboard operation
phoenix_live_view copied to clipboard

upload does not accept any new drag&drops after issuing a `:too_many_files` error

Open DaTrader opened this issue 1 year ago • 6 comments

Environment

  • Phoenix version (mix deps): 0.20.17

Actual behavior

The allow_upload/3 config is external, autoload, max_entries: 1

Once a :too_many_files error is issued for trying to upload more files when only 1 is allowed, it no longer accepts any new drag & drops, not event of a single file. It simply sticks to its original :too_many_files error and ignores the new drag & drops.

This only happens in case of a :too_many_files error. Uploading a file by drag & drop after a different error (e.g. :not_accepted, :external_client_failure) works properly.

Expected behavior

Obviously, a new drag & drop upload should be validated and taken into consideration regardless of the previous error.

DaTrader avatar Jul 24 '24 14:07 DaTrader

+1

noozo avatar Oct 15 '24 15:10 noozo

I think we were bitten by this as well, and maybe even found a workaround

lessless avatar Oct 23 '24 10:10 lessless

If you can make a minimal reproduction of the error, i would love to take a stab at it! @DaTrader

rmand97 avatar Aug 01 '25 11:08 rmand97

@rmand97 Will do

DaTrader avatar Aug 03 '25 11:08 DaTrader

@rmand97 Here it is. And please remember to co-author me.

PS. In general it should definitely become a common practice, since it often takes way more time create a repro app than fix the bug.

Application.put_env( :sample, Example.Endpoint,
  http: [ ip: { 127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [ signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate( "a", 64),
  pubsub_server: Example.PubSub
)

Mix.install( [
  { :plug_cowboy, "~> 2.5"},
  { :jason, "~> 1.4"},
  { :phoenix, "~> 1.7.11"},
  { :phoenix_html, "~> 3.3"},
  { :phoenix_live_view, "~> 1.1.8"}
])

defmodule Example.ErrorView do
  def render( template, _), do: Phoenix.Controller.status_message_from_template( template)
end

# --- LiveComponent for upload ---
defmodule Example.UploadComponent do
  use Phoenix.LiveComponent
  import Phoenix.LiveView

  def mount( socket) do
    socket =
      allow_upload( socket, :file,
        accept: ~w(.jpg .jpeg .png .gif),
        max_entries: 1,
        max_file_size: 5_000_000
      )

    { :ok, assign( socket, entries: [], errors: [], saved_files: [])}
  end

  def consolidated_upload_errors( config) do
    config.entries
    |> Enum.take( 1)
    |> Enum.map( &upload_errors( config, &1))
    |> List.flatten()
    |> then( & &1 ++ upload_errors( config))
  end

  def render( assigns) do
    ~H"""
    <div class="p-4 border rounded-2xl shadow-md max-w-md mx-auto">
      <form id={"upload-form-#{@id}"} phx-change="validate" phx-submit="save" phx-target={@myself}>
        <!-- Drag & Drop Area -->
        <div
          phx-drop-target={@uploads.file.ref}
          class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-400 rounded-xl cursor-pointer hover:bg-gray-100 transition"
        >
          <p class="text-gray-600">Drag & drop a file here</p>
          <p class="text-sm text-gray-400">or click below to browse</p>
        </div>

        <!-- Fallback File Input Button -->
        <.live_file_input upload={@uploads.file}
          class="mt-4 bg-blue-500 text-white px-4 py-2 rounded-xl hover:bg-blue-600 cursor-pointer" />

        <button type="submit"
          class="mt-4 w-full bg-green-500 text-white px-4 py-2 rounded-xl hover:bg-green-600">
          Upload
        </button>
      </form>

      <div class="mt-4">
        <!-- Show persisted files -->
        <div :for={path <- @saved_files}>
          <img
            :if={String.match?( path, ~r/\.(png|jpg|jpeg|gif)$/i)}
            src={"/uploads/#{Path.basename( path)}"} class="mt-2 rounded-xl shadow max-h-64 mx-auto"
          />

          <p
            :if={!String.match?( path, ~r/\.(png|jpg|jpeg|gif)$/i)}
            class="text-gray-800"
          >
              Uploaded file: <%= Path.basename( path) %>
          </p>
        </div>

        <!-- Show errors -->
        <p :for={err <- consolidated_upload_errors( @uploads.file)} class="alert alert-danger">
          <%= upload_error_to_string( err) %>
        </p>
      </div>
    </div>
    """
  end

  def handle_event( event, _params, socket) do
    socket =
      case event do
        "save" ->
          save( socket)

        "validate" ->
          socket
      end

    { :noreply, socket}
  end

  def save( socket) do
    uploaded_files =
      consume_uploaded_entries( socket, :file, fn %{ path: path}, entry ->
        dest_dir = Path.expand( "priv/static/uploads", __DIR__)
        File.mkdir_p!( dest_dir)

        dest = Path.join( dest_dir, entry.client_name)
        File.cp!( path, dest)

        { :ok, "/uploads/#{entry.client_name}"}
      end)

    assign( socket, :saved_files, uploaded_files)
  end

  defp upload_error_to_string( :too_large), do: "File too large"
  defp upload_error_to_string( :too_many_files), do: "Too many files"
  defp upload_error_to_string( :not_accepted), do: "Unaccepted file type"
  defp upload_error_to_string( other), do: inspect( other)
end

# --- LiveView wrapper ---
defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: { __MODULE__, :live}

  def render( "live.html", assigns) do
    ~H"""
    <script src="https://cdn.jsdelivr.net/npm/phoenix/priv/static/phoenix.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/phoenix_live_view/priv/static/phoenix_live_view.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>* { font-size: 16px; }</style>
    <%= @inner_content %>
    """
  end

  def render( assigns) do
    ~H"""
    <div class="min-h-screen flex items-center justify-center bg-gray-50">
      <.live_component module={Example.UploadComponent} id="uploader" />
    </div>
    """
  end
end

# --- Router / Endpoint ---
defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug( :accepts, [ "html"])
  end

  scope "/", Example do
    pipe_through( :browser)
    live( "/", HomeLive)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample

  socket( "/live", Phoenix.LiveView.Socket)

  plug Plug.Static,
       at: "/uploads",
       from: Path.expand( "priv/static/uploads", __DIR__),
       gzip: false

  plug( Example.Router)
end

children = [
  Example.Endpoint,
  { Phoenix.PubSub, name: Example.PubSub }
]

{ :ok, _} = Supervisor.start_link( children, strategy: :one_for_one)
Process.sleep( :infinity)

DaTrader avatar Aug 22 '25 14:08 DaTrader

@DaTrader I agree and I will!

rmand97 avatar Aug 23 '25 20:08 rmand97