Summary#
Upload is a file picker you supply the appearance of. Put anything inside it — a button, a row with an icon,
a whole drop zone — and it becomes the thing a person clicks to choose a file.
When they choose one, the bytes are stored and your action is called with an UploadedFile:
| field | what it is |
|---|---|
Path | where the bytes were stored. This is the value you keep. |
FileName | the original name the browser reported |
ContentType | the type the browser reported |
Length | the size in bytes, as the browser reported it |
The same value comes back from camera and microphone's Camera.Capture() and both StopRecording()s, so one
Ingest(UploadedFile f) serves a chosen file and a photograph alike.
Signature#
Upload(onUploaded: Ingest) { Text("Choose a file"); } // your own content is the button
Upload(accept: "image/*", onUploaded: Ingest) { … } // narrow what the picker offers
Upload(multiple: true, onUploaded: Ingest) { … } // several files; the action runs once per file
Stack(onFile: Ingest) { TextArea(value: draft); } // a file PASTED inside it or DROPPED on itDescription#
The content inside it IS the button#
There is no built-in appearance to override. Whatever you render inside the Upload is what a person sees and
clicks — so it takes your icons, your spacing and your variants like anything else.
[Composable] component UploadButton(Action<UploadedFile> onUploaded) {
variants {
base { Display = Display.InlineFlex; H = "32px"; Px = 3; Bg = Colors.Primary; Color = Colors.OnPrimary; Rounded = Radius.Md;
FontSize = FontSize.Body; FontWeight = FontWeight.Semibold; WhiteSpace = WhiteSpace.Nowrap; Transition = Motion.Fast; }
}
render {
Upload(onUploaded: onUploaded) {
Row(gap: 2, align: Align.Center, justify: Justify.Center) { Icon(Icons.Upload, size: 18); Text("Upload"); }
}
}
}The bytes never reach your code#
By the time your action runs, the file is already stored. You receive a path, not a buffer — so a 9 MB photograph costs your action nothing, and there is no way to accidentally hold one in a field.
What you do with the path is the interesting part: usually build a FileAsset from it, which is what gives the
file an owner, grants, deduplication and a quota. demo/file-manager is the worked version.
What may be uploaded#
Images, audio, video, PDF, ZIP (and EPUB), and a few text formats (text/plain, text/markdown, text/csv,
application/json). The limit is 10 MB per file; a larger one is refused with 413 and the sentence "File
exceeds the 10 MB limit", which is what the page shows, and nothing is stored.
A private upload (below) may be any type — a source file, a log, an .html a customer saved. It never lands
where anonymous pages fetch from, and a file whose type a browser would open as a document (HTML, SVG, XML,
JavaScript) is always served as a download (Content-Disposition: attachment), never rendered.
⛔ SVG is deliberately refused, even though it is an image. An SVG is a document that can carry script, so a stored one served back from your own origin is a cross-site-scripting vector; every other image format is inert. If you want vector art in your app, ship it as an asset instead — see textures for which folder each kind of art belongs in.
Uploading is an authorized act#
A public upload is accepted from a signed-in user only. A signed-out visitor may upload privately — a customer pasting a screenshot into a support widget — and the file their code then keeps is their visit's: they can read and attach it, and nobody else can (see [Attachment] — a file readable wherever its row is). Anything else with no session is refused, and the refusal arrives as an error your action can catch and report, not as a silent nothing.
A private upload's path also records who uploaded it — the signed-in person, or the browser's visit — so a
function that keeps it as the caller's file asks File.IsOwnUpload(f.Path) first, and a path that leaks cannot be
claimed by another session (File.IsOwnUpload and File.SaveUpload). Uploads are limited per browser (or person) and per network
address; past the limit the upload is refused with a sentence that says how long to wait, and nothing is stored.
This is the rule camera and microphone runs into, and for the same reason: a file that arrives from a browser has an owner.
A file nobody may fetch but the people you sign a link for#
An ordinary upload lands under public/, which anybody may fetch — right for a product photograph, wrong for a file
you SELL. privately: true stores it outside public/, at a path the platform chooses (uploads/<random>-<owner>.<ext>),
and it is served only through a link your code signs for somebody, File.SignedUrl:
Upload(accept: "application/pdf", privately: true, onUploaded: Keep) { Text("Sell as a file…"); }
action Keep(UploadedFile f) { SetDownloadFile(variant, f, ""); } // f.Path is "uploads/3f2a….pdf"
// …and later, for a buyer: File.SignedUrl(variant.DownloadPath) — a link to that one file, for fifteen minutes.The path is the platform's, never the caller's: a private upload that names a path is refused, so an app user can
never use it to overwrite a private file the app wrote itself (a receipt, a label). The Osysharp.Shop kit's digital
products are built on it.
Several files, and files that are pasted or dropped#
multiple: true lets the picker take several files; your action runs once for each, in the order they were chosen.
onFile: makes ANY element take files the way a message box should: a screenshot pasted while focus is inside
it, or a file dropped on it. Each file is uploaded privately and your action runs once per file with its
UploadedFile — the same value onUploaded hands over. A paste with no file in it (plain text) is left alone, so a
text box inside the region still pastes text.
Stack(onFile: Took) {
TextArea(value: draft, label: "Your message");
Upload("Attach files", privately: true, multiple: true, onUploaded: Took);
}
action Took(UploadedFile f) { pending.Add(KeepUpload(f)); } // the Conversations kit keeps it as the writer's fileA browser names every pasted screenshot image.png; the platform renames it pasted-<date>-<time>.png so a person
can tell three of them apart. In a test, Ui.Upload("Your message", "notes.txt", "…") drops the file on the region
the label sits in.
accept: is held by the server, against what the file IS#
accept: "application/pdf, image/*" does two things. It narrows what the browser's picker offers by default, and the
server holds it: the upload is refused unless the file's own content is one of the kinds the field names. The
picker's "All files" switch, a drag and drop, or a renamed file does not get past it.
The type is read from the bytes, never from the name or the browser's claim (both of which a rename sets):
| the field says | admits | refuses |
|---|---|---|
application/pdf or .pdf | a file that begins %PDF- | cert.pdf.html holding a <script> — it is a web page |
image/* | PNG, JPEG, GIF, WebP, AVIF, HEIC | an SVG, which is a document that can carry script — name image/svg+xml to take one |
.md, text/plain, text/csv, application/json | valid text | a file that begins with markup (<html>, <div>, <svg>) |
.docx, .xlsx, .epub, application/zip | a ZIP container | anything else |
an extension it has no signature for (.stl, .glb) | that name, unless the content is a known kind | a web page or an image renamed to it |
A refused file is not stored, and the answer is 415 with a sentence the page shows as it stands — "This file is a
web page, and this field accepts PDF documents and images." A field with no accept: takes whatever the route takes
(the list above for a public upload; any type for a private one).
⚠ It is the field's own rule, kept for the person who chose the wrong file — the way a column's [MaxLength] is. A
request written by hand can leave accept out, and is then held only to what the route always holds. So code that
must know what it was handed still asks the file: a private upload is any type, and is always served as a download.
See also#
- camera and microphone — the camera and microphone, which answer the same
UploadedFile - [Attachment] — a file readable wherever its row is — a file readable exactly where the row holding it is, and a visitor's own files
- Files (addressing something the app stores) — what to do with the path once you have it
- File.SignedUrl — the link that hands a private file to one person
- textures — art the app SHIPS, which is a different thing from a file a person uploads
- component — actions, and where an
onUploadedhandler lives