Static Files
The openviper.staticfiles package provides development-time static file
serving and a collectstatic utility for production builds.
Overview
In development (DEBUG=True), static files are served directly by the
framework via StaticFilesMiddleware. In
production (DEBUG=False), static files should be served by a reverse
proxy such as nginx; the middleware is not mounted.
Call static() and/or media() in routes.py to opt in to
framework-managed serving:
# routes.py
from openviper.staticfiles import static, media
route_paths = [
("/", main_router),
("/admin", get_admin_site()),
] + static() + media()
Key Classes & Functions
- class openviper.staticfiles.StaticFilesMiddleware(app, url_path='/static', directories=None, cache_max_age=3600, max_file_size=52428800)
ASGI middleware that intercepts requests whose path starts with url_path and serves the matching file from one of the directories.
app- the next ASGI application in the chain.url_path- URL prefix to intercept (default:"/static").directories- list of filesystem directories to search. Defaults to["static"].cache_max_age-Cache-Control: max-agevalue in seconds (default:3600).max_file_size- maximum file size in bytes; larger files receive a 413 response (default: 50 MiB).
Supports conditional requests (
If-None-Match,If-Modified-Since), byte-range requests (Range/If-Range), correct MIME type detection withX-Content-Type-Options: nosniff,Content-Length,ETag, andLast-Modifiedheaders.Security features include path traversal sanitisation, symlink rejection, and directory confinement checks.
- openviper.staticfiles.static() list[str]
Signal the framework to enable static file serving at
STATIC_URL. Returns an empty list so it can be appended safely toroute_paths.
- openviper.staticfiles.media() list[str]
Signal the framework to enable media file serving at
MEDIA_URL. Returns an empty list so it can be appended safely toroute_paths.
- openviper.staticfiles.is_static_enabled() bool
Return
Trueifstatic()has been called (i.e. the user opted in).
- openviper.staticfiles.collect_static(source_dirs, dest_dir, *, clear=False) int
Copy all static files from source_dirs (including per-app
static/directories discovered viadiscover_app_static_dirs()) into dest_dir. Returns the number of files collected.When clear is
Trueand the destination is not a symlink and does not overlap a source directory, the destination is deleted before collection begins. RaisesValueErrorif clear isTrueand the destination is a symlink.
- openviper.staticfiles.handlers.discover_app_static_dirs() tuple[Path, ...]
Discover
static/directories inside every installed app (includingopenviper.admin). Results are cached viafunctools.lru_cache.
- openviper.staticfiles.handlers.sanitize_relative_path(relative) str | None
Neutralise path traversal, encoded slashes, and null bytes in a relative path. Returns the cleaned path or
Noneif the path is unsafe.
- openviper.staticfiles.handlers.parse_range(range_header, file_size) tuple[int, int] | Literal['ignore', 'unsatisfiable']
Parse a
bytesRange header. Returns(start, end)for a satisfiable single range,"ignore"for multi-range or malformed input (serve 200), or"unsatisfiable"when the range is beyond EOF (respond 416).
- class openviper.staticfiles.handlers.NotModifiedResponse
A 304 Not Modified ASGI response carrying
ETagandLast-Modifiedheaders.
- class openviper.staticfiles.handlers.FileEntry
Bundles a resolved
Pathwith its pre-fetchedos.stat_result. Used internally byStaticFilesMiddlewareto avoid redundant stat calls.
Security
The static file serving pipeline enforces several security measures:
Path traversal sanitisation -
sanitize_relative_path()decodes percent-encoded sequences, rejects null bytes, encoded slashes (%2f,%5c), and any path component equal to...Symlink rejection -
StaticFilesMiddlewareskips files that are symlinks or have symlinked parent directories within the serving root.Directory confinement - resolved file paths are verified to remain inside the configured serving directory via
Path.relative_to().Zip-Slip protection -
copy_tree()rejects symlinks that resolve outside the source root and files whose resolved destination escapes the target directory.Symlink-destination guard -
collect_static()withclear=Truerefuses to delete a destination that is a symlink.X-Content-Type-Options - all responses include the
nosniffheader to prevent MIME-type sniffing.Method restriction - only
GETandHEADare served; other methods receive a 405 response.File size limit - files exceeding
max_file_sizereceive a 413 response.
Example Usage
See also
Working projects that serve static and media files:
examples/ai_smart_recipe_generator/ -
static()+media()route helpersexamples/ecommerce_clone/ - static assets and user-uploaded media
Development Static Serving
# routes.py
from openviper.routing.router import Router
from openviper.staticfiles import static, media
router = Router()
@router.get("/")
async def home(request): ...
route_paths = [
("/", router),
] + static() + media()
Now /static/app.js resolves to <project_root>/static/app.js and
/media/uploads/photo.jpg resolves to <MEDIA_ROOT>/uploads/photo.jpg
when DEBUG=True.
Direct Middleware Usage
Attach the middleware manually when you need custom URL paths or multiple static directories:
from openviper import OpenViper
from openviper.staticfiles import StaticFilesMiddleware
app = OpenViper()
app = StaticFilesMiddleware(
app,
url_path="/assets",
directories=["static", "frontend/dist"],
)
Collecting Static Files for Production
openviper viperctl collectstatic .
Or call it programmatically:
from openviper.staticfiles.handlers import collect_static
count = collect_static(
source_dirs=["static", "myapp/static"],
dest_dir="public/static",
)
print(f"Collected {count} files")
To clear the destination before collecting:
collect_static(
source_dirs=["static"],
dest_dir="public/static",
clear=True,
)
Configuration
@dataclasses.dataclass(frozen=True)
class MySettings(Settings):
STATIC_URL: str = "/static/"
STATIC_ROOT: str = "staticfiles"
MEDIA_URL: str = "/media/"
MEDIA_ROOT: str = "media"