Packages
phoenix_live_view
0.15.5
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 Phoenix.ChannelTest
alias Phoenix.LiveViewTest.{Upload, ClientProxy}
def channel_pids(%Upload{pid: pid}) do
GenServer.call(pid, :channel_pids)
end
def allow_acknowledged?(%Upload{pid: pid}) do
GenServer.call(pid, :allow_acknowledged)
end
def chunk(%Upload{pid: pid, element: element}, name, percent, proxy_pid) do
GenServer.call(pid, {:chunk, name, percent, proxy_pid, element})
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, entries_resp) do
GenServer.call(pid, {:allowed_ack, ref, config, entries, entries_resp})
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 =
case Keyword.fetch(opts, :socket_builder) do
{:ok, func} ->
{:ok, socket} = func.()
socket
:error ->
nil
end
{: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}
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}}
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
ref = make_ref()
new_state = chunk_upload(state, {self(), ref}, entry_name, percent, proxy_pid, element)
# We need to wait for ClientProxy to sync the
# :upload_progress before replying to the caller,
# otherwise the proxy's reply will race our own.
receive do
{^ref, {:ok, _}} ->
{:reply, :ok, new_state}
after
1000 -> exit(:timeout)
end
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
1000 -> 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_start: 0,
}
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_start: 0,
}
end
defp progress_stats(entry, percent) do
chunk_size = trunc(entry.size * (percent / 100))
start = entry.chunk_start
new_start = start + chunk_size
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_start >= entry.size 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_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)
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_start(state, entry, stats.new_start)
after
1000 -> 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)
%{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)}"
end
end
def handle_info(:garbage_collect, state) do
{:noreply, state}
end
end