upload does not accept any new drag&drops after issuing a `:too_many_files` error
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.
+1
I think we were bitten by this as well, and maybe even found a workaround
If you can make a minimal reproduction of the error, i would love to take a stab at it! @DaTrader
@rmand97 Will do
@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 I agree and I will!