Storehouse.Live.consume/3 stores the files of a LiveView upload: each
entry that is done, once, checked by its bytes and stored under a new
key; and those the upload refused, cancelled. It gives a result for
each, so that the LiveView shows the file, or says why it is not there.
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:photos, [])
|> allow_upload(:photos,
accept: ~w(.png .jpg .jpeg .gif .webp),
max_file_size: 10_000_000,
max_entries: 10,
auto_upload: true,
progress: &handle_progress/3
)}
end
defp handle_progress(:photos, _entry, socket), do: {:noreply, store_photos(socket)}
# The form's phx-change: a file that fails validation (too large, a type
# that is not accepted) is cancelled here.
def handle_event("validate", _params, socket), do: {:noreply, store_photos(socket)}
defp store_photos(socket) do
{socket, results} =
Storehouse.Live.consume(socket, :photos,
accept: Storehouse.Check.images(),
max_pixels: 50_000_000
)
Enum.reduce(results, socket, fn
{_entry, {:ok, file}}, socket ->
update(socket, :photos, &[file | &1])
{entry, {:error, reason}}, socket ->
put_flash(socket, :error, "#{entry.client_name} was not added: #{inspect(reason)}")
end)
end<form id="photos" phx-change="validate">
<.live_file_input upload={@uploads.photos} />
</form>What is checked
The size limit and the accepted extensions are the upload's
(Phoenix.LiveView.allow_upload/3): the browser checks them first, and
the server again. Then Storehouse reads the first bytes of each file. It
keeps a content type only when the bytes prove it (Storehouse.Check);
the type the browser sends is not trusted. :accept refuses the checked
types you do not take: a page renamed .png is not an image, and gives
{:error, :not_accepted}.
Once each
LiveView drops a consumed entry a message after it is consumed. Two
calls in that time (the progress of the next file, a form change) would
consume it twice, and the second would crash the LiveView: consume/3
keeps the refs it consumed, and leaves those entries alone. Call it from
the progress callback, from the form's change handler, or both.
The results
Each result is {entry, {:ok, file}} or {entry, {:error, reason}}, in
the entries' order: the entry's ref matches what the browser has (a
placeholder of yours for the file, say). The reasons are the upload's
(:too_large, :not_accepted, :too_many_files), the check's
(:not_accepted for :accept, :too_large for :max_pixels) and the
store's own.
Direct and streamed uploads
Storehouse.Live.allow/3, in place of allow_upload/3, sends the files
straight from the browser to the store, when the store takes direct
uploads (Storehouse.S3, and Storehouse.Local in development), or, with
mode: :stream, through the LiveView and on to the store as they come,
with no file on the app's disk: consume/3 gives them stored and
checked. See Direct uploads, and Ecto
for files in records.
What happens to files, live
A photo's analysis may finish after the LiveView that uploaded it has
moved on, and other people may have the page open. Tell your app what
happens to files (Storehouse.Notify), and broadcast it on your PubSub,
on a topic of each account:
# config/config.exs
config :storehouse,
notify: {Storehouse.Notify.PubSub, :broadcast, [MyApp.PubSub, {MyApp.Files, :topic, []}]}defmodule MyApp.Files do
# Keys made with a prefix per account (`key:`): "accounts/42/…".
def topic(%{file: %{key: key}}), do: topic_of(key)
def topic(%{key: key}), do: topic_of(key)
def topic(_info), do: nil
defp topic_of("accounts/" <> rest), do: "files:" <> hd(String.split(rest, "/"))
defp topic_of(_key), do: nil
enddef mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "files:#{socket.assigns.account.id}")
{:ok, socket}
end
def handle_info({:storehouse, :analyzed, %{file: file}}, socket),
do: {:noreply, update_photo(socket, file)}
def handle_info({:storehouse, _event, _info}, socket), do: {:noreply, socket}:analyzed from Storehouse.analyze_later/3 comes once its :then has
the results, so a LiveView that reloads the record finds them saved. A
JavaScript client follows the same topic through a Phoenix Channel.