Files and assets

Assets fingerprints a shipped tree; Files serves a live one.

Assets and Files each expose a directory on disk at a URL prefix. Use them for any tree you serve over HTTP: shipped CSS and JavaScript, images, uploads, generated files.

Build the object at import time and call href() there; call await attach(app) in bootstrap.

from stario import App, Assets, Files, Span
 
STATIC = Assets("./static", "/static")
UPLOADS = Files("./uploads", "/data")
STYLE_CSS = STATIC.href("css/app.css")
 
 
async def bootstrap(app: App, span: Span):
    span.attrs(await STATIC.attach(app))
    await UPLOADS.attach(app)
    yield

Which class#

Assets is for files you ship with the app. href() hashes the file and pins that digest; the public URL includes the digest. A request for the logical path receives 307 to the hashed URL. The default cache policy is immutable for one year. The directory must exist when you construct Assets.

Files is for a live tree. href() checks that the path exists and returns the same relative path under the prefix (default /data). GET opens the current file; a new file on disk is served without another load(). Do not use Assets for visitor uploads: a pin is a snapshot.

For a directory that holds visitor bytes, Files(root, "/media", untrusted=True) adds Content-Security-Policy: sandbox to every served response, and Content-Disposition: attachment to active types — HTML, XML, SVG, and JavaScript — so the browser downloads a file instead of executing it in the site’s origin.

Import either class from stario or stario.filesystem.

Construction#

from pathlib import Path
 
from stario import Assets, Files
 
ASSETS = Assets(Path("static"), "/static")
MEDIA = Files(Path("uploads"), "/media")

url_prefix is a string and must not contain placeholders. A host on the prefix is valid for href() — CDN URLs. attach() and register() require an app-relative prefix such as /static or /data.

Hidden path segments (names starting with .) are skipped unless include_hidden=True. Symlinks are skipped unless follow_symlinks=True; followed symlinks must stay inside the root.

Pass content_types={".webmanifest": "application/manifest+json"} to add or override MIME types. Keys are suffixes that start with a dot.

href#

href(path) takes a relative POSIX path (css/app.css). It rejects .., a leading slash, backslashes, and empty segments. Optional query and fragment arguments append to the URL.

For Assets, the first href() of a path hashes the file (xxHash64) and pins size and mtime; a later href() of the same path returns the pin. If the file is missing, href() raises StarioError. If the file changes while hashing, it raises StarioRuntime.

For Files, href() resolves the path on each call. A missing file raises StarioError.

attach, register, load#

await attach(app) registers GET under {prefix}/{path...}, then loads the tree. The GET route also answers HEAD, as every GET route does (HEAD). It returns the same stats mapping as load().

register(app) and await load() stay available when you must split the work: tests, a host-only prefix for href(), or a walk without routes. attach() is the bootstrap call.

load() walks the directory once. It holds files that fit max_file_size (default 1 MiB) and the remaining max_bytes budget (default 64 MiB); larger files stream from disk. Assets also hashes files that were not passed to href(). You can load only once.

After attach():

  • Files GET is live — a file written later is served.

  • The Assets catalog is a snapshot. GET of an unpinned new file is 404; href() after attach still pins that file and serves it.

cache_control= on attach() or register() overrides the class default (public, max-age=31536000, immutable for Assets; private, no-cache for Files).

HTTP#

Both classes send X-Content-Type-Options: nosniff, a strong ETag, Last-Modified, and Accept-Ranges: bytes. Conditional GET returns 304; If-None-Match takes precedence over If-Modified-Since.

Assets ETags use the content digest, distinct per content-coding. Files ETags use device, inode, mtime, and size.

A satisfiable Range: bytes=… returns 206 of the identity bytes, not a compressed representation. Unsatisfiable ranges return 416. If-Range is a strong comparison.

Logical Assets paths without a digest receive 307 to the hashed URL, so HTML should call href() and embed the final URL. If a pinned Assets file changes on disk after the pin, a streamed GET of the hashed URL is 404; a cached pin keeps serving the bytes captured at pin time.

Pre-compression#

Assets precompresses by default (br, zstd, gzip). Files does not unless you pass precompress=. Pass precompress=() to disable, or a subset such as ("br", "gzip").

Variants are stored only when they are smaller than the identity body and the type is compressible. image/svg+xml is compressible; other image/, audio/, video/, WOFF, and archive types are not. Cached replies negotiate Accept-Encoding and set Vary: Accept-Encoding when variants exist. Among ties Stario prefers brotli, then zstd, then gzip — the bytes are encoded once at load, so only wire size counts.

If the identity body fits the byte budget and a codec does not, the identity body stays in memory and that codec is skipped.

Pass compression=CompressionConfig(…) to tune levels and minimum size for this pass — defaults favor higher quality than live Writer compression. Streamed files are not precompressed.

stats#

After load() or attach(), stats is a mapping: files, cached_files, streamed_files, budget_skipped_files, retained_bytes, compressed_files. Attach it to a bootstrap span when you want the cost in traces.

class Assets(directory, url_prefix=None, *, include_hidden=False, follow_symlinks=False, content_types=None)#

Fingerprinted files. href() hashes and pins. GET uses the hashed URL.

async Assets.__call__(context, writer)#

Call self as a function.

Assets.href(path, /, *, query=None, fragment=None)#

class Files(directory, url_prefix=None, *, include_hidden=False, follow_symlinks=False, content_types=None, untrusted=False)#

Live files. href() builds the URL. GET uses ETag and Range.

href() does not read the disk: it checks the path shape and builds the URL, like Route.href. A path that is missing, a directory, or an uncontained symlink is a 404 on GET.

Pass untrusted=True for directories holding user uploads or other untrusted bytes: every served response then carries content-security-policy: sandbox, and active media types (HTML, XML, SVG, JavaScript) also get content-disposition: attachment so browsers download them instead of executing them in the site’s origin. The 404 for missing paths is left unchanged.

async Files.__call__(context, writer)#

Call self as a function.

Files.href(path, /, *, query=None, fragment=None)#