Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

upload

Upload(onUploaded: Ingest) { …your own content… } · Upload(accept: "image/*", onUploaded: Ingest) · Upload(privately: true, multiple: true, onUploaded: Keep) · Stack(onFile: Keep) { … } — a file picker, or a region a file is pasted or dropped into, whose action receives an UploadedFile

`Upload(onUploaded: Ingest) { … }` is a file picker wearing whatever you put inside it. When someone chooses a file its bytes are stored and your action receives an `UploadedFile` — the path they landed at, plus the name, type and size the browser reported. The bytes never pass through your code.

previewuifilesstorageforms

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:

fieldwhat it is
Pathwhere the bytes were stored. This is the value you keep.
FileNamethe original name the browser reported
ContentTypethe type the browser reported
Lengththe 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 it

Description#

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 file

A 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 saysadmitsrefuses
application/pdf or .pdfa file that begins %PDF-cert.pdf.html holding a <script> — it is a web page
image/*PNG, JPEG, GIF, WebP, AVIF, HEICan SVG, which is a document that can carry script — name image/svg+xml to take one
.md, text/plain, text/csv, application/jsonvalid texta file that begins with markup (<html>, <div>, <svg>)
.docx, .xlsx, .epub, application/zipa ZIP containeranything else
an extension it has no signature for (.stl, .glb)that name, unless the content is a known kinda 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#

Related

camera and microphone

Take a photograph, record a video, or record audio. `Camera.Start()` asks for the device and `Camera()` shows the…

Files (addressing something the app stores)

The platform stores a file two ways, and which one you hold decides how you address it. A PATH-keyed file lives at an…

File.IsOwnUpload and File.SaveUpload

A private upload's path records WHO uploaded it — the signed-in account, or the browser's visit when nobody was signed…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

textures

Drop `.png`, `.jpg` or `.webp` files into `model/textures/` and blit them onto a canvas with `Draw.Image(wall, …)`. The…