Coverage for src/lexigram/web/middleware/static.py: 33%
39 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Static file serving middleware."""
3from __future__ import annotations
5from collections.abc import Awaitable, Callable
6import mimetypes
7from pathlib import Path
9from starlette.middleware.base import BaseHTTPMiddleware
10from starlette.requests import Request
11from starlette.responses import FileResponse, Response
12from starlette.types import ASGIApp
15class StaticFilesMiddleware(BaseHTTPMiddleware):
16 """Middleware for serving static files"""
18 def __init__(
19 self,
20 app: ASGIApp,
21 directory: str = "static",
22 prefix: str = "/static",
23 html: bool = False,
24 check_dir: bool = True,
25 cache_max_age: int = 31536000,
26 ) -> None:
27 super().__init__(app)
28 self.directory = Path(directory)
29 self.prefix = prefix.rstrip("/")
30 self.html = html
31 self.cache_max_age = cache_max_age
33 if check_dir and not self.directory.exists():
34 raise RuntimeError(f"Static files directory '{directory}' does not exist")
36 if check_dir and not self.directory.is_dir():
37 raise RuntimeError(f"Static files path '{directory}' is not a directory")
39 async def dispatch(
40 self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
41 ) -> Response:
42 """Serve static files if path matches prefix"""
43 if not request.url.path.startswith(self.prefix + "/"):
44 return await call_next(request)
46 # Extract file path from URL
47 path = request.url.path[len(self.prefix) + 1 :]
49 # Prevent directory traversal
50 if ".." in path or path.startswith("/"):
51 return Response("Forbidden", status_code=403)
53 # Build full file path
54 file_path = self.directory / path
56 # Check if file exists
57 if not file_path.exists() or not file_path.is_file():
58 if (self.html and path == "") or path.endswith("/"):
59 # Try index.html for directory requests
60 index_path = (
61 file_path / "index.html"
62 if file_path.is_dir()
63 else file_path.with_suffix(".html")
64 )
65 if index_path.exists() and index_path.is_file():
66 file_path = index_path
67 else:
68 return await call_next(request)
69 else:
70 return await call_next(request)
72 # Determine content type
73 content_type, _ = mimetypes.guess_type(str(file_path))
74 if content_type is None:
75 content_type = "application/octet-stream"
77 # Return file response
78 return FileResponse(
79 path=file_path,
80 media_type=content_type,
81 headers={"Cache-Control": f"public, max-age={self.cache_max_age}"},
82 )
85# Re-export StaticFileProvider from static package
86from lexigram.web.static import StaticFileProvider
88__all__ = ["StaticFileProvider", "StaticFilesMiddleware"]