Skip to content

Commit 3179652

Browse files
authored
Add custom upload validator to allow_upload/3 options (#4189)
1 parent 34c0f64 commit 3179652

File tree

6 files changed

+142
-10
lines changed

6 files changed

+142
-10
lines changed

lib/phoenix_component.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,13 +1216,15 @@ defmodule Phoenix.Component do
12161216
* `:not_accepted` - The entry does not match the `:accept` MIME types
12171217
* `:external_client_failure` - When external upload fails
12181218
* `{:writer_failure, reason}` - When the custom writer fails with `reason`
1219+
* `reason` - When the custom validator fails with `reason`
12191220
12201221
## Examples
12211222
12221223
```elixir
12231224
defp upload_error_to_string(:too_large), do: "The file is too large"
12241225
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
12251226
defp upload_error_to_string(:external_client_failure), do: "Something went terribly wrong"
1227+
defp upload_error_to_string(:custom_validator_error), do: "Custom validation error"
12261228
```
12271229
12281230
```heex

lib/phoenix_live_view.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,13 +891,26 @@ defmodule Phoenix.LiveView do
891891
writing the uploaded chunks. Defaults to writing to a temporary file for consumption.
892892
See the `Phoenix.LiveView.UploadWriter` docs for custom usage.
893893
894+
* `:validator` - An optional 1-arity function for performing custom validation
895+
on each upload entry. The function receives the upload entry and must return
896+
either `:ok` or `{:error, reason}`. When an error tuple is returned, the
897+
entry is marked as failed and the error is exposed as `reason` via `upload_errors/2`.
898+
894899
Raises when a previously allowed upload under the same name is still active.
895900
896901
## Examples
897902
898903
allow_upload(socket, :avatar, accept: ~w(.jpg .jpeg), max_entries: 2)
899904
allow_upload(socket, :avatar, accept: :any)
900905
906+
allow_upload(socket, :avatar, validator: fn entry ->
907+
if String.length(entry.client_name) > 100 do
908+
{:error, :filename_too_long}
909+
else
910+
:ok
911+
end
912+
end)
913+
901914
For consuming files automatically as they are uploaded, you can pair `auto_upload: true` with
902915
a custom progress function to consume the entries as they are completed. For example:
903916

lib/phoenix_live_view/upload_config.ex

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ defmodule Phoenix.LiveView.UploadConfig do
7777
:errors,
7878
:auto_upload?,
7979
:progress_event,
80-
:writer
80+
:writer,
81+
:validator
8182
]}
8283

8384
defstruct name: nil,
@@ -99,7 +100,8 @@ defmodule Phoenix.LiveView.UploadConfig do
99100
errors: [],
100101
auto_upload?: false,
101102
progress_event: nil,
102-
writer: nil
103+
writer: nil,
104+
validator: nil
103105

104106
@type t :: %__MODULE__{
105107
name: atom() | String.t(),
@@ -124,6 +126,7 @@ defmodule Phoenix.LiveView.UploadConfig do
124126
auto_upload?: boolean(),
125127
writer: (name :: atom() | String.t(), UploadEntry.t(), Phoenix.LiveView.Socket.t() ->
126128
{module(), term()}),
129+
validator: (UploadEntry.t() -> :ok | {:error, atom()}) | nil,
127130
progress_event:
128131
(name :: atom() | String.t(), UploadEntry.t(), Phoenix.LiveView.Socket.t() ->
129132
{:noreply, Phoenix.LiveView.Socket.t()})
@@ -294,6 +297,24 @@ defmodule Phoenix.LiveView.UploadConfig do
294297
fn _name, _entry, _socket -> {Phoenix.LiveView.UploadTmpFileWriter, []} end
295298
end
296299

300+
validator =
301+
case Keyword.fetch(opts, :validator) do
302+
{:ok, func} when is_function(func, 1) ->
303+
func
304+
305+
{:ok, other} ->
306+
raise ArgumentError, """
307+
invalid :validator value provided to allow_upload.
308+
309+
Only a 1-arity anonymous function is supported. Got:
310+
311+
#{inspect(other)}
312+
"""
313+
314+
:error ->
315+
fn _entry -> :ok end
316+
end
317+
297318
%UploadConfig{
298319
ref: random_ref,
299320
name: name,
@@ -309,6 +330,7 @@ defmodule Phoenix.LiveView.UploadConfig do
309330
chunk_timeout: chunk_timeout,
310331
progress_event: progress_event,
311332
writer: writer,
333+
validator: validator,
312334
auto_upload?: Keyword.get(opts, :auto_upload, false),
313335
allowed?: true
314336
}
@@ -558,6 +580,7 @@ defmodule Phoenix.LiveView.UploadConfig do
558580
{:ok, entry}
559581
|> validate_max_file_size(conf)
560582
|> validate_accepted(conf)
583+
|> call_validator(conf)
561584
|> case do
562585
{:ok, entry} ->
563586
{:ok, put_valid_entry(conf, entry)}
@@ -632,6 +655,15 @@ defmodule Phoenix.LiveView.UploadConfig do
632655
end
633656
end
634657

658+
defp call_validator({:ok, entry}, %UploadConfig{validator: validator_fun}) do
659+
case validator_fun.(entry) do
660+
{:error, reason} -> {:error, reason}
661+
_ -> {:ok, entry}
662+
end
663+
end
664+
665+
defp call_validator({:error, _} = error, _conf), do: error
666+
635667
defp recalculate_computed_fields(%UploadConfig{} = conf) do
636668
recalculate_errors(conf)
637669
end

test/phoenix_live_view/upload/channel_test.exs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,20 @@ defmodule Phoenix.LiveView.UploadChannelTest do
179179
end
180180

181181
defp opts_for_allow_upload(opts) do
182-
case Keyword.fetch(opts, :progress) do
183-
{:ok, progress} ->
184-
Keyword.put(opts, :progress, fn _, entry, socket ->
185-
apply(__MODULE__, progress, [entry, socket])
186-
end)
182+
opts =
183+
case Keyword.fetch(opts, :progress) do
184+
{:ok, progress} ->
185+
Keyword.put(opts, :progress, fn _, entry, socket ->
186+
apply(__MODULE__, progress, [entry, socket])
187+
end)
188+
189+
:error ->
190+
opts
191+
end
187192

188-
:error ->
189-
opts
193+
case Keyword.fetch(opts, :validator_response) do
194+
{:ok, response} -> Keyword.put(opts, :validator, fn _ -> response end)
195+
:error -> opts
190196
end
191197
end
192198

@@ -406,6 +412,25 @@ defmodule Phoenix.LiveView.UploadChannelTest do
406412
assert {:error, [[_ref, :too_large]]} = render_upload(avatar, "foo.jpeg")
407413
end
408414

415+
@tag allow: [
416+
max_entries: 1,
417+
chunk_size: 20,
418+
accept: :any,
419+
max_file_size: 100,
420+
validator_response: {:error, :custom_validation_error}
421+
]
422+
test "render_change error with validator failure upload", %{lv: lv} do
423+
avatar = file_input(lv, "form", :avatar, [%{name: "foo.jpeg", content: "ok"}])
424+
425+
assert lv
426+
|> form("form", user: %{})
427+
|> render_change(avatar) =~
428+
"entry_error::custom_validation_error"
429+
430+
assert {:error, [[_ref, :custom_validation_error]]} =
431+
render_upload(avatar, "foo.jpeg")
432+
end
433+
409434
@tag allow: [max_entries: 1, chunk_size: 20, accept: :any, auto_upload: true]
410435
test "render_upload too many files with auto_upload", %{lv: lv} do
411436
avatar =

test/phoenix_live_view/upload/config_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,32 @@ defmodule Phoenix.LiveView.UploadConfigTest do
153153

154154
assert %UploadConfig{max_file_size: 10_000_000} = socket.assigns.uploads.avatar
155155
end
156+
157+
test "raises when invalid :validator provided" do
158+
assert_raise ArgumentError, ~r/invalid :validator value provided to allow_upload/, fn ->
159+
LiveView.allow_upload(build_socket(), :avatar, accept: :any, validator: 0)
160+
end
161+
162+
assert_raise ArgumentError, ~r/invalid :validator value provided to allow_upload/, fn ->
163+
LiveView.allow_upload(build_socket(), :avatar, accept: :any, validator: "bad")
164+
end
165+
166+
assert_raise ArgumentError, ~r/invalid :validator value provided to allow_upload/, fn ->
167+
wrong_arity_fun = fn _, _ -> :ok end
168+
LiveView.allow_upload(build_socket(), :avatar, accept: :any, validator: wrong_arity_fun)
169+
end
170+
end
171+
172+
test "supports optional :validator" do
173+
socket =
174+
LiveView.allow_upload(build_socket(), :avatar,
175+
accept: :any,
176+
validator: fn _ -> :ok end
177+
)
178+
179+
%UploadConfig{validator: validator_fun} = socket.assigns.uploads.avatar
180+
assert is_function(validator_fun, 1)
181+
end
156182
end
157183

158184
describe "disallow_upload/2" do

test/phoenix_live_view/upload/external_test.exs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ defmodule Phoenix.LiveView.UploadExternalTest do
4545
opts
4646
end
4747

48+
opts =
49+
case Keyword.fetch(opts, :validator_response) do
50+
{:ok, response} -> Keyword.put(opts, :validator, fn _ -> response end)
51+
:error -> opts
52+
end
53+
4854
{:ok, lv} = mount_lv(fn socket -> Phoenix.LiveView.allow_upload(socket, :avatar, opts) end)
4955

5056
{:ok, lv: lv}
@@ -81,7 +87,13 @@ defmodule Phoenix.LiveView.UploadExternalTest do
8187
assert render_upload(avatar, "foo1.jpeg", 1) =~ "relative path:some/path/to/foo1.jpeg"
8288
end
8389

84-
@tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :preflight]
90+
@tag allow: [
91+
max_entries: 2,
92+
chunk_size: 20,
93+
accept: :any,
94+
external: :preflight,
95+
validator_response: :ok
96+
]
8597
test "external upload invokes preflight per entry", %{lv: lv} do
8698
avatar =
8799
file_input(lv, "form", :avatar, [
@@ -165,6 +177,28 @@ defmodule Phoenix.LiveView.UploadExternalTest do
165177
assert {:error, :not_allowed} = render_upload(avatar, "foo2.jpeg", 1)
166178
end
167179

180+
@tag allow: [
181+
max_entries: 1,
182+
max_file_size: 100,
183+
auto_upload: true,
184+
accept: :any,
185+
external: :preflight,
186+
validator_response: {:error, :custom_validation_error}
187+
]
188+
test "custom validator returns error", %{lv: lv} do
189+
avatar = file_input(lv, "form", :avatar, [%{name: "foo.jpeg", content: "ok"}])
190+
191+
html =
192+
lv
193+
|> form("form", user: %{})
194+
|> render_change(avatar)
195+
196+
assert html =~ "foo.jpeg:0%"
197+
198+
assert {:error, [[_, %{reason: :custom_validation_error}]]} =
199+
render_upload(avatar, "foo.jpeg", 1)
200+
end
201+
168202
def bad_preflight(%LiveView.UploadEntry{} = _entry, socket), do: {:ok, %{}, socket}
169203

170204
@tag allow: [max_entries: 1, chunk_size: 20, accept: :any, external: :bad_preflight]

0 commit comments

Comments
 (0)