Files the browser sends straight to where the app keeps them, such as a bucket taking presigned PUTs: their bytes never pass through the app.
An island takes files with use-uploads, which keeps their list and renders
the island again as it changes:
(let [{:keys [uploads start]}
(use-uploads :attachments
{:target (fn [{:keys [id name type size]} uploads]
(if (< (count (remove (comp #{:refused :failed} :state) uploads)) 10)
(let [key (str "uploads/" id)]
{:ref key :url (presign-put key type) :headers {"Content-Type" type}})
{:refused "At most ten files."}))})]
[:div
[:input {:type "file" :multiple true :data-on:change (start "evt.target.files")}]
(for [{:keys [id name state reason]} uploads]
...)])
How a file goes:
start's expression is given files, from a file input or a drop. The
browser posts their names, types and sizes, not their bytes.:target is called for each file, in order, with the list as it stands,
the pick's earlier files included: one pick of the island at a time, so a
limit holds. It returns where the bytes go, {:ref :url :method :headers}
(:method PUT unless given), or {:refused reason}. The file joins the
list either way, so the island shows it at once. :ref is the app's own
reference, such as the storage key, and stays on the server: the browser
knows the upload by its id.:url, at most three files at a time, its
progress in a signal (progress).:arrived, if
given, which can check it, as with a HEAD, and refuse it with
{:refused reason}. One that failed gets the browser's reason.take!, as a message being sent takes its files, or, once none is on
its way, together to :settled, as a project takes in a pick as one
batch.An upload is a map: :id, what the browser declared (:name, :type,
:size), :ref, and its :state:
:uploading its bytes are on their way.:arrived the browser reported them sent, and :arrived took them.:refused :target or :arrived refused it, with its :reason.:failed sending failed, or :arrived or :settled threw, with a
:reason in English, for the log or the island.The list lives as long as the island. An island that unmounts, or a session that closes, forgets it, and reports for it are refused, as is a report naming an id the island didn't give, such as one of another session: its token reaches its own list only.
What is left to the app:
:arrived.connect-src allowing the store's origin.Files the browser sends straight to where the app keeps them, such as a
bucket taking presigned PUTs: their bytes never pass through the app.
An island takes files with `use-uploads`, which keeps their list and renders
the island again as it changes:
(let [{:keys [uploads start]}
(use-uploads :attachments
{:target (fn [{:keys [id name type size]} uploads]
(if (< (count (remove (comp #{:refused :failed} :state) uploads)) 10)
(let [key (str "uploads/" id)]
{:ref key :url (presign-put key type) :headers {"Content-Type" type}})
{:refused "At most ten files."}))})]
[:div
[:input {:type "file" :multiple true :data-on:change (start "evt.target.files")}]
(for [{:keys [id name state reason]} uploads]
...)])
How a file goes:
1. `start`'s expression is given files, from a file input or a drop. The
browser posts their names, types and sizes, not their bytes.
2. `:target` is called for each file, in order, with the list as it stands,
the pick's earlier files included: one pick of the island at a time, so a
limit holds. It returns where the bytes go, `{:ref :url :method :headers}`
(`:method` PUT unless given), or `{:refused reason}`. The file joins the
list either way, so the island shows it at once. `:ref` is the app's own
reference, such as the storage key, and stays on the server: the browser
knows the upload by its id.
3. The browser sends the bytes to `:url`, at most three files at a time, its
progress in a signal (`progress`).
4. It reports how it went. An upload that arrived is passed to `:arrived`, if
given, which can check it, as with a HEAD, and refuse it with
`{:refused reason}`. One that failed gets the browser's reason.
5. Those that arrived leave the list when the island takes them with
`take!`, as a message being sent takes its files, or, once none is on
its way, together to `:settled`, as a project takes in a pick as one
batch.
An upload is a map: `:id`, what the browser declared (`:name`, `:type`,
`:size`), `:ref`, and its `:state`:
- `:uploading` its bytes are on their way.
- `:arrived` the browser reported them sent, and `:arrived` took them.
- `:refused` `:target` or `:arrived` refused it, with its `:reason`.
- `:failed` sending failed, or `:arrived` or `:settled` threw, with a
`:reason` in English, for the log or the island.
The list lives as long as the island. An island that unmounts, or a session
that closes, forgets it, and reports for it are refused, as is a report
naming an id the island didn't give, such as one of another session: its
token reaches its own list only.
What is left to the app:
- What the browser declares, and its report that a file arrived, are
untrusted. Sign the size and type into the target, or check the object in
`:arrived`.
- An object whose arrival is never reported, because the tab closed or the
island unmounted, or that is removed or refused once it arrived, stays
where it went. Upload under a prefix the store expires, and keep what the
app takes.
- The target must accept the browser's request: a store on another origin
with CORS allowing the page's origin, the method and the headers, and a
page whose CSP restricts `connect-src` allowing the store's origin.(drop-area start)(drop-area attrs start)Attributes that make an element a drop area for start, the :start that
use-uploads returns, added to the element's own attrs. Files dragged
over it are the area's, and a drop starts uploading them; over an area
inside it, they are the inner one's alone. A drag of text or a link isn't
the area's, and goes on as if it weren't there.
[:div (upload/drop-area {:class [:relative]} start)
content
[:div (ui/attr {:class [:absolute :inset-0]} :hidden true (str "!" (upload/dropping start)))
"Drop files to add them"]]
The browser fires dragover again and again at the element under the
pointer while files are dragged: the innermost area around it takes the
drag there, and keeps it from the areas around that. Leaving the area, or
the window, or calling the drag off, lets it go. A drop outside every area
is refused by the page's body (co.multiply.tropical.ui/page-attrs), so
the browser doesn't open the file in the tab.
An attribute of the element's own that one of these would replace throws.
Two areas for one start are one area to dropping: dragging over either
shows both.
Attributes that make an element a drop area for `start`, the `:start` that
`use-uploads` returns, added to the element's own `attrs`. Files dragged
over it are the area's, and a drop starts uploading them; over an area
inside it, they are the inner one's alone. A drag of text or a link isn't
the area's, and goes on as if it weren't there.
[:div (upload/drop-area {:class [:relative]} start)
content
[:div (ui/attr {:class [:absolute :inset-0]} :hidden true (str "!" (upload/dropping start)))
"Drop files to add them"]]
The browser fires `dragover` again and again at the element under the
pointer while files are dragged: the innermost area around it takes the
drag there, and keeps it from the areas around that. Leaving the area, or
the window, or calling the drag off, lets it go. A drop outside every area
is refused by the page's body (`co.multiply.tropical.ui/page-attrs`), so
the browser doesn't open the file in the tab.
An attribute of the element's own that one of these would replace throws.
Two areas for one `start` are one area to `dropping`: dragging over either
shows both.(drop-guard)Attributes for the page's body, part of co.multiply.tropical.ui/page-attrs:
files dragged anywhere outside a drop area are refused, so a drop there
does nothing, where the browser would open the file in the tab, and no area
shows them. A file input still takes files dropped on it.
Attributes for the page's body, part of `co.multiply.tropical.ui/page-attrs`: files dragged anywhere outside a drop area are refused, so a drop there does nothing, where the browser would open the file in the tab, and no area shows them. A file input still takes files dropped on it.
(dropping start)A JavaScript expression, true while files are dragged over the drop area for
start (drop-area), for the app's own overlay.
A JavaScript expression, true while files are dragged over the drop area for `start` (`drop-area`), for the app's own overlay.
(progress id)A JavaScript expression for how much of the upload id the browser has
sent, from 0 to 1, for an expression such as a bar's width:
(str "(100 * " (progress id) ") + '%'"). It reads the upload's signal,
_upload.<id>, which holds {loaded, total} in bytes, and which the island
declares while the upload is :uploading.
A JavaScript expression for how much of the upload `id` the browser has
sent, from 0 to 1, for an expression such as a bar's width:
`(str "(100 * " (progress id) ") + '%'")`. It reads the upload's signal,
`_upload.<id>`, which holds `{loaded, total}` in bytes, and which the island
declares while the upload is `:uploading`.(use-uploads k {:keys [target arrived settled]})Files the browser sends straight to the targets that :target names, kept
under k in this island (see the namespace docstring). The island renders
again as the list changes. Returns a map:
:uploads the list, in the order the files were given.:start (start files): the expression that uploads files, a
JavaScript expression for a FileList or an array of Files, such
as evt.target.files in an input's change, or
evt.dataTransfer.files in a drop. It reads them as it runs,
so the input can be cleared after it.:remove! (remove! id): drops an upload from the list. One on its way
still arrives where it went, and its report is refused. The
last one on its way ends the batch, as its report would.:take! (take!): removes the uploads that arrived and returns them,
at once, as a message being sent takes its files.Options:
:target (fn [file uploads] ...): file is {:id :name :type :size},
and uploads the list as it stands. Returns
{:ref :url :method :headers} or {:refused reason}.:arrived optional, (fn [upload] ...): called once the browser reports
the upload's bytes sent. Returns {:refused reason} to refuse
it, anything else to take it.:settled optional, (fn [arrived] ...): called once nothing is
uploading, with the uploads that arrived, which it takes, as
take! does: a pick taken in whole, with any picked while it
was on its way. One refused or failed stays in the list, and
doesn't hold back the rest. It isn't called when none arrived,
and if it throws, those it was given fail.Each runs on the thread of the request it answers, :settled on that of
the report or the action that ended the last upload, one at a time per
island and k: a report waits for the one before, so :settled starts
long work rather than doing it. They are replaced by each render's, as an
action's handler is.
Files the browser sends straight to the targets that `:target` names, kept
under `k` in this island (see the namespace docstring). The island renders
again as the list changes. Returns a map:
- `:uploads` the list, in the order the files were given.
- `:start` `(start files)`: the expression that uploads `files`, a
JavaScript expression for a FileList or an array of Files, such
as `evt.target.files` in an input's `change`, or
`evt.dataTransfer.files` in a `drop`. It reads them as it runs,
so the input can be cleared after it.
- `:remove!` `(remove! id)`: drops an upload from the list. One on its way
still arrives where it went, and its report is refused. The
last one on its way ends the batch, as its report would.
- `:take!` `(take!)`: removes the uploads that arrived and returns them,
at once, as a message being sent takes its files.
Options:
- `:target` `(fn [file uploads] ...)`: `file` is `{:id :name :type :size}`,
and `uploads` the list as it stands. Returns
`{:ref :url :method :headers}` or `{:refused reason}`.
- `:arrived` optional, `(fn [upload] ...)`: called once the browser reports
the upload's bytes sent. Returns `{:refused reason}` to refuse
it, anything else to take it.
- `:settled` optional, `(fn [arrived] ...)`: called once nothing is
uploading, with the uploads that arrived, which it takes, as
`take!` does: a pick taken in whole, with any picked while it
was on its way. One refused or failed stays in the list, and
doesn't hold back the rest. It isn't called when none arrived,
and if it throws, those it was given fail.
Each runs on the thread of the request it answers, `:settled` on that of
the report or the action that ended the last upload, one at a time per
island and `k`: a report waits for the one before, so `:settled` starts
long work rather than doing it. They are replaced by each render's, as an
action's handler is.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |