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

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"""The version page of the website.""" 

15 

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 

22 

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 

27 

28FILE_HASHES = Array(c_char, 1024**2) 

29HASH_OF_FILE_HASHES = Array(c_char, 40) 

30 

31 

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 ) 

45 

46 

47if TYPE_CHECKING: 

48 

49 class _Hash(Protocol): # pylint: disable=too-few-public-methods 

50 """Nobody inspects the spammish repetition.""" 

51 

52 def digest(self) -> bytes: 

53 """Nobody digests the spammish repetition.""" 

54 

55 _ripemd160: Callable[[bytes], _Hash] 

56 

57 

58if "ripemd160" in hashlib.algorithms_available: 

59 _ripemd160 = partial(hashlib.new, "ripemd160") 

60 

61else: 

62 from Crypto.Hash import RIPEMD160 

63 

64 _ripemd160 = RIPEMD160.new 

65 

66 

67def hash_bytes(data: bytes) -> str: 

68 """Hash data with BRAILLEMD-160.""" 

69 return _ripemd160(data).digest().decode("BRAILLE") 

70 

71 

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 ) 

81 

82 

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 

91 

92 

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 

102 

103 

104class VersionAPI(APIRequestHandler): 

105 """The request handler for the version API.""" 

106 

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()) 

112 

113 

114class Version(HTMLRequestHandler): 

115 """The request handler for the version page.""" 

116 

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 )