Packages
phoenix_live_view
0.20.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
lib/phoenix_live_view/test/upload_client.ex
defmodule Phoenix.LiveViewTest.UploadClient do
@moduledoc false
use GenServer
require Logger
require Phoenix.ChannelTest
alias Phoenix.LiveViewTest.{Upload, ClientProxy}
def child_spec(opts) do
%{
id: make_ref(),
start: {__MODULE__, :start_link, [opts]},
restart: :temporary
}
end
def channel_pids(%Upload{pid: pid}) do
GenServer.call(pid, :channel_pids)
end
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
GenServer.call(pid, {:chunk, name, percent, proxy_pid, element})
catch
:exit, {{:shutdown, :closed}, _} -> {:ok, :closed}
end
def simulate_attacker_chunk(%Upload{pid: pid}, name, chunk) do
GenServer.call(pid, {:simulate_attacker_chunk, name, chunk})
end
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
GenServer.start_link(__MODULE__, Keyword.merge(opts, caller: self()))
end
def init(opts) do
cid = Keyword.fetch!(opts, :cid)
socket = Keyword.get(opts, :socket)
socket = socket && put_in(socket.transport_pid, self())
{:ok, %{socket: socket, cid: cid, upload_ref: nil, config: %{}, entries: %{}}}
end
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, 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
pids = Enum.into(state.entries, %{}, fn {name, entry} -> {name, entry.socket.channel_pid} end)
{:reply, pids, state}
end
def handle_call({:chunk, entry_name, percent, proxy_pid, element}, from, state) do
{:noreply, chunk_upload(state, from, entry_name, percent, proxy_pid, element)}
end
def handle_call({:simulate_attacker_chunk, entry_name, chunk}, _from, state) do
Process.flag(:trap_exit, true)
entry = get_entry!(state, entry_name)
ref = Phoenix.ChannelTest.push(entry.socket, "chunk", {:binary, chunk})
receive do
%Phoenix.Socket.Reply{ref: ^ref, status: status, payload: payload} ->
{:stop, :normal, {status, payload}, state}
after
get_chunk_timeout(state) -> exit(:timeout)
end
end
defp build_and_join_entry(%{socket: nil} = _state, client_entry, token) do
%{
"name" => name,
"content" => content,
"size" => _,
"type" => type,
"ref" => ref
} = client_entry
%{
name: name,
content: content,
size: byte_size(content),
type: type,
ref: ref,
token: token,
chunk_percent: 0
}
|> with_chunk_boundaries()
end
defp build_and_join_entry(state, client_entry, token) do
%{
"name" => name,
"content" => content,
"size" => _,
"type" => type,
"ref" => ref
} = client_entry
{:ok, _resp, entry_socket} =
Phoenix.ChannelTest.subscribe_and_join(state.socket, "lvu:123", %{"token" => token})
%{
name: name,
content: content,
size: byte_size(content),
type: type,
socket: entry_socket,
ref: ref,
token: token,
chunk_percent: 0
}
|> with_chunk_boundaries()
end
def with_chunk_boundaries(entry) do
{boundaries, _} =
Enum.map_reduce(99..1//-1, {100, entry.size}, fn
x, {prev_perc, prev_bytes} ->
bytes = ceil(entry.size * x / 100)
if bytes == prev_bytes do
{{x, {prev_perc, prev_bytes}}, {prev_perc, prev_bytes}}
else
{{x, bytes}, {x, bytes}}
end
end)
Map.put(
entry,
:chunk_boundaries,
boundaries |> Map.new() |> Map.merge(%{0 => 0, 100 => entry.size})
)
end
defp progress_stats(entry, percent) when percent in 0..100 do
start =
case Map.fetch!(entry.chunk_boundaries, entry.chunk_percent) do
bytes when is_integer(bytes) -> bytes
end
new_start =
case Map.fetch!(entry.chunk_boundaries, entry.chunk_percent + percent) do
{result_percent, bytes} ->
Logger.warning(
"Filesize cannot be chunked to #{percent}%. #{result_percent - entry.chunk_percent}% will be uploaded."
)
bytes
bytes ->
bytes
end
chunk_size = new_start - start
new_percent = trunc(new_start / entry.size * 100)
%{
chunk_size: chunk_size,
start: start,
new_start: new_start,
new_percent: new_percent
}
end
defp chunk_upload(state, from, entry_name, percent, proxy_pid, element) do
entry = get_entry!(state, entry_name)
if entry.chunk_percent >= 100 do
state
else
do_chunk(state, from, entry, proxy_pid, element, percent)
end
end
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
)
update_entry_percent(state, entry, stats.new_percent)
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)
else
:binary.part(entry.content, stats.start, stats.chunk_size)
end
ref = Phoenix.ChannelTest.push(entry.socket, "chunk", {:binary, chunk})
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
)
update_entry_percent(state, entry, stats.new_percent)
%Phoenix.Socket.Reply{ref: ^ref, status: :error} ->
:ok =
ClientProxy.report_upload_progress(
proxy_pid,
from,
element,
entry.ref,
%{"error" => "failure"},
state.cid
)
update_entry_percent(state, entry, stats.new_percent)
after
get_chunk_timeout(state) -> exit(:timeout)
end
end
defp update_entry_percent(state, entry, new_percent) do
new_entries =
Map.update!(state.entries, entry.name, fn entry -> %{entry | chunk_percent: new_percent} end)
%{state | entries: new_entries}
end
defp get_entry!(state, name) do
case Map.fetch(state.entries, name) do
{:ok, entry} -> entry
: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
def handle_info(:garbage_collect, state) do
{:noreply, state}
end
end