Where an upload's bytes wait between "the server has them" and "they are stored", so a failure in between is something a person can see and act on.
The media browser used to copy a finished transfer to a temp path, try to store it, and delete the temp file either way. A failed store became one anonymous count in a flash ("2 failed") and the bytes were gone; a raise took the LiveView down with every queued file in it; a page refresh while files were queued lost them without a word. Someone who dropped ten pictures and found six had no way to learn which four, or to try again.
Now a received upload is written here first and removed only once it is
stored. One that fails stays, marked with why; one whose processing never
finished (the LiveView died, the page was refreshed) stays too, and reads
as interrupted once it is old enough that nothing can still be working on
it. list/1 is what the browser's "Upload problems" panel shows, and it
survives a refresh because it lives on disk rather than in a socket.
One directory per user (<root>/<user_uuid>/), holding each upload's bytes
at <id> and what is known about it at <id>.json. The root defaults to
the system temp dir — this is a waiting room, not storage — and can be
moved with config :phoenix_kit, :upload_inbox_dir, "/path". Anything
older than a week is swept on the next listing, and all users' inboxes are
swept at most once an hour when an upload arrives.
Where an upload belongs
An item remembers the destination it was dropped on (library_uuid and
folder_uuid, recorded by the first browser that claims it) and who is
working on it (owner, the LiveView's pid). A retry goes to the recorded
destination, never to whichever browser the click happened in — a private
library's file must not land in Media — and a panel lists only the items of
its own library. An item whose owner is still alive is live, whatever its
age; one whose owner is gone reads as interrupted at once. Retry and
Discard from a panel that has gone stale go through claim/4 and
discard/2, which refuse an item another process is working on.
Persistence
The default root is node-local temporary storage: a multi-node deployment,
or a container with an ephemeral filesystem, loses "survives a refresh"
when the reconnect lands elsewhere or the host restarts. Point
:upload_inbox_dir at storage every node shares if that matters. Directories
are created 0700. There is no per-user quota yet.
Summary
Functions
Takes an item to work on, for the calling process: it reads as live while
this process is, and dest (%{library_uuid: _, folder_uuid: _}) becomes
its destination unless it already has one. {:ok, item} carries the item as
it now stands — a retry stores it at item.dest, not where the click was.
Removes an item — it was stored, or the user discarded it.
Removes an item the user let go of. {:error, :busy} — and nothing removed —
while another live process is working on it: a panel that went stale must not
delete bytes a second tab is storing.
Marks an item failed, keeping its bytes for a retry.
An item as it stands, or nil when it is gone. Its dest is where a retry
must put it.
The items that need the user's attention, oldest first: failed ones, and
received ones old enough that their processing was clearly interrupted
(those come back with status "interrupted"). Sweeps week-old items.
The inbox path of path's item as {user_uuid, id}, or nil if path is not in an inbox.
The bytes of an item, when it is still here.
Moves src into user_uuid's inbox as a received upload and returns the
item. meta carries the client's :client_name, :client_type and
:client_size. {:error, reason} when the user is not a uuid or the disk
refuses — the caller then falls back to its own temp file, which is still
where it was: a failure here never costs the bytes.
The inbox root.
Marks an item as being worked on again (a retry), so it reads as live, not interrupted.
Whether an upload is in somebody's hands right now: received, and either
claimed by a live process or too young to call abandoned. A page uses it to
keep looking while that is so (see list/1).
Types
@type item() :: %{ id: String.t(), client_name: String.t(), client_type: String.t() | nil, client_size: non_neg_integer() | nil, status: String.t(), error: String.t() | nil, received_at: integer(), dest: %{library_uuid: String.t() | nil, folder_uuid: String.t() | nil} | nil, owner: String.t() | nil }
Functions
Takes an item to work on, for the calling process: it reads as live while
this process is, and dest (%{library_uuid: _, folder_uuid: _}) becomes
its destination unless it already has one. {:ok, item} carries the item as
it now stands — a retry stores it at item.dest, not where the click was.
{:error, :busy} when another live process holds it (a stale panel retried
an item a second tab has already taken), {:error, :not_found} when its
bytes are gone.
Removes an item — it was stored, or the user discarded it.
Removes an item the user let go of. {:error, :busy} — and nothing removed —
while another live process is working on it: a panel that went stale must not
delete bytes a second tab is storing.
Marks an item failed, keeping its bytes for a retry.
An item as it stands, or nil when it is gone. Its dest is where a retry
must put it.
The items that need the user's attention, oldest first: failed ones, and
received ones old enough that their processing was clearly interrupted
(those come back with status "interrupted"). Sweeps week-old items.
The inbox path of path's item as {user_uuid, id}, or nil if path is not in an inbox.
The bytes of an item, when it is still here.
Moves src into user_uuid's inbox as a received upload and returns the
item. meta carries the client's :client_name, :client_type and
:client_size. {:error, reason} when the user is not a uuid or the disk
refuses — the caller then falls back to its own temp file, which is still
where it was: a failure here never costs the bytes.
copy: true leaves src in place (a second browser's own item).
The inbox root.
Marks an item as being worked on again (a retry), so it reads as live, not interrupted.
Whether an upload is in somebody's hands right now: received, and either
claimed by a live process or too young to call abandoned. A page uses it to
keep looking while that is so (see list/1).