Coverage for an_website / utils / static_file_handling.py: 100.000%
34 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-15 14:36 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-15 14:36 +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/>.
14"""Useful stuff for handling static files."""
17import logging
18import sys
19from collections.abc import Mapping
20from functools import cache
21from importlib.resources.abc import Traversable
22from pathlib import Path
23from typing import Final
25import defity
26import orjson as json
27import tornado.web
28from openmoji_dist import get_openmoji_data
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
37LOGGER: Final = logging.getLogger(__name__)
39FILE_HASHES_DICT: Final[Mapping[str, str]] = create_file_hashes_dict()
41CONTENT_TYPES: Final[Mapping[str, str]] = json.loads(
42 (ROOT_DIR / "vendored" / "media-types.json").read_bytes()
43)
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
51 handlers: list[Handler] = [
52 (
53 "/static/openmoji/(.*)",
54 TraversableStaticFileHandler,
55 {"root": get_openmoji_data(), "hashes": {}},
56 ),
57 (
58 r"(?:/static)?/(\.env|favicon\.(?:png|jxl)|humans\.txt|robots\.txt)",
59 TraversableStaticFileHandler,
60 {"root": STATIC_DIR, "hashes": FILE_HASHES_DICT},
61 ),
62 (
63 "/favicon.ico",
64 tornado.web.RedirectHandler,
65 {"url": fix_static_path("favicon.png")},
66 ),
67 (
68 r"/static/(img/netcup-oekostrom2\..*)",
69 TraversableStaticFileHandler,
70 {
71 "root": STATIC_DIR,
72 "hashes": FILE_HASHES_DICT,
73 "headers": (("X-Robots-Tag", "noindex, nofollow"),),
74 },
75 ),
76 ]
77 debug_style_dir = (
78 ROOT_DIR.absolute().parent / "style"
79 if isinstance(ROOT_DIR, Path)
80 else None
81 )
82 if sys.flags.dev_mode and debug_style_dir and debug_style_dir.exists():
83 # add handlers for the unminified CSS files
84 handlers.append(
85 (
86 r"/static/css/(.+\.css)",
87 TraversableStaticFileHandler,
88 {"root": debug_style_dir, "hashes": {}},
89 )
90 )
92 handlers.append(
93 (
94 r"/static/(.*)",
95 TraversableStaticFileHandler,
96 {"root": STATIC_DIR, "hashes": FILE_HASHES_DICT},
97 )
98 )
99 return handlers
102@cache
103def fix_static_path(path: str) -> str:
104 """Fix the path for static files."""
105 return fix_static_path_impl(path, FILE_HASHES_DICT)
108def content_type_from_path(url_path: str, file: Traversable) -> str | None:
109 """Extract the Content-Type from a path."""
110 content_type: str | None = CONTENT_TYPES.get(Path(url_path).suffix[1:])
111 if not content_type:
112 with file.open("rb") as io:
113 content_type = defity.from_file(io)
114 if content_type and content_type.startswith("text/"):
115 content_type += "; charset=UTF-8"
116 return content_type