Coverage for an_website/utils/static_file_handling.py: 100.000%

38 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-10 18:56 +0000

1# This program is free software: you can redistribute it and/or modify 

2# it under the terms of the GNU Affero General Public License as 

3# published by the Free Software Foundation, either version 3 of the 

4# License, or (at your option) any later version. 

5# 

6# This program is distributed in the hope that it will be useful, 

7# but WITHOUT ANY WARRANTY; without even the implied warranty of 

8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

9# GNU Affero General Public License for more details. 

10# 

11# You should have received a copy of the GNU Affero General Public License 

12# along with this program. If not, see <https://www.gnu.org/licenses/>. 

13 

14"""Useful stuff for handling static files.""" 

15 

16import logging 

17import sys 

18import typing 

19from collections.abc import Mapping 

20from functools import cache 

21from importlib.resources.abc import Traversable 

22from pathlib import Path 

23from typing import Final 

24 

25import defity 

26import orjson as json 

27import tornado.web 

28from openmoji_dist import get_openmoji_data 

29 

30from .. import DIR as ROOT_DIR, STATIC_DIR 

31from .fix_static_path_impl import ( 

32 create_file_hashes_dict, 

33 fix_static_path_impl, 

34) 

35from .utils import Handler 

36 

37LOGGER: Final = logging.getLogger(__name__) 

38 

39FILE_HASHES_DICT: Final[Mapping[str, str]] = create_file_hashes_dict() 

40 

41CONTENT_TYPES: Final[Mapping[str, str]] = json.loads( 

42 (ROOT_DIR / "vendored" / "media-types.json").read_bytes() 

43) 

44 

45 

46def get_handlers() -> list[Handler]: 

47 """Return a list of handlers for static files.""" 

48 # pylint: disable=import-outside-toplevel, cyclic-import 

49 from .static_file_from_traversable import TraversableStaticFileHandler 

50 

51 class NoRatelimitTraversableStaticFileHandler(TraversableStaticFileHandler): 

52 """Service files without ratelimits.""" 

53 

54 @typing.override 

55 async def prepare(self) -> None: 

56 """Make b1nzy sad.""" 

57 

58 handlers: list[Handler] = [ 

59 ( 

60 r"/static/openmoji/(svg/[1-9A-F-]+\.svg)", 

61 NoRatelimitTraversableStaticFileHandler, 

62 {"root": get_openmoji_data(), "hashes": {}}, 

63 ), 

64 ( 

65 "/static/openmoji/(.*)", 

66 TraversableStaticFileHandler, 

67 {"root": get_openmoji_data(), "hashes": {}}, 

68 ), 

69 ( 

70 r"(?:/static)?/(\.env|favicon\.(?:png|jxl)|humans\.txt|robots\.txt|llms\.txt)", 

71 TraversableStaticFileHandler, 

72 {"root": STATIC_DIR, "hashes": FILE_HASHES_DICT}, 

73 ), 

74 ( 

75 "/favicon.ico", 

76 tornado.web.RedirectHandler, 

77 {"url": fix_static_path("favicon.png")}, 

78 ), 

79 ( 

80 r"/static/(img/netcup-oekostrom2\..*)", 

81 TraversableStaticFileHandler, 

82 { 

83 "root": STATIC_DIR, 

84 "hashes": FILE_HASHES_DICT, 

85 "headers": (("X-Robots-Tag", "noindex, nofollow"),), 

86 }, 

87 ), 

88 ] 

89 debug_style_dir = ( 

90 ROOT_DIR.absolute().parent / "style" 

91 if isinstance(ROOT_DIR, Path) 

92 else None 

93 ) 

94 if sys.flags.dev_mode and debug_style_dir and debug_style_dir.exists(): 

95 # add handlers for the unminified CSS files 

96 handlers.append( 

97 ( 

98 r"/static/css/(.+\.css)", 

99 TraversableStaticFileHandler, 

100 {"root": debug_style_dir, "hashes": {}}, 

101 ) 

102 ) 

103 

104 handlers.append( 

105 ( 

106 r"/static/(.*)", 

107 TraversableStaticFileHandler, 

108 {"root": STATIC_DIR, "hashes": FILE_HASHES_DICT}, 

109 ) 

110 ) 

111 return handlers 

112 

113 

114@cache 

115def fix_static_path(path: str) -> str: 

116 """Fix the path for static files.""" 

117 return fix_static_path_impl(path, FILE_HASHES_DICT) 

118 

119 

120def content_type_from_path(url_path: str, file: Traversable) -> str | None: 

121 """Extract the Content-Type from a path.""" 

122 content_type: str | None = CONTENT_TYPES.get(Path(url_path).suffix[1:]) 

123 if not content_type: 

124 with file.open("rb") as io: 

125 content_type = defity.from_file(io) 

126 if content_type and content_type.startswith("text/"): 

127 content_type += "; charset=UTF-8" 

128 return content_type