Coverage for an_website/version/version.py: 100.000%

40 statements  

« 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/>. 

13 

14"""The version page of the website.""" 

15 

16from __future__ import annotations 

17 

18from ctypes import c_char 

19from multiprocessing import Array 

20 

21from Crypto.Hash import RIPEMD160 

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 

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

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

49 return RIPEMD160.new(data).digest().decode("BRAILLE") 

50 

51 

52def hash_all_files() -> str: 

53 """Hash all files.""" 

54 return "\n".join( 

55 f"{hash_bytes((ROOT_DIR / path).read_bytes())} {path}" 

56 for path in sorted( 

57 recurse_directory(ROOT_DIR, lambda path: path.is_file()) 

58 ) 

59 if "__pycache__" not in path.split("/") 

60 ) 

61 

62 

63def get_file_hashes() -> str: 

64 """Return the file hashes.""" 

65 with FILE_HASHES: 

66 if FILE_HASHES.value: 

67 return FILE_HASHES.value.decode("UTF-8") 

68 file_hashes = hash_all_files() 

69 FILE_HASHES.value = file_hashes.encode("UTF-8") 

70 return file_hashes 

71 

72 

73def get_hash_of_file_hashes() -> str: 

74 """Return a hash of the file hashes.""" 

75 with HASH_OF_FILE_HASHES: 

76 if HASH_OF_FILE_HASHES.value: 

77 # .raw to fix bug with \x00 in hash 

78 return HASH_OF_FILE_HASHES.raw.decode("UTF-16-BE") 

79 hash_of_file_hashes = hash_bytes(get_file_hashes().encode("UTF-8")) 

80 HASH_OF_FILE_HASHES.raw = hash_of_file_hashes.encode("UTF-16-BE") 

81 return hash_of_file_hashes 

82 

83 

84class VersionAPI(APIRequestHandler): 

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

86 

87 async def get(self, *, head: bool = False) -> None: 

88 """Handle GET requests to the version API.""" 

89 if head: 

90 return 

91 await self.finish_dict(version=VERSION, hash=get_hash_of_file_hashes()) 

92 

93 

94class Version(HTMLRequestHandler): 

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

96 

97 async def get(self, full: str, *, head: bool = False) -> None: 

98 """Handle GET requests to the version page.""" 

99 if head: 

100 return 

101 await self.render( 

102 "pages/version.html", 

103 version=VERSION, 

104 file_hashes=get_file_hashes(), 

105 hash_of_file_hashes=get_hash_of_file_hashes(), 

106 full=full, 

107 )