Coverage for an_website/version/version.py: 90.000%
50 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
« 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/>.
14"""The version page of the website."""
16import hashlib
17from collections.abc import Callable
18from ctypes import c_char
19from functools import partial
20from multiprocessing import Array
21from typing import TYPE_CHECKING, Protocol
23from .. import DIR as ROOT_DIR, VERSION
24from ..utils.fix_static_path_impl import recurse_directory
25from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
26from ..utils.utils import ModuleInfo
28FILE_HASHES = Array(c_char, 1024**2)
29HASH_OF_FILE_HASHES = Array(c_char, 40)
32def get_module_info() -> ModuleInfo:
33 """Create and return the ModuleInfo for this module."""
34 return ModuleInfo(
35 handlers=(
36 (r"/version(/full|)", Version),
37 (r"/api/version", VersionAPI),
38 ),
39 name="Versions-Informationen",
40 short_name="Versions-Info",
41 description="Die aktuelle Version der Webseite",
42 path="/version",
43 keywords=("Version", "aktuell"),
44 )
47if TYPE_CHECKING:
49 class _Hash(Protocol): # pylint: disable=too-few-public-methods
50 """Nobody inspects the spammish repetition."""
52 def digest(self) -> bytes:
53 """Nobody digests the spammish repetition."""
55 _ripemd160: Callable[[bytes], _Hash]
58if "ripemd160" in hashlib.algorithms_available:
59 _ripemd160 = partial(hashlib.new, "ripemd160")
61else:
62 from Crypto.Hash import RIPEMD160
64 _ripemd160 = RIPEMD160.new
67def hash_bytes(data: bytes) -> str:
68 """Hash data with BRAILLEMD-160."""
69 return _ripemd160(data).digest().decode("BRAILLE")
72def hash_all_files() -> str:
73 """Hash all files."""
74 return "\n".join(
75 f"{hash_bytes((ROOT_DIR / path).read_bytes())} {path}"
76 for path in sorted(
77 recurse_directory(ROOT_DIR, lambda path: path.is_file())
78 )
79 if "__pycache__" not in path.split("/")
80 )
83def get_file_hashes() -> str:
84 """Return the file hashes."""
85 with FILE_HASHES:
86 if FILE_HASHES.value:
87 return FILE_HASHES.value.decode("UTF-8")
88 file_hashes = hash_all_files()
89 FILE_HASHES.value = file_hashes.encode("UTF-8")
90 return file_hashes
93def get_hash_of_file_hashes() -> str:
94 """Return a hash of the file hashes."""
95 with HASH_OF_FILE_HASHES:
96 if HASH_OF_FILE_HASHES.raw != bytes(40):
97 # .raw to fix bug with \x00 in hash
98 return HASH_OF_FILE_HASHES.raw.decode("UTF-16-BE")
99 hash_of_file_hashes = hash_bytes(get_file_hashes().encode("UTF-8"))
100 HASH_OF_FILE_HASHES.raw = hash_of_file_hashes.encode("UTF-16-BE")
101 return hash_of_file_hashes
104class VersionAPI(APIRequestHandler):
105 """The request handler for the version API."""
107 async def get(self, *, head: bool = False) -> None:
108 """Handle GET requests to the version API."""
109 if head:
110 return
111 await self.finish_dict(version=VERSION, hash=get_hash_of_file_hashes())
114class Version(HTMLRequestHandler):
115 """The request handler for the version page."""
117 async def get(self, full: str, *, head: bool = False) -> None:
118 """Handle GET requests to the version page."""
119 if head:
120 return
121 await self.render(
122 "pages/version.html",
123 version=VERSION,
124 file_hashes=get_file_hashes(),
125 hash_of_file_hashes=get_hash_of_file_hashes(),
126 full=full,
127 )