peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
3.15 kB
"""
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)