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

1"""Static file serving middleware.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Awaitable, Callable 

6import mimetypes 

7from pathlib import Path 

8 

9from starlette.middleware.base import BaseHTTPMiddleware 

10from starlette.requests import Request 

11from starlette.responses import FileResponse, Response 

12from starlette.types import ASGIApp 

13 

14 

15class StaticFilesMiddleware(BaseHTTPMiddleware): 

16 """Middleware for serving static files""" 

17 

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 

32 

33 if check_dir and not self.directory.exists(): 

34 raise RuntimeError(f"Static files directory '{directory}' does not exist") 

35 

36 if check_dir and not self.directory.is_dir(): 

37 raise RuntimeError(f"Static files path '{directory}' is not a directory") 

38 

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) 

45 

46 # Extract file path from URL 

47 path = request.url.path[len(self.prefix) + 1 :] 

48 

49 # Prevent directory traversal 

50 if ".." in path or path.startswith("/"): 

51 return Response("Forbidden", status_code=403) 

52 

53 # Build full file path 

54 file_path = self.directory / path 

55 

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) 

71 

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" 

76 

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 ) 

83 

84 

85# Re-export StaticFileProvider from static package 

86from lexigram.web.static import StaticFileProvider 

87 

88__all__ = ["StaticFileProvider", "StaticFilesMiddleware"]