# upload

> `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.

<!-- id: ui-upload · area: ui · stability: preview · html: https://osysharp.com/reference/ui/upload/ -->

## Summary        {#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](https://osysharp.com/reference/ui/capture/)'s `Camera.Capture()` and both `StopRecording()`s, so one
`Ingest(UploadedFile f)` serves a chosen file and a photograph alike.

## Signature      {#signature}
```osy syntax
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    {#description}

### The content inside it IS the button   {#content}
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.

```osy title="a styled upload button" sample=file-manager/model/pages/manager.osy#UploadButton
[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   {#bytes}
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   {#types}
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](https://osysharp.com/reference/ui/textures/) for which folder each kind of
art belongs in.

### Uploading is an authorized act   {#authorization}
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](https://osysharp.com/reference/storage/attachment/)). 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](https://osysharp.com/reference/storage/own-upload/)). 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](https://osysharp.com/reference/ui/capture/) 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   {#private}
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](https://osysharp.com/reference/storage/file-signed-url/):

```osy syntax
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   {#on-file}
`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.

```osy syntax
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}
`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   {#see-also}
- [camera and microphone](https://osysharp.com/reference/ui/capture/) — the camera and microphone, which answer the same `UploadedFile`
- [[Attachment] — a file readable wherever its row is](https://osysharp.com/reference/storage/attachment/) — a file readable exactly where the row holding it is, and a visitor's own files
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — what to do with the path once you have it
- [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — the link that hands a private file to one person
- [textures](https://osysharp.com/reference/ui/textures/) — art the app SHIPS, which is a different thing from a file a person uploads
- [component](https://osysharp.com/reference/ui/component/) — actions, and where an `onUploaded` handler lives
