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

35 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 12:07 +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 

16from __future__ import annotations 

17 

18import logging 

19import sys 

20from collections.abc import Mapping 

21from functools import cache 

22from importlib.resources.abc import Traversable 

23from pathlib import Path 

24from typing import Final 

25 

26import defity 

27import orjson as json 

28import tornado.web 

29from openmoji_dist import get_openmoji_data 

30 

31from .. import DIR as ROOT_DIR, STATIC_DIR 

32from .fix_static_path_impl import ( 

33 create_file_hashes_dict, 

34 fix_static_path_impl, 

35) 

36from .utils import Handler 

37 

38LOGGER: Final = logging.getLogger(__name__) 

39 

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

41 

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

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

44) 

45 

46 

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

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

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

50 from .static_file_from_traversable import TraversableStaticFileHandler 

51 

52 handlers: list[Handler] = [ 

53 ( 

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

55 TraversableStaticFileHandler, 

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

57 ), 

58 ( 

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

60 TraversableStaticFileHandler, 

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

62 ), 

63 ( 

64 "/favicon.ico", 

65 tornado.web.RedirectHandler, 

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

67 ), 

68 ( 

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

70 TraversableStaticFileHandler, 

71 { 

72 "root": STATIC_DIR, 

73 "hashes": FILE_HASHES_DICT, 

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

75 }, 

76 ), 

77 ] 

78 debug_style_dir = ( 

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

80 if isinstance(ROOT_DIR, Path) 

81 else None 

82 ) 

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

84 # add handlers for the unminified CSS files 

85 handlers.append( 

86 ( 

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

88 TraversableStaticFileHandler, 

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

90 ) 

91 ) 

92 

93 handlers.append( 

94 ( 

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

96 TraversableStaticFileHandler, 

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

98 ) 

99 ) 

100 return handlers 

101 

102 

103@cache 

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

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

106 return fix_static_path_impl(path, FILE_HASHES_DICT) 

107 

108 

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

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

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

112 if not content_type: 

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

114 content_type = defity.from_file(io) 

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

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

117 return content_type