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.

WhatDefaultIf exceeded
Request body size10 MiB413
Stall between chunks30 s408

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

python
import mimetypes
from pathlib import Path
 
import stario.responses as responses
from 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.

Structuring apps. Testing with TestClient.