Coverage for daklib/import_repository.py: 26%
127 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1# Copyright (C) 2015, Ansgar Burchardt <ansgar@debian.org>
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17# This is still work-in-progress and far too incomplete.
18# ruff: noqa
19# type: ignore
21import os
22import shutil
23import tempfile
24import urllib.error
25import urllib.parse
26import urllib.request
27from typing import Optional
29import apt_pkg
30from sqlalchemy import select
31from sqlalchemy.orm import object_session
33import daklib.compress
34import daklib.config
35import daklib.dakapt
36import daklib.dbconn
37import daklib.gpg
38import daklib.regexes
39import daklib.upload
40from daklib.dbconn import Archive, DBBinary, DBSource, PoolFile
42# Hmm, maybe use APT directly for all of this?
44_release_hashes_fields = ("MD5Sum", "SHA1", "SHA256")
47class Release:
48 def __init__(self, base, suite_name, data):
49 self._base = base
50 self._suite_name = suite_name
51 self._dict = apt_pkg.TagSection(data)
52 self._hashes = daklib.upload.parse_file_list(
53 self._dict, False, daklib.regexes.re_file_safe_slash, _release_hashes_fields
54 )
56 def architectures(self):
57 return self._dict["Architectures"].split()
59 def components(self):
60 return self._dict["Components"].split()
62 def packages(self, component, architecture):
63 fn = "{0}/binary-{1}/Packages".format(component, architecture)
64 tmp = obtain_release_file(self, fn)
65 return apt_pkg.TagFile(tmp.fh())
67 def sources(self, component):
68 fn = "{0}/source/Sources".format(component)
69 tmp = obtain_release_file(self, fn)
70 return apt_pkg.TagFile(tmp.fh())
72 def suite(self):
73 return self._dict["Suite"]
75 def codename(self):
76 return self._dict["Codename"]
78 # TODO: Handle Date/Valid-Until to make sure we import
79 # a newer version than before
82class File:
83 def __init__(self):
84 config = daklib.config.Config()
85 self._tmp = tempfile.NamedTemporaryFile(dir=config["Dir::TempPath"])
87 def fh(self):
88 self._tmp.seek(0)
89 return self._tmp
91 def hashes(self):
92 return daklib.dakapt.DakHashes(self.fh())
95def obtain_file(base, path) -> File:
96 """Obtain a file 'path' located below 'base'
98 .. note::
100 return type can still change
101 """
102 fn = "{0}/{1}".format(base, path)
103 tmp = File()
104 if fn.startswith("http://"):
105 fh = urllib.request.urlopen(fn, timeout=300)
106 shutil.copyfileobj(fh, tmp._tmp)
107 fh.close()
108 else:
109 with open(fn, "rb") as fh:
110 shutil.copyfileobj(fh, tmp._tmp)
111 return tmp
114def obtain_release(base, suite_name, keyring, fingerprint=None) -> Release:
115 """Obtain release information"""
116 tmp = obtain_file(base, "dists/{0}/InRelease".format(suite_name))
117 data = tmp.fh().read()
118 f = daklib.gpg.SignedFile(data, [keyring])
119 r = Release(base, suite_name, f.contents)
120 if r.suite() != suite_name and r.codename() != suite_name:
121 raise Exception(
122 "Suite {0} doesn't match suite or codename from Release file.".format(
123 suite_name
124 )
125 )
126 return r
129_compressions = (".zst", ".xz", ".gz", ".bz2")
132def obtain_release_file(release, filename) -> File:
133 """Obtain file referenced from Release
135 A compressed version is automatically selected and decompressed if it exists.
136 """
137 if filename not in release._hashes:
138 raise ValueError("File {0} not referenced in Release".format(filename))
140 compressed = False
141 for ext in _compressions:
142 compressed_file = filename + ext
143 if compressed_file in release._hashes:
144 compressed = True
145 filename = compressed_file
146 break
148 # Obtain file and check hashes
149 tmp = obtain_file(
150 release._base, "dists/{0}/{1}".format(release._suite_name, filename)
151 )
152 hashedfile = release._hashes[filename]
153 hashedfile.check_fh(tmp.fh())
155 if compressed:
156 tmp2 = File()
157 daklib.compress.decompress(tmp.fh(), tmp2.fh(), filename)
158 tmp = tmp2
160 return tmp
163def import_source_to_archive(base, entry, transaction, archive, component) -> DBSource:
164 """Import source package described by 'entry' into the given 'archive' and 'component'
166 'entry' needs to be a dict-like object with at least the following
167 keys as used in a Sources index: Directory, Files, Checksums-Sha1,
168 Checksums-Sha256
169 """
170 # Obtain and verify files
171 if not daklib.regexes.re_file_safe_slash.match(entry["Directory"]):
172 raise Exception("Unsafe path in Directory field")
173 hashed_files = daklib.upload.parse_file_list(entry, False)
174 files = []
175 for f in hashed_files.values():
176 path = os.path.join(entry["Directory"], f.filename)
177 tmp = obtain_file(base, path)
178 f.check_fh(tmp.fh())
179 files.append(tmp)
180 directory, f.input_filename = os.path.split(tmp.fh().name)
182 # Inject files into archive
183 source = daklib.upload.Source(
184 directory, list(hashed_files.values()), [], require_signature=False
185 )
186 # TODO: ugly hack!
187 for f in hashed_files.keys():
188 if f.endswith(".dsc"):
189 continue
190 source.files[f].input_filename = hashed_files[f].input_filename
192 # TODO: allow changed_by to be NULL
193 changed_by = source.dsc["Maintainer"]
194 db_changed_by = daklib.dbconn.get_or_set_maintainer(changed_by, transaction.session)
195 db_source = transaction.install_source_to_archive(
196 directory, source, archive, component, db_changed_by
197 )
199 return db_source
202def import_package_to_suite(base, entry, transaction, suite, component) -> DBBinary:
203 """Import binary package described by 'entry' into the given 'suite' and 'component'
205 'entry' needs to be a dict-like object with at least the following
206 keys as used in a Packages index: Filename, Size, MD5sum, SHA1,
207 SHA256
208 """
209 # Obtain and verify file
210 filename = entry["Filename"]
211 tmp = obtain_file(base, filename)
212 directory, fn = os.path.split(tmp.fh().name)
213 hashedfile = daklib.upload.HashedFile(
214 os.path.basename(filename),
215 int(entry["Size"]),
216 entry["MD5sum"],
217 entry["SHA1"],
218 entry["SHA256"],
219 input_filename=fn,
220 )
221 hashedfile.check_fh(tmp.fh())
223 # Inject file into archive
224 binary = daklib.upload.Binary(directory, hashedfile)
225 db_binary = transaction.install_binary(directory, binary, suite, component)
226 transaction.flush()
228 return db_binary
231def import_source_to_suite(base, entry, transaction, suite, component):
232 """Import source package described by 'entry' into the given 'suite' and 'component'
234 'entry' needs to be a dict-like object with at least the following
235 keys as used in a Sources index: Directory, Files, Checksums-Sha1,
236 Checksums-Sha256
237 """
238 source = import_source_to_archive(
239 base, entry, transaction, suite.archive, component
240 )
241 source.suites.append(suite)
242 transaction.flush()
245def source_in_archive(
246 source: str,
247 version: str,
248 archive: Archive,
249 component: Optional[daklib.dbconn.Component] = None,
250) -> bool:
251 """Check that source package 'source' with version 'version' exists in 'archive',
252 with an optional check for the given component 'component'.
254 .. note::
256 This should probably be moved somewhere else
257 """
258 session = object_session(archive)
259 query = (
260 select(DBSource)
261 .filter_by(source=source, version=version)
262 .join(DBSource.poolfile)
263 .join(PoolFile.archives)
264 .filter_by(archive=archive)
265 )
266 if component is not None:
267 query = query.filter_by(component=component)
268 return bool(session.scalar(select(query.exists())))