Spaces:
Paused
Paused
File size: 3,149 Bytes
a5784e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | """
Static files serving routes
Uses FastAPI/Starlette native static files service
Optimization points:
- Use StaticFiles for high-performance static file serving
- Automatic handling of cache headers, byte-range requests, directory traversal protection
- SPA routing uses catch-all to return only index.html
"""
import logging
from pathlib import Path
from fastapi import Depends, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from ..dependencies import get_logger
_BASE_DIR = Path(__file__).parent.parent.parent
# React build directory
_REACT_DIST = _BASE_DIR / "static" / "frontend" / "dist"
_REACT_ASSETS = _REACT_DIST / "assets"
def get_static_files_app() -> StaticFiles | None:
"""
Create a StaticFiles app for the assets directory.
Returns None if the directory doesn't exist (frontend not built).
"""
if _REACT_ASSETS.exists():
return StaticFiles(directory=str(_REACT_ASSETS))
return None
async def read_index(logger: logging.Logger = Depends(get_logger)) -> FileResponse:
"""Serve React index.html for SPA routing."""
react_index = _REACT_DIST / "index.html"
if react_index.exists():
return FileResponse(react_index, media_type="text/html")
logger.error("React build not found - run 'npm run build' in static/frontend/")
raise HTTPException(
status_code=503,
detail="Frontend not built. Run 'npm run build' in static/frontend/",
)
async def serve_react_assets(
filename: str, logger: logging.Logger = Depends(get_logger)
) -> FileResponse:
"""
Serve React built assets (JS, CSS, etc.).
Note: For production deployments, consider mounting StaticFiles directly
in the app configuration for better performance:
from fastapi.staticfiles import StaticFiles
app.mount("/assets", StaticFiles(directory="static/frontend/dist/assets"))
This fallback route is provided for flexibility and development convenience.
"""
asset_path = _REACT_ASSETS / filename
if not asset_path.exists():
logger.debug(f"Asset not found: {asset_path}")
raise HTTPException(status_code=404, detail=f"Asset {filename} not found")
# Security: Prevent directory traversal
try:
asset_path.resolve().relative_to(_REACT_ASSETS.resolve())
except ValueError:
logger.warning(f"Directory traversal attempt blocked: {filename}")
raise HTTPException(status_code=403, detail="Access denied")
# Determine media type based on suffix
suffix_to_media_type = {
".js": "application/javascript",
".css": "text/css",
".map": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".eot": "application/vnd.ms-fontobject",
}
media_type = suffix_to_media_type.get(asset_path.suffix.lower())
return FileResponse(asset_path, media_type=media_type)
|