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

39 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-16 19: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 

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.request_handler import APIRequestHandler, HTMLRequestHandler 

25from ..utils.utils import ModuleInfo, recurse_directory 

26 

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

28HASH_OF_FILE_HASHES = Array(c_char, 40) 

29 

30 

31def get_module_info() -> ModuleInfo: 

32 """Create and return the ModuleInfo for this module.""" 

33 return ModuleInfo( 

34 handlers=( 

35 (r"/version(/full|)", Version), 

36 (r"/api/version", VersionAPI), 

37 ), 

38 name="Versions-Informationen", 

39 short_name="Versions-Info", 

40 description="Die aktuelle Version der Webseite", 

41 path="/version", 

42 keywords=("Version", "aktuell"), 

43 ) 

44 

45 

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

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

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

49 

50 

51def hash_all_files() -> str: 

52 """Hash all files.""" 

53 return "\n".join( 

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

55 for path in sorted( 

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

57 ) 

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

59 ) 

60 

61 

62def get_file_hashes() -> str: 

63 """Return the file hashes.""" 

64 with FILE_HASHES: 

65 if FILE_HASHES.value: 

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

67 file_hashes = hash_all_files() 

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

69 return file_hashes 

70 

71 

72def get_hash_of_file_hashes() -> str: 

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

74 with HASH_OF_FILE_HASHES: 

75 if HASH_OF_FILE_HASHES.value: 

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

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

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

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

80 return hash_of_file_hashes 

81 

82 

83class VersionAPI(APIRequestHandler): 

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

85 

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

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

88 if head: 

89 return 

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

91 

92 

93class Version(HTMLRequestHandler): 

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

95 

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

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

98 if head: 

99 return 

100 await self.render( 

101 "pages/version.html", 

102 version=VERSION, 

103 file_hashes=get_file_hashes(), 

104 hash_of_file_hashes=get_hash_of_file_hashes(), 

105 full=full, 

106 )