User uploads and storage
StaticAssets is for build-time files you ship with the app. User uploads are visitor-supplied bytes. Stario does not ship an upload class. This page is handler-level read and write.
Prefer object storage for larger files
If uploads are large or frequent, do not route every byte through Python. Issue a signed URL. The browser puts the object in the bucket. The app stores the key after the upload finishes.
If you store files locally
Do not use the client filename as the on-disk name. Use your own scheme (UUIDs, content hashes, per-user directories) and a database row for metadata and authorization.
A download handler reads the bytes, sets Content-Type, and responds. Content-Disposition: inline renders in the page. attachment prompts a download.
Server limits
Request.body() and Request.stream() count against the same cap. body() buffers the whole request. stream() reads in chunks. Raise caps with STARIO_REQUESTS_MAX_BODY_BYTES. Defaults: Deployment.
| What | Default | If exceeded |
|---|---|---|
| Request body size | 10 MiB | 413 |
| Stall between chunks | 30 s | 408 |
Per-route tighter caps apply only to buffered reads: await c.req.body(max_size=65536). If the client disconnects while you read, both paths raise ClientDisconnected. Delete partial files or make finalize steps idempotent.
Do not call body() after you have begun iterating stream(), or the other way around.
Minimal handlers
import mimetypesfrom pathlib import Path import stario.responses as responsesfrom stario import Context, Writer UPLOAD_DIR = Path("var/uploads") async def upload_raw_body(c: Context, w: Writer) -> None: dest = UPLOAD_DIR / "myfile.bin" dest.write_bytes(await c.req.body()) responses.text(w, "ok") async def download_stored_file(c: Context, w: Writer) -> None: path = UPLOAD_DIR / "myfile.bin" if not path.is_file(): responses.text(w, "Not found", status=404) return mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream" w.respond(path.read_bytes(), mime.encode("ascii"))For a large upload, write each chunk from c.req.stream() to disk. For a large download, set Content-Length, call w.write_headers(200), then w.write chunks and w.end().
These examples use a fixed filename. They are not safe for concurrent users.
Multipart and Datastar file signals
Classic forms send multipart/form-data. Parse parts with a library you choose. Datastar-driven UIs often send files as signals instead. See Reading and writing signals.
Never trust client-supplied paths or filenames for server-side opens. Map opaque ids to paths under one vetted root. Reject ... Cap sizes. Restrict MIME types before you write to disk or return a file to another user.