1#

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>

3#

4# This program is free software: you can redistribute it and/or modify

5# it under the terms of the GNU Lesser General Public License as published by

6# the Free Software Foundation, either version 3 of the License, or

7# (at your option) any later version.

8#

9# This program is distributed in the hope that it will be useful,

10# but WITHOUT ANY WARRANTY; without even the implied warranty of

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

12# GNU Lesser General Public License for more details.

13#

14# You should have received a copy of the GNU Lesser General Public License

15# along with this program. If not, see <http://www.gnu.org/licenses/>.

16

17 from __future__ import annotations

18

19 from logging import getLogger

20 from typing import TYPE_CHECKING

21 from urllib.parse import urljoin

22

23 from requests import codes

24

25 from gitlabracadabra.packages.package_file import PackageFile

26 from gitlabracadabra.packages.source import Source

27

28 if TYPE_CHECKING: 28 ↛ 29 line 28 didn't jump to line 29 because the condition on line 28 was never true

29 from requests.models import Response

30

31 from gitlabracadabra.packages.destination import Destination

32

33 logger = getLogger(__name__)

34

35

36 class PulpManifestSource(Source):

37 """PULP_MANIFEST repository."""

38

39 def __init__(

40 self,

41 *,

42 log_prefix: str = "",

43 url: str,

44 package_name: str,

45 package_version: str | None = None,

46 ) -> None:

47 """Initialize a PULP_MANIFEST Source object.

48

49 Args:

50 log_prefix: Log prefix.

51 url: PULP_MANIFEST URL.

52 package_name: Destination package name.

53 package_version: Destination package version.

54 """

55 super().__init__()

56 self._log_prefix = log_prefix

57 self._url = url

58 self._package_name = package_name

59 self._package_version = package_version or "0"

60

61 def __str__(self) -> str:

62 """Return string representation.

63

64 Returns:

65 A string.

66 """

67 return f"PULP_MANIFEST repository (url={self._url})"

68

69 def package_files(

70 self,

71 destination: Destination,

72 ) -> list[PackageFile]:

73 """Return list of package files.

74

75 Returns:

76 List of package files.

77 """

78 pulp_manifest = self._package_file(None)

79 pulp_manifest.force = True

80 destination_pulp_manifest_url = destination.get_url(pulp_manifest)

81 destination_pulp_manifest_response = destination.session.request(

82 "GET", destination_pulp_manifest_url, stream=True

83 )

84 current_pulp_manifest_files: list[PackageFile] = []

85 if destination_pulp_manifest_response.status_code == codes["ok"]: 85 ↛ 87 line 85 didn't jump to line 87 because the condition on line 85 was always true

86 current_pulp_manifest_files = self._pulp_manifest_files(destination_pulp_manifest_response)

87 elif destination_pulp_manifest_response.status_code != codes["not_found"]:

88 logger.warning(

89 '%sUnexpected HTTP status for %s package file "%s" from "%s" version %s (%s): received %i %s with GET method on destination',

90 self._log_prefix,

91 pulp_manifest.package_type,

92 pulp_manifest.file_name,

93 pulp_manifest.package_name,

94 pulp_manifest.package_version,

95 destination_pulp_manifest_url,

96 destination_pulp_manifest_response.status_code,

97 destination_pulp_manifest_response.reason,

98 )

99 return []

100

101 source_pulp_manifest_response = self.session.request("GET", self._url, stream=True)

102 if source_pulp_manifest_response.status_code != codes["ok"]: 102 ↛ 103 line 102 didn't jump to line 103 because the condition on line 102 was never true

103 logger.warning(

104 "%sUnexpected HTTP status for PULP_MANIFEST url %s: received %i %s",

105 self._log_prefix,

106 self._url,

107 source_pulp_manifest_response.status_code,

108 source_pulp_manifest_response.reason,

109 )

110 return []

111 target_pulp_manifest_files = self._pulp_manifest_files(source_pulp_manifest_response)

112 package_files: list[PackageFile] = []

113 for package_file in target_pulp_manifest_files:

114 if package_file not in current_pulp_manifest_files:

115 package_file.force = True

116 package_files.append(package_file)

117 package_files.append(pulp_manifest)

118 for package_file in current_pulp_manifest_files:

119 if package_file not in target_pulp_manifest_files:

120 package_file.delete = True

121 package_file.force = True

122 package_files.append(package_file)

123 if len(package_files) > 1: 123 ↛ 130 line 123 didn't jump to line 130 because the condition on line 123 was always true

124 old_pulp_manifest = self._package_file(None)

125 old_pulp_manifest.delete = True

126 old_pulp_manifest.force = True

127 package_files.append(old_pulp_manifest)

128 destination.cache_project_package_package_files("generic", self._package_name, self._package_version)

129 else:

130 package_files.remove(pulp_manifest)

131 return package_files

132

133 def _pulp_manifest_files(self, pulp_manifest_response: Response) -> list[PackageFile]:

134 if pulp_manifest_response.encoding is None:

135 pulp_manifest_response.encoding = "utf-8"

136 package_files: list[PackageFile] = []

137 for pulp_manifest_line in pulp_manifest_response.iter_lines(decode_unicode=True):

138 try:

139 filename, sha256_checksum, size_in_bytes = pulp_manifest_line.split(",", 3)

140 except ValueError:

141 logger.warning(

142 "%sInvalid PULP_MANIFEST line: %s",

143 self._log_prefix,

144 pulp_manifest_line,

145 )

146 continue

147 package_file = self._package_file(filename, sha256_checksum, size_in_bytes)

148 if package_file: 148 ↛ 137 line 148 didn't jump to line 137 because the condition on line 148 was always true

149 package_files.append(package_file)

150 return package_files

151

152 def _package_file(

153 self, filename: str | None, sha256_checksum: str | None = None, size_in_bytes: str | None = None

154 ) -> PackageFile:

155 url = urljoin(self._url, filename, allow_fragments=False)

156 package_file = PackageFile(

157 url,

158 "generic",

159 self._package_name,

160 self._package_version,

161 url.split("/").pop(),

162 )

163 if sha256_checksum is not None:

164 package_file.metadata["sha256_checksum"] = sha256_checksum

165 if size_in_bytes is not None:

166 package_file.metadata["size_in_bytes"] = size_in_bytes

167 return package_file