Spaces:
Paused
Paused
| """ | |
| 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) | |