Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix auto upload #2807

Merged
merged 5 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/js/phoenix_live_view/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ let DOM = {

isUploadInput(el){ return el.type === "file" && el.getAttribute(PHX_UPLOAD_REF) !== null },

isAutoUpload(inputEl){ return inputEl.hasAttribute("data-phx-auto-upload") },

findUploadInputs(node){ return this.all(node, `input[type="file"][${PHX_UPLOAD_REF}]`) },

findComponentNodeList(node, cid){
Expand Down
1 change: 1 addition & 0 deletions assets/js/phoenix_live_view/live_uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class LiveUploader {
})

let groupedEntries = this._entries.reduce((acc, entry) => {
if(!entry.meta){ return acc }
let {name, callback} = entry.uploader(liveSocket.uploaders)
acc[name] = acc[name] || {callback: callback, entries: []}
acc[name].entries.push(entry)
Expand Down
3 changes: 2 additions & 1 deletion assets/js/phoenix_live_view/upload_entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "./utils"

import LiveUploader from "./live_uploader"
import DOM from "./dom"

export default class UploadEntry {
static isActive(fileEl, file){
Expand Down Expand Up @@ -71,7 +72,7 @@ export default class UploadEntry {
error(reason = "failed"){
this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)
this.view.pushFileProgress(this.fileEl, this.ref, {error: reason})
LiveUploader.clearFiles(this.fileEl)
if(!DOM.isAutoUpload(this.fileEl)){ LiveUploader.clearFiles(this.fileEl) }
}

//private
Expand Down
5 changes: 4 additions & 1 deletion assets/js/phoenix_live_view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ export default class View {
}
this.pushWithReply(refGenerator, "event", event, resp => {
DOM.showError(inputEl, this.liveSocket.binding(PHX_FEEDBACK_FOR))
if(DOM.isUploadInput(inputEl) && inputEl.getAttribute("data-phx-auto-upload") !== null){
if(DOM.isUploadInput(inputEl) && DOM.isAutoUpload(inputEl)){
if(LiveUploader.filesAwaitingPreflight(inputEl).length > 0){
let [ref, _els] = refGenerator()
this.uploadFiles(inputEl.form, targetCtx, ref, cid, (_uploads) => {
Expand Down Expand Up @@ -1073,7 +1073,10 @@ export default class View {
let inputs = Array.from(form.elements).filter(el => DOM.isFormInput(el) && el.name && !el.hasAttribute(phxChange))
if(inputs.length === 0){ return }

// we must clear tracked uploads before recovery as they no longer have valid refs
inputs.forEach(input => input.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input))
let input = inputs.find(el => el.type !== "hidden") || inputs[0]

let phxEvent = form.getAttribute(this.binding(PHX_AUTO_RECOVER)) || form.getAttribute(this.binding("change"))
JS.exec("change", phxEvent, view, input, ["push", {_target: input.name, newCid: newCid, callback: callback}])
})
Expand Down
5 changes: 1 addition & 4 deletions lib/phoenix_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2797,17 +2797,14 @@ defmodule Phoenix.Component do
data-phx-active-refs={join_refs(for(entry <- @upload.entries, do: entry.ref))}
data-phx-done-refs={join_refs(for(entry <- @upload.entries, entry.done?, do: entry.ref))}
data-phx-preflighted-refs={join_refs(for(entry <- @upload.entries, entry.preflighted?, do: entry.ref))}
data-phx-auto-upload={valid_upload?(@upload) and @upload.auto_upload?}
data-phx-auto-upload={@upload.auto_upload?}
{if @upload.max_entries > 1, do: Map.put(@rest, :multiple, true), else: @rest}
/>
"""
end

defp join_refs(entries), do: Enum.join(entries, ",")

defp valid_upload?(%{entries: [_ | _], errors: []}), do: true
defp valid_upload?(%{}), do: false

@doc """
Generates an image preview on the client for a selected file.

Expand Down
44 changes: 32 additions & 12 deletions lib/phoenix_live_view/test/live_view_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1863,19 +1863,39 @@ defmodule Phoenix.LiveViewTest do
you will need to call `render_submit/1`.
"""
def render_upload(%Upload{} = upload, entry_name, percent \\ 100) do
if UploadClient.allow_acknowledged?(upload) do
render_chunk(upload, entry_name, percent)
else
case preflight_upload(upload) do
{:ok, %{ref: ref, config: config, entries: entries_resp}} ->
case UploadClient.allowed_ack(upload, ref, config, entries_resp) do
:ok -> render_chunk(upload, entry_name, percent)
{:error, reason} -> {:error, reason}
end
entry_ref =
Enum.find_value(upload.entries, fn
%{"name" => ^entry_name, "ref" => ref} -> ref
%{} -> nil
end)

unless entry_name do
raise ArgumentError, "no such entry with name #{inspect(entry_name)}"
end

{:error, reason} ->
{:error, reason}
end
case UploadClient.fetch_allow_acknowledged(upload, entry_name) do
{:ok, _token} ->
render_chunk(upload, entry_name, percent)

{:error, :nopreflight} ->
case preflight_upload(upload) do
{:ok, %{ref: ref, config: config, entries: entries_resp, errors: errors}} ->
if entry_errors = errors[entry_ref] do
UploadClient.allowed_ack(upload, ref, config, entry_name, entries_resp, errors)
{:error, for(reason <- entry_errors , do: [entry_ref, reason])}
else
case UploadClient.allowed_ack(upload, ref, config, entry_name, entries_resp, errors) do
:ok -> render_chunk(upload, entry_name, percent)
{:error, reason} -> {:error, reason}
end
end

{:error, reason} ->
{:error, reason}
end

{:error, reason} ->
{:error, reason}
end
end

Expand Down
95 changes: 75 additions & 20 deletions lib/phoenix_live_view/test/upload_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ defmodule Phoenix.LiveViewTest.UploadClient do
GenServer.call(pid, :channel_pids)
end

def allow_acknowledged?(%Upload{pid: pid}) do
GenServer.call(pid, :allow_acknowledged)
def fetch_allow_acknowledged(%Upload{pid: pid}, entry_name) do
GenServer.call(pid, {:fetch_allow_acknowledged, entry_name})
end

def chunk(%Upload{pid: pid, element: element}, name, percent, proxy_pid) do
Expand All @@ -31,8 +31,8 @@ defmodule Phoenix.LiveViewTest.UploadClient do
GenServer.call(pid, {:simulate_attacker_chunk, name, chunk})
end

def allowed_ack(%Upload{pid: pid, entries: entries}, ref, config, entries_resp) do
GenServer.call(pid, {:allowed_ack, ref, config, entries, entries_resp})
def allowed_ack(%Upload{pid: pid, entries: entries}, ref, config, name, entries_resp, errors) do
GenServer.call(pid, {:allowed_ack, ref, config, name, entries, entries_resp, errors})
end

def start_link(opts) do
Expand All @@ -46,19 +46,38 @@ defmodule Phoenix.LiveViewTest.UploadClient do
{:ok, %{socket: socket, cid: cid, upload_ref: nil, config: %{}, entries: %{}}}
end

def handle_call(:allow_acknowledged, _from, state) do
{:reply, state.upload_ref != nil, state}
def handle_call({:fetch_allow_acknowledged, entry_name}, _from, state) do
case Map.fetch(state.entries, entry_name) do
{:ok, {:error, reason}} -> {:reply, {:error, reason}, state}
{:ok, token} -> {:reply, {:ok, token}, state}
:error -> {:reply, {:error, :nopreflight}, state}
end
end

def handle_call({:allowed_ack, ref, config, entries, entries_resp}, _from, state) do
entries =
for client_entry <- entries, into: %{} do
%{"ref" => ref, "name" => name} = client_entry
token = Map.fetch!(entries_resp, ref)
{name, build_and_join_entry(state, client_entry, token)}
end

{:reply, :ok, %{state | upload_ref: ref, config: config, entries: entries}}
def handle_call(
{:allowed_ack, upload_ref, config, name, entries, entries_resp, errors},
_from,
state
) do
new_entries =
Enum.reduce(entries, state.entries, fn %{"ref" => ref, "name" => name} = client_entry,
acc ->
case Map.fetch(entries_resp, ref) do
{:ok, token} ->
Map.put(acc, name, build_and_join_entry(state, client_entry, token))

:error ->
Map.put(acc, name, {:error, Map.get(errors, ref, :not_allowed)})
end
end)

new_state = %{state | upload_ref: upload_ref, config: config, entries: new_entries}

case new_entries do
%{^name => {:error, reason}} -> {:reply, {:error, reason}, new_state}
%{^name => _} -> {:reply, :ok, new_state}
%{} -> raise_unknown_entry!(state, name)
end
end

def handle_call(:channel_pids, _from, state) do
Expand Down Expand Up @@ -148,12 +167,23 @@ defmodule Phoenix.LiveViewTest.UploadClient do

defp do_chunk(%{socket: nil, cid: cid} = state, from, entry, proxy_pid, element, percent) do
stats = progress_stats(entry, percent)
:ok = ClientProxy.report_upload_progress(proxy_pid, from, element, entry.ref, stats.new_percent, cid)

:ok =
ClientProxy.report_upload_progress(
proxy_pid,
from,
element,
entry.ref,
stats.new_percent,
cid
)

update_entry_start(state, entry, stats.new_start)
end

defp do_chunk(state, from, entry, proxy_pid, element, percent) do
stats = progress_stats(entry, percent)

chunk =
if stats.start + stats.chunk_size > entry.size do
:binary.part(entry.content, stats.start, entry.size - stats.start)
Expand All @@ -165,28 +195,53 @@ defmodule Phoenix.LiveViewTest.UploadClient do

receive do
%Phoenix.Socket.Reply{ref: ^ref, status: :ok} ->
:ok = ClientProxy.report_upload_progress(proxy_pid, from, element, entry.ref, stats.new_percent, state.cid)
:ok =
ClientProxy.report_upload_progress(
proxy_pid,
from,
element,
entry.ref,
stats.new_percent,
state.cid
)

update_entry_start(state, entry, stats.new_start)

%Phoenix.Socket.Reply{ref: ^ref, status: :error} ->
:ok = ClientProxy.report_upload_progress(proxy_pid, from, element, entry.ref, %{"error" => "failure"}, state.cid)
:ok =
ClientProxy.report_upload_progress(
proxy_pid,
from,
element,
entry.ref,
%{"error" => "failure"},
state.cid
)

update_entry_start(state, entry, stats.new_start)
after
get_chunk_timeout(state) -> exit(:timeout)
end
end

defp update_entry_start(state, entry, new_start) do
new_entries = Map.update!(state.entries, entry.name, fn entry -> %{entry | chunk_start: new_start} end)
new_entries =
Map.update!(state.entries, entry.name, fn entry -> %{entry | chunk_start: new_start} end)

%{state | entries: new_entries}
end

defp get_entry!(state, name) do
case Map.fetch(state.entries, name) do
{:ok, entry} -> entry
:error -> raise "no file input with name \"#{name}\" found in #{inspect(state.entries)}"
:error -> raise_unknown_entry!(state, name)
end
end

defp raise_unknown_entry!(state, name) do
raise "no file input with name \"#{name}\" found in #{inspect(state.entries)}"
end

defp get_chunk_timeout(state) do
state.socket.assigns[:chunk_timeout] || 10_000
end
Expand Down
43 changes: 31 additions & 12 deletions lib/phoenix_live_view/upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ defmodule Phoenix.LiveView.Upload do
%{} = client_config_meta
) do
reply_entries =
for entry <- entries, into: %{} do
for entry <- entries, entry.valid?, into: %{} do
token =
Phoenix.LiveView.Static.sign_token(socket.endpoint, %{
pid: self(),
Expand All @@ -382,25 +382,44 @@ defmodule Phoenix.LiveView.Upload do
{entry.ref, token}
end

{:ok, %{ref: conf.ref, config: client_config_meta, entries: reply_entries}, socket}
errors =
for entry <- entries, not entry.valid?, into: %{}, do: {entry.ref, entry_errors(conf, entry)}

reply = %{ref: conf.ref, config: client_config_meta, entries: reply_entries, errors: errors}
{:ok, reply, socket}
end

defp entry_errors(%UploadConfig{} = conf, %UploadEntry{} = entry) do
for {ref, err} <- conf.errors, ref == entry.ref, do: err
end

defp external_preflight(%Socket{} = socket, %UploadConfig{} = conf, entries, client_config_meta) do
reply_entries =
Enum.reduce_while(entries, {:ok, %{}, socket}, fn entry, {:ok, metas, acc} ->
case conf.external.(entry, acc) do
{:ok, %{} = meta, new_socket} ->
new_socket = update_upload_entry_meta(new_socket, conf.name, entry, meta)
{:cont, {:ok, Map.put(metas, entry.ref, meta), new_socket}}

{:error, %{} = meta, new_socket} ->
{:halt, {:error, {entry.ref, meta}, new_socket}}
Enum.reduce_while(entries, {:ok, %{}, %{}, socket}, fn entry, {:ok, metas, errors, acc} ->
if conf.auto_upload? and not entry.valid? do
new_errors = Map.put(errors, entry.ref, entry.errors)
{:cont, {:ok, metas, new_errors, acc}}
else
case conf.external.(entry, acc) do
{:ok, %{} = meta, new_socket} ->
new_socket = update_upload_entry_meta(new_socket, conf.name, entry, meta)
{:cont, {:ok, Map.put(metas, entry.ref, meta), errors, new_socket}}

{:error, %{} = meta, new_socket} ->
if conf.auto_upload? do
new_errors = Map.put(errors, entry.ref, [meta])
{:cont, {:ok, metas, new_errors, new_socket}}
else
{:halt, {:error, {entry.ref, meta}, new_socket}}
end
end
end
end)

case reply_entries do
{:ok, entry_metas, new_socket} ->
{:ok, %{ref: conf.ref, config: client_config_meta, entries: entry_metas}, new_socket}
{:ok, entry_metas, errors, new_socket} ->
reply = %{ref: conf.ref, config: client_config_meta, entries: entry_metas, errors: errors}
{:ok, reply, new_socket}

{:error, {entry_ref, meta_reason}, new_socket} ->
new_socket = put_upload_error(new_socket, conf.name, entry_ref, meta_reason)
Expand Down
Loading