Coverage for an_website/utils/static_file_handling.py: 94.286%
35 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 14:47 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 14:47 +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."""
16from __future__ import annotations
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
26import defity
27import orjson as json
28import tornado.web
29from openmoji_dist import get_openmoji_data
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
38LOGGER: Final = logging.getLogger(__name__)
40FILE_HASHES_DICT: Final[Mapping[str, str]] = create_file_hashes_dict()
42CONTENT_TYPES: Final[Mapping[str, str]] = json.loads(
43 (ROOT_DIR / "vendored" / "media-types.json").read_bytes()
44)
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
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 debug_style_dir = (
70 ROOT_DIR.absolute().parent / "style"
71 if isinstance(ROOT_DIR, Path)
72 else None
73 )
74 if sys.flags.dev_mode and debug_style_dir and debug_style_dir.exists():
75 # add handlers for the unminified CSS files
76 handlers.append(
77 (
78 r"/static/css/(.+\.css)",
79 TraversableStaticFileHandler,
80 {"root": debug_style_dir, "hashes": {}},
81 )
82 )
83 handlers.append(
84 (
85 r"/static/(.*)",
86 TraversableStaticFileHandler,
87 {"root": STATIC_DIR, "hashes": FILE_HASHES_DICT},
88 )
89 )
90 return handlers
93@cache
94def fix_static_path(path: str) -> str:
95 """Fix the path for static files."""
96 return fix_static_path_impl(path, FILE_HASHES_DICT)
99def content_type_from_path(url_path: str, file: Traversable) -> str | None:
100 """Extract the Content-Type from a path."""
101 content_type: str | None = CONTENT_TYPES.get(Path(url_path).suffix[1:])
102 if not content_type:
103 with file.open("rb") as io:
104 content_type = defity.from_file(io)
105 if content_type and content_type.startswith("text/"):
106 content_type += "; charset=UTF-8"
107 return content_type