import_repository.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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.
  16. import daklib.compress
  17. import daklib.config
  18. import daklib.dakapt
  19. import daklib.dbconn
  20. import daklib.gpg
  21. import daklib.upload
  22. import daklib.regexes
  23. import apt_pkg
  24. import os
  25. import shutil
  26. import tempfile
  27. import urllib.request
  28. import urllib.error
  29. import urllib.parse
  30. from daklib.dbconn import Archive, Component, DBBinary, DBSource, PoolFile
  31. from sqlalchemy.orm import object_session
  32. from typing import Optional
  33. # Hmm, maybe use APT directly for all of this?
  34. _release_hashes_fields = ('MD5Sum', 'SHA1', 'SHA256')
  35. class Release:
  36. def __init__(self, base, suite_name, data):
  37. self._base = base
  38. self._suite_name = suite_name
  39. self._dict = apt_pkg.TagSection(data)
  40. self._hashes = daklib.upload.parse_file_list(self._dict, False, daklib.regexes.re_file_safe_slash, _release_hashes_fields)
  41. def architectures(self):
  42. return self._dict['Architectures'].split()
  43. def components(self):
  44. return self._dict['Components'].split()
  45. def packages(self, component, architecture):
  46. fn = '{0}/binary-{1}/Packages'.format(component, architecture)
  47. tmp = obtain_release_file(self, fn)
  48. return apt_pkg.TagFile(tmp.fh())
  49. def sources(self, component):
  50. fn = '{0}/source/Sources'.format(component)
  51. tmp = obtain_release_file(self, fn)
  52. return apt_pkg.TagFile(tmp.fh())
  53. def suite(self):
  54. return self._dict['Suite']
  55. def codename(self):
  56. return self._dict['Codename']
  57. # TODO: Handle Date/Valid-Until to make sure we import
  58. # a newer version than before
  59. class File:
  60. def __init__(self):
  61. config = daklib.config.Config()
  62. self._tmp = tempfile.NamedTemporaryFile(dir=config['Dir::TempPath'])
  63. def fh(self):
  64. self._tmp.seek(0)
  65. return self._tmp
  66. def hashes(self):
  67. return daklib.dakapt.DakHashes(self.fh())
  68. def obtain_file(base, path) -> File:
  69. """Obtain a file 'path' located below 'base'
  70. .. note::
  71. return type can still change
  72. """
  73. fn = '{0}/{1}'.format(base, path)
  74. tmp = File()
  75. if fn.startswith('http://'):
  76. fh = urllib.request.urlopen(fn, timeout=300)
  77. shutil.copyfileobj(fh, tmp._tmp)
  78. fh.close()
  79. else:
  80. with open(fn, 'rb') as fh:
  81. shutil.copyfileobj(fh, tmp._tmp)
  82. return tmp
  83. def obtain_release(base, suite_name, keyring, fingerprint=None) -> Release:
  84. """Obtain release information"""
  85. tmp = obtain_file(base, 'dists/{0}/InRelease'.format(suite_name))
  86. data = tmp.fh().read()
  87. f = daklib.gpg.SignedFile(data, [keyring])
  88. r = Release(base, suite_name, f.contents)
  89. if r.suite() != suite_name and r.codename() != suite_name:
  90. raise Exception("Suite {0} doesn't match suite or codename from Release file.".format(suite_name))
  91. return r
  92. _compressions = ('.zst', '.xz', '.gz', '.bz2')
  93. def obtain_release_file(release, filename) -> File:
  94. """Obtain file referenced from Release
  95. A compressed version is automatically selected and decompressed if it exists.
  96. """
  97. if filename not in release._hashes:
  98. raise ValueError("File {0} not referenced in Release".format(filename))
  99. compressed = False
  100. for ext in _compressions:
  101. compressed_file = filename + ext
  102. if compressed_file in release._hashes:
  103. compressed = True
  104. filename = compressed_file
  105. break
  106. # Obtain file and check hashes
  107. tmp = obtain_file(release._base, 'dists/{0}/{1}'.format(release._suite_name, filename))
  108. hashedfile = release._hashes[filename]
  109. hashedfile.check_fh(tmp.fh())
  110. if compressed:
  111. tmp2 = File()
  112. daklib.compress.decompress(tmp.fh(), tmp2.fh(), filename)
  113. tmp = tmp2
  114. return tmp
  115. def import_source_to_archive(base, entry, transaction, archive, component) -> DBSource:
  116. """Import source package described by 'entry' into the given 'archive' and 'component'
  117. 'entry' needs to be a dict-like object with at least the following
  118. keys as used in a Sources index: Directory, Files, Checksums-Sha1,
  119. Checksums-Sha256
  120. """
  121. # Obtain and verify files
  122. if not daklib.regexes.re_file_safe_slash.match(entry['Directory']):
  123. raise Exception("Unsafe path in Directory field")
  124. hashed_files = daklib.upload.parse_file_list(entry, False)
  125. files = []
  126. for f in hashed_files.values():
  127. path = os.path.join(entry['Directory'], f.filename)
  128. tmp = obtain_file(base, path)
  129. f.check_fh(tmp.fh())
  130. files.append(tmp)
  131. directory, f.input_filename = os.path.split(tmp.fh().name)
  132. # Inject files into archive
  133. source = daklib.upload.Source(directory, list(hashed_files.values()), [], require_signature=False)
  134. # TODO: ugly hack!
  135. for f in hashed_files.keys():
  136. if f.endswith('.dsc'):
  137. continue
  138. source.files[f].input_filename = hashed_files[f].input_filename
  139. # TODO: allow changed_by to be NULL
  140. changed_by = source.dsc['Maintainer']
  141. db_changed_by = daklib.dbconn.get_or_set_maintainer(changed_by, transaction.session)
  142. db_source = transaction.install_source_to_archive(directory, source, archive, component, db_changed_by)
  143. return db_source
  144. def import_package_to_suite(base, entry, transaction, suite, component) -> DBBinary:
  145. """Import binary package described by 'entry' into the given 'suite' and 'component'
  146. 'entry' needs to be a dict-like object with at least the following
  147. keys as used in a Packages index: Filename, Size, MD5sum, SHA1,
  148. SHA256
  149. """
  150. # Obtain and verify file
  151. filename = entry['Filename']
  152. tmp = obtain_file(base, filename)
  153. directory, fn = os.path.split(tmp.fh().name)
  154. hashedfile = daklib.upload.HashedFile(os.path.basename(filename), int(entry['Size']), entry['MD5sum'], entry['SHA1'], entry['SHA256'], input_filename=fn)
  155. hashedfile.check_fh(tmp.fh())
  156. # Inject file into archive
  157. binary = daklib.upload.Binary(directory, hashedfile)
  158. db_binary = transaction.install_binary(directory, binary, suite, component)
  159. transaction.flush()
  160. return db_binary
  161. def import_source_to_suite(base, entry, transaction, suite, component):
  162. """Import source package described by 'entry' into the given 'suite' and 'component'
  163. 'entry' needs to be a dict-like object with at least the following
  164. keys as used in a Sources index: Directory, Files, Checksums-Sha1,
  165. Checksums-Sha256
  166. """
  167. source = import_source_to_archive(base, entry, transaction, suite.archive, component)
  168. source.suites.append(suite)
  169. transaction.flush()
  170. def source_in_archive(source: str, version: str, archive: Archive, component: Optional[daklib.dbconn.Component] = None) -> bool:
  171. """Check that source package 'source' with version 'version' exists in 'archive',
  172. with an optional check for the given component 'component'.
  173. .. note::
  174. This should probably be moved somewhere else
  175. """
  176. session = object_session(archive)
  177. query = session.query(DBSource).filter_by(source=source, version=version) \
  178. .join(DBSource.poolfile).join(PoolFile.archives).filter_by(archive=archive)
  179. if component is not None:
  180. query = query.filter_by(component=component)
  181. return session.query(query.exists()).scalar()