]> git.decadent.org.uk Git - dak.git/blob - daklib/archive.py
Set install_date properly for source table
[dak.git] / daklib / archive.py
1 # Copyright (C) 2012, 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
17 """module to manipulate the archive
18
19 This module provides classes to manipulate the archive.
20 """
21
22 from daklib.dbconn import *
23 import daklib.checks as checks
24 from daklib.config import Config
25 import daklib.upload as upload
26 import daklib.utils as utils
27 from daklib.fstransactions import FilesystemTransaction
28 from daklib.regexes import re_changelog_versions, re_bin_only_nmu
29 import daklib.daksubprocess
30
31 import apt_pkg
32 from datetime import datetime
33 import os
34 import shutil
35 from sqlalchemy.orm.exc import NoResultFound
36 from sqlalchemy.orm import object_session
37 import sqlalchemy.exc
38 import tempfile
39 import traceback
40
41 class ArchiveException(Exception):
42     pass
43
44 class HashMismatchException(ArchiveException):
45     pass
46
47 class ArchiveTransaction(object):
48     """manipulate the archive in a transaction
49     """
50     def __init__(self):
51         self.fs = FilesystemTransaction()
52         self.session = DBConn().session()
53
54     def get_file(self, hashed_file, source_name, check_hashes=True):
55         """Look for file C{hashed_file} in database
56
57         @type  hashed_file: L{daklib.upload.HashedFile}
58         @param hashed_file: file to look for in the database
59
60         @type  source_name: str
61         @param source_name: source package name
62
63         @type  check_hashes: bool
64         @param check_hashes: check size and hashes match
65
66         @raise KeyError: file was not found in the database
67         @raise HashMismatchException: hash mismatch
68
69         @rtype:  L{daklib.dbconn.PoolFile}
70         @return: database entry for the file
71         """
72         poolname = os.path.join(utils.poolify(source_name), hashed_file.filename)
73         try:
74             poolfile = self.session.query(PoolFile).filter_by(filename=poolname).one()
75             if check_hashes and (poolfile.filesize != hashed_file.size
76                                  or poolfile.md5sum != hashed_file.md5sum
77                                  or poolfile.sha1sum != hashed_file.sha1sum
78                                  or poolfile.sha256sum != hashed_file.sha256sum):
79                 raise HashMismatchException('{0}: Does not match file already existing in the pool.'.format(hashed_file.filename))
80             return poolfile
81         except NoResultFound:
82             raise KeyError('{0} not found in database.'.format(poolname))
83
84     def _install_file(self, directory, hashed_file, archive, component, source_name):
85         """Install a file
86
87         Will not give an error when the file is already present.
88
89         @rtype:  L{daklib.dbconn.PoolFile}
90         @return: database object for the new file
91         """
92         session = self.session
93
94         poolname = os.path.join(utils.poolify(source_name), hashed_file.filename)
95         try:
96             poolfile = self.get_file(hashed_file, source_name)
97         except KeyError:
98             poolfile = PoolFile(filename=poolname, filesize=hashed_file.size)
99             poolfile.md5sum = hashed_file.md5sum
100             poolfile.sha1sum = hashed_file.sha1sum
101             poolfile.sha256sum = hashed_file.sha256sum
102             session.add(poolfile)
103             session.flush()
104
105         try:
106             session.query(ArchiveFile).filter_by(archive=archive, component=component, file=poolfile).one()
107         except NoResultFound:
108             archive_file = ArchiveFile(archive, component, poolfile)
109             session.add(archive_file)
110             session.flush()
111
112             path = os.path.join(archive.path, 'pool', component.component_name, poolname)
113             hashed_file_path = os.path.join(directory, hashed_file.input_filename)
114             self.fs.copy(hashed_file_path, path, link=False, mode=archive.mode)
115
116         return poolfile
117
118     def install_binary(self, directory, binary, suite, component, allow_tainted=False, fingerprint=None, source_suites=None, extra_source_archives=None):
119         """Install a binary package
120
121         @type  directory: str
122         @param directory: directory the binary package is located in
123
124         @type  binary: L{daklib.upload.Binary}
125         @param binary: binary package to install
126
127         @type  suite: L{daklib.dbconn.Suite}
128         @param suite: target suite
129
130         @type  component: L{daklib.dbconn.Component}
131         @param component: target component
132
133         @type  allow_tainted: bool
134         @param allow_tainted: allow to copy additional files from tainted archives
135
136         @type  fingerprint: L{daklib.dbconn.Fingerprint}
137         @param fingerprint: optional fingerprint
138
139         @type  source_suites: SQLAlchemy subquery for C{daklib.dbconn.Suite} or C{True}
140         @param source_suites: suites to copy the source from if they are not
141                               in C{suite} or C{True} to allow copying from any
142                               suite.
143
144         @type  extra_source_archives: list of L{daklib.dbconn.Archive}
145         @param extra_source_archives: extra archives to copy Built-Using sources from
146
147         @rtype:  L{daklib.dbconn.DBBinary}
148         @return: databse object for the new package
149         """
150         session = self.session
151         control = binary.control
152         maintainer = get_or_set_maintainer(control['Maintainer'], session)
153         architecture = get_architecture(control['Architecture'], session)
154
155         (source_name, source_version) = binary.source
156         source_query = session.query(DBSource).filter_by(source=source_name, version=source_version)
157         source = source_query.filter(DBSource.suites.contains(suite)).first()
158         if source is None:
159             if source_suites != True:
160                 source_query = source_query.join(DBSource.suites) \
161                     .filter(Suite.suite_id == source_suites.c.id)
162             source = source_query.first()
163             if source is None:
164                 raise ArchiveException('{0}: trying to install to {1}, but could not find source'.format(binary.hashed_file.filename, suite.suite_name))
165             self.copy_source(source, suite, component)
166
167         db_file = self._install_file(directory, binary.hashed_file, suite.archive, component, source_name)
168
169         unique = dict(
170             package=control['Package'],
171             version=control['Version'],
172             architecture=architecture,
173             )
174         rest = dict(
175             source=source,
176             maintainer=maintainer,
177             poolfile=db_file,
178             binarytype=binary.type,
179             )
180         # Other attributes that are ignored for purposes of equality with
181         # an existing source
182         rest2 = dict(
183             fingerprint=fingerprint,
184             )
185
186         try:
187             db_binary = session.query(DBBinary).filter_by(**unique).one()
188             for key, value in rest.iteritems():
189                 if getattr(db_binary, key) != value:
190                     raise ArchiveException('{0}: Does not match binary in database.'.format(binary.hashed_file.filename))
191         except NoResultFound:
192             db_binary = DBBinary(**unique)
193             for key, value in rest.iteritems():
194                 setattr(db_binary, key, value)
195             for key, value in rest2.iteritems():
196                 setattr(db_binary, key, value)
197             session.add(db_binary)
198             session.flush()
199             import_metadata_into_db(db_binary, session)
200
201             self._add_built_using(db_binary, binary.hashed_file.filename, control, suite, extra_archives=extra_source_archives)
202
203         if suite not in db_binary.suites:
204             db_binary.suites.append(suite)
205
206         session.flush()
207
208         return db_binary
209
210     def _ensure_extra_source_exists(self, filename, source, archive, extra_archives=None):
211         """ensure source exists in the given archive
212
213         This is intended to be used to check that Built-Using sources exist.
214
215         @type  filename: str
216         @param filename: filename to use in error messages
217
218         @type  source: L{daklib.dbconn.DBSource}
219         @param source: source to look for
220
221         @type  archive: L{daklib.dbconn.Archive}
222         @param archive: archive to look in
223
224         @type  extra_archives: list of L{daklib.dbconn.Archive}
225         @param extra_archives: list of archives to copy the source package from
226                                if it is not yet present in C{archive}
227         """
228         session = self.session
229         db_file = session.query(ArchiveFile).filter_by(file=source.poolfile, archive=archive).first()
230         if db_file is not None:
231             return True
232
233         # Try to copy file from one extra archive
234         if extra_archives is None:
235             extra_archives = []
236         db_file = session.query(ArchiveFile).filter_by(file=source.poolfile).filter(ArchiveFile.archive_id.in_([ a.archive_id for a in extra_archives])).first()
237         if db_file is None:
238             raise ArchiveException('{0}: Built-Using refers to package {1} (= {2}) not in target archive {3}.'.format(filename, source.source, source.version, archive.archive_name))
239
240         source_archive = db_file.archive
241         for dsc_file in source.srcfiles:
242             af = session.query(ArchiveFile).filter_by(file=dsc_file.poolfile, archive=source_archive, component=db_file.component).one()
243             # We were given an explicit list of archives so it is okay to copy from tainted archives.
244             self._copy_file(af.file, archive, db_file.component, allow_tainted=True)
245
246     def _add_built_using(self, db_binary, filename, control, suite, extra_archives=None):
247         """Add Built-Using sources to C{db_binary.extra_sources}
248         """
249         session = self.session
250
251         for bu_source_name, bu_source_version in daklib.utils.parse_built_using(control):
252             bu_source = session.query(DBSource).filter_by(source=bu_source_name, version=bu_source_version).first()
253             if bu_source is None:
254                 raise ArchiveException('{0}: Built-Using refers to non-existing source package {1} (= {2})'.format(filename, bu_source_name, bu_source_version))
255
256             self._ensure_extra_source_exists(filename, bu_source, suite.archive, extra_archives=extra_archives)
257
258             db_binary.extra_sources.append(bu_source)
259
260     def install_source_to_archive(self, directory, source, archive, component, changed_by, allow_tainted=False, fingerprint=None):
261         session = self.session
262         control = source.dsc
263         maintainer = get_or_set_maintainer(control['Maintainer'], session)
264         source_name = control['Source']
265
266         ### Add source package to database
267
268         # We need to install the .dsc first as the DBSource object refers to it.
269         db_file_dsc = self._install_file(directory, source._dsc_file, archive, component, source_name)
270
271         unique = dict(
272             source=source_name,
273             version=control['Version'],
274             )
275         rest = dict(
276             maintainer=maintainer,
277             poolfile=db_file_dsc,
278             dm_upload_allowed=(control.get('DM-Upload-Allowed', 'no') == 'yes'),
279             )
280         # Other attributes that are ignored for purposes of equality with
281         # an existing source
282         rest2 = dict(
283             changedby=changed_by,
284             fingerprint=fingerprint,
285             )
286
287         created = False
288         try:
289             db_source = session.query(DBSource).filter_by(**unique).one()
290             for key, value in rest.iteritems():
291                 if getattr(db_source, key) != value:
292                     raise ArchiveException('{0}: Does not match source in database.'.format(source._dsc_file.filename))
293         except NoResultFound:
294             created = True
295             db_source = DBSource(**unique)
296             for key, value in rest.iteritems():
297                 setattr(db_source, key, value)
298             for key, value in rest2.iteritems():
299                 setattr(db_source, key, value)
300             session.add(db_source)
301             session.flush()
302
303             # Add .dsc file. Other files will be added later.
304             db_dsc_file = DSCFile()
305             db_dsc_file.source = db_source
306             db_dsc_file.poolfile = db_file_dsc
307             session.add(db_dsc_file)
308             session.flush()
309
310         if not created:
311             for f in db_source.srcfiles:
312                 self._copy_file(f.poolfile, archive, component, allow_tainted=allow_tainted)
313             return db_source
314
315         ### Now add remaining files and copy them to the archive.
316
317         for hashed_file in source.files.itervalues():
318             hashed_file_path = os.path.join(directory, hashed_file.input_filename)
319             if os.path.exists(hashed_file_path):
320                 db_file = self._install_file(directory, hashed_file, archive, component, source_name)
321                 session.add(db_file)
322             else:
323                 db_file = self.get_file(hashed_file, source_name)
324                 self._copy_file(db_file, archive, component, allow_tainted=allow_tainted)
325
326             db_dsc_file = DSCFile()
327             db_dsc_file.source = db_source
328             db_dsc_file.poolfile = db_file
329             session.add(db_dsc_file)
330
331         session.flush()
332
333         # Importing is safe as we only arrive here when we did not find the source already installed earlier.
334         import_metadata_into_db(db_source, session)
335
336         # Uploaders are the maintainer and co-maintainers from the Uploaders field
337         db_source.uploaders.append(maintainer)
338         if 'Uploaders' in control:
339             from daklib.textutils import split_uploaders
340             for u in split_uploaders(control['Uploaders']):
341                 db_source.uploaders.append(get_or_set_maintainer(u, session))
342         session.flush()
343
344         return db_source
345
346     def install_source(self, directory, source, suite, component, changed_by, allow_tainted=False, fingerprint=None):
347         """Install a source package
348
349         @type  directory: str
350         @param directory: directory the source package is located in
351
352         @type  source: L{daklib.upload.Source}
353         @param source: source package to install
354
355         @type  suite: L{daklib.dbconn.Suite}
356         @param suite: target suite
357
358         @type  component: L{daklib.dbconn.Component}
359         @param component: target component
360
361         @type  changed_by: L{daklib.dbconn.Maintainer}
362         @param changed_by: person who prepared this version of the package
363
364         @type  allow_tainted: bool
365         @param allow_tainted: allow to copy additional files from tainted archives
366
367         @type  fingerprint: L{daklib.dbconn.Fingerprint}
368         @param fingerprint: optional fingerprint
369
370         @rtype:  L{daklib.dbconn.DBSource}
371         @return: database object for the new source
372         """
373         db_source = self.install_source_to_archive(directory, source, suite.archive, component, changed_by, allow_tainted, fingerprint)
374
375         if suite in db_source.suites:
376             return db_source
377         db_source.suites.append(suite)
378         self.session.flush()
379
380         return db_source
381
382     def _copy_file(self, db_file, archive, component, allow_tainted=False):
383         """Copy a file to the given archive and component
384
385         @type  db_file: L{daklib.dbconn.PoolFile}
386         @param db_file: file to copy
387
388         @type  archive: L{daklib.dbconn.Archive}
389         @param archive: target archive
390
391         @type  component: L{daklib.dbconn.Archive}
392         @param component: target component
393
394         @type  allow_tainted: bool
395         @param allow_tainted: allow to copy from tainted archives (such as NEW)
396         """
397         session = self.session
398
399         if session.query(ArchiveFile).filter_by(archive=archive, component=component, file=db_file).first() is None:
400             query = session.query(ArchiveFile).filter_by(file=db_file)
401             if not allow_tainted:
402                 query = query.join(Archive).filter(Archive.tainted == False)
403
404             source_af = query.first()
405             if source_af is None:
406                 raise ArchiveException('cp: Could not find {0} in any archive.'.format(db_file.filename))
407             target_af = ArchiveFile(archive, component, db_file)
408             session.add(target_af)
409             session.flush()
410             self.fs.copy(source_af.path, target_af.path, link=False, mode=archive.mode)
411
412     def copy_binary(self, db_binary, suite, component, allow_tainted=False, extra_archives=None):
413         """Copy a binary package to the given suite and component
414
415         @type  db_binary: L{daklib.dbconn.DBBinary}
416         @param db_binary: binary to copy
417
418         @type  suite: L{daklib.dbconn.Suite}
419         @param suite: target suite
420
421         @type  component: L{daklib.dbconn.Component}
422         @param component: target component
423
424         @type  allow_tainted: bool
425         @param allow_tainted: allow to copy from tainted archives (such as NEW)
426
427         @type  extra_archives: list of L{daklib.dbconn.Archive}
428         @param extra_archives: extra archives to copy Built-Using sources from
429         """
430         session = self.session
431         archive = suite.archive
432         if archive.tainted:
433             allow_tainted = True
434
435         filename = db_binary.poolfile.filename
436
437         # make sure source is present in target archive
438         db_source = db_binary.source
439         if session.query(ArchiveFile).filter_by(archive=archive, file=db_source.poolfile).first() is None:
440             raise ArchiveException('{0}: cannot copy to {1}: source is not present in target archive'.format(filename, suite.suite_name))
441
442         # make sure built-using packages are present in target archive
443         for db_source in db_binary.extra_sources:
444             self._ensure_extra_source_exists(filename, db_source, archive, extra_archives=extra_archives)
445
446         # copy binary
447         db_file = db_binary.poolfile
448         self._copy_file(db_file, suite.archive, component, allow_tainted=allow_tainted)
449         if suite not in db_binary.suites:
450             db_binary.suites.append(suite)
451         self.session.flush()
452
453     def copy_source(self, db_source, suite, component, allow_tainted=False):
454         """Copy a source package to the given suite and component
455
456         @type  db_source: L{daklib.dbconn.DBSource}
457         @param db_source: source to copy
458
459         @type  suite: L{daklib.dbconn.Suite}
460         @param suite: target suite
461
462         @type  component: L{daklib.dbconn.Component}
463         @param component: target component
464
465         @type  allow_tainted: bool
466         @param allow_tainted: allow to copy from tainted archives (such as NEW)
467         """
468         archive = suite.archive
469         if archive.tainted:
470             allow_tainted = True
471         for db_dsc_file in db_source.srcfiles:
472             self._copy_file(db_dsc_file.poolfile, archive, component, allow_tainted=allow_tainted)
473         if suite not in db_source.suites:
474             db_source.suites.append(suite)
475         self.session.flush()
476
477     def remove_file(self, db_file, archive, component):
478         """Remove a file from a given archive and component
479
480         @type  db_file: L{daklib.dbconn.PoolFile}
481         @param db_file: file to remove
482
483         @type  archive: L{daklib.dbconn.Archive}
484         @param archive: archive to remove the file from
485
486         @type  component: L{daklib.dbconn.Component}
487         @param component: component to remove the file from
488         """
489         af = self.session.query(ArchiveFile).filter_by(file=db_file, archive=archive, component=component)
490         self.fs.unlink(af.path)
491         self.session.delete(af)
492
493     def remove_binary(self, binary, suite):
494         """Remove a binary from a given suite and component
495
496         @type  binary: L{daklib.dbconn.DBBinary}
497         @param binary: binary to remove
498
499         @type  suite: L{daklib.dbconn.Suite}
500         @param suite: suite to remove the package from
501         """
502         binary.suites.remove(suite)
503         self.session.flush()
504
505     def remove_source(self, source, suite):
506         """Remove a source from a given suite and component
507
508         @type  source: L{daklib.dbconn.DBSource}
509         @param source: source to remove
510
511         @type  suite: L{daklib.dbconn.Suite}
512         @param suite: suite to remove the package from
513
514         @raise ArchiveException: source package is still referenced by other
515                                  binaries in the suite
516         """
517         session = self.session
518
519         query = session.query(DBBinary).filter_by(source=source) \
520             .filter(DBBinary.suites.contains(suite))
521         if query.first() is not None:
522             raise ArchiveException('src:{0} is still used by binaries in suite {1}'.format(source.source, suite.suite_name))
523
524         source.suites.remove(suite)
525         session.flush()
526
527     def commit(self):
528         """commit changes"""
529         try:
530             self.session.commit()
531             self.fs.commit()
532         finally:
533             self.session.rollback()
534             self.fs.rollback()
535
536     def rollback(self):
537         """rollback changes"""
538         self.session.rollback()
539         self.fs.rollback()
540
541     def flush(self):
542         self.session.flush()
543
544     def __enter__(self):
545         return self
546
547     def __exit__(self, type, value, traceback):
548         if type is None:
549             self.commit()
550         else:
551             self.rollback()
552         return None
553
554 def source_component_from_package_list(package_list, suite):
555     """Get component for a source package
556
557     This function will look at the Package-List field to determine the
558     component the source package belongs to. This is the first component
559     the source package provides binaries for (first with respect to the
560     ordering of components).
561
562     It the source package has no Package-List field, None is returned.
563
564     @type  package_list: L{daklib.packagelist.PackageList}
565     @param package_list: package list of the source to get the override for
566
567     @type  suite: L{daklib.dbconn.Suite}
568     @param suite: suite to consider for binaries produced
569
570     @rtype:  L{daklib.dbconn.Component} or C{None}
571     @return: component for the given source or C{None}
572     """
573     if package_list.fallback:
574         return None
575     session = object_session(suite)
576     packages = package_list.packages_for_suite(suite)
577     components = set(p.component for p in packages)
578     query = session.query(Component).order_by(Component.ordering) \
579             .filter(Component.component_name.in_(components))
580     return query.first()
581
582 class ArchiveUpload(object):
583     """handle an upload
584
585     This class can be used in a with-statement::
586
587        with ArchiveUpload(...) as upload:
588           ...
589
590     Doing so will automatically run any required cleanup and also rollback the
591     transaction if it was not committed.
592     """
593     def __init__(self, directory, changes, keyrings):
594         self.transaction = ArchiveTransaction()
595         """transaction used to handle the upload
596         @type: L{daklib.archive.ArchiveTransaction}
597         """
598
599         self.session = self.transaction.session
600         """database session"""
601
602         self.original_directory = directory
603         self.original_changes = changes
604
605         self.changes = None
606         """upload to process
607         @type: L{daklib.upload.Changes}
608         """
609
610         self.directory = None
611         """directory with temporary copy of files. set by C{prepare}
612         @type: str
613         """
614
615         self.keyrings = keyrings
616
617         self.fingerprint = self.session.query(Fingerprint).filter_by(fingerprint=changes.primary_fingerprint).one()
618         """fingerprint of the key used to sign the upload
619         @type: L{daklib.dbconn.Fingerprint}
620         """
621
622         self.reject_reasons = []
623         """reasons why the upload cannot by accepted
624         @type: list of str
625         """
626
627         self.warnings = []
628         """warnings
629         @note: Not used yet.
630         @type: list of str
631         """
632
633         self.final_suites = None
634
635         self.new = False
636         """upload is NEW. set by C{check}
637         @type: bool
638         """
639
640         self._checked = False
641         """checks passes. set by C{check}
642         @type: bool
643         """
644
645         self._new_queue = self.session.query(PolicyQueue).filter_by(queue_name='new').one()
646         self._new = self._new_queue.suite
647
648     def warn(self, message):
649         """add a warning message
650
651         Adds a warning message that can later be seen in C{self.warnings}
652
653         @type  message: string
654         @param message: warning message
655         """
656         self.warnings.append(message)
657
658     def prepare(self):
659         """prepare upload for further processing
660
661         This copies the files involved to a temporary directory.  If you use
662         this method directly, you have to remove the directory given by the
663         C{directory} attribute later on your own.
664
665         Instead of using the method directly, you can also use a with-statement::
666
667            with ArchiveUpload(...) as upload:
668               ...
669
670         This will automatically handle any required cleanup.
671         """
672         assert self.directory is None
673         assert self.original_changes.valid_signature
674
675         cnf = Config()
676         session = self.transaction.session
677
678         group = cnf.get('Dinstall::UnprivGroup') or None
679         self.directory = utils.temp_dirname(parent=cnf.get('Dir::TempPath'),
680                                             mode=0o2750, group=group)
681         with FilesystemTransaction() as fs:
682             src = os.path.join(self.original_directory, self.original_changes.filename)
683             dst = os.path.join(self.directory, self.original_changes.filename)
684             fs.copy(src, dst, mode=0o640)
685
686             self.changes = upload.Changes(self.directory, self.original_changes.filename, self.keyrings)
687
688             for f in self.changes.files.itervalues():
689                 src = os.path.join(self.original_directory, f.filename)
690                 dst = os.path.join(self.directory, f.filename)
691                 if not os.path.exists(src):
692                     continue
693                 fs.copy(src, dst, mode=0o640)
694
695             source = None
696             try:
697                 source = self.changes.source
698             except Exception:
699                 # Do not raise an exception here if the .dsc is invalid.
700                 pass
701
702             if source is not None:
703                 for f in source.files.itervalues():
704                     src = os.path.join(self.original_directory, f.filename)
705                     dst = os.path.join(self.directory, f.filename)
706                     if not os.path.exists(dst):
707                         try:
708                             db_file = self.transaction.get_file(f, source.dsc['Source'], check_hashes=False)
709                             db_archive_file = session.query(ArchiveFile).filter_by(file=db_file).first()
710                             fs.copy(db_archive_file.path, dst, mode=0o640)
711                         except KeyError:
712                             # Ignore if get_file could not find it. Upload will
713                             # probably be rejected later.
714                             pass
715
716     def unpacked_source(self):
717         """Path to unpacked source
718
719         Get path to the unpacked source. This method does unpack the source
720         into a temporary directory under C{self.directory} if it has not
721         been done so already.
722
723         @rtype:  str or C{None}
724         @return: string giving the path to the unpacked source directory
725                  or C{None} if no source was included in the upload.
726         """
727         assert self.directory is not None
728
729         source = self.changes.source
730         if source is None:
731             return None
732         dsc_path = os.path.join(self.directory, source._dsc_file.filename)
733
734         sourcedir = os.path.join(self.directory, 'source')
735         if not os.path.exists(sourcedir):
736             devnull = open('/dev/null', 'w')
737             daklib.daksubprocess.check_call(["dpkg-source", "--no-copy", "--no-check", "-x", dsc_path, sourcedir], shell=False, stdout=devnull)
738         if not os.path.isdir(sourcedir):
739             raise Exception("{0} is not a directory after extracting source package".format(sourcedir))
740         return sourcedir
741
742     def _map_suite(self, suite_name):
743         for rule in Config().value_list("SuiteMappings"):
744             fields = rule.split()
745             rtype = fields[0]
746             if rtype == "map" or rtype == "silent-map":
747                 (src, dst) = fields[1:3]
748                 if src == suite_name:
749                     suite_name = dst
750                     if rtype != "silent-map":
751                         self.warnings.append('Mapping {0} to {1}.'.format(src, dst))
752             elif rtype == "ignore":
753                 ignored = fields[1]
754                 if suite_name == ignored:
755                     self.warnings.append('Ignoring target suite {0}.'.format(ignored))
756                     suite_name = None
757             elif rtype == "reject":
758                 rejected = fields[1]
759                 if suite_name == rejected:
760                     raise checks.Reject('Uploads to {0} are not accepted.'.format(rejected))
761             ## XXX: propup-version and map-unreleased not yet implemented
762         return suite_name
763
764     def _mapped_suites(self):
765         """Get target suites after mappings
766
767         @rtype:  list of L{daklib.dbconn.Suite}
768         @return: list giving the mapped target suites of this upload
769         """
770         session = self.session
771
772         suite_names = []
773         for dist in self.changes.distributions:
774             suite_name = self._map_suite(dist)
775             if suite_name is not None:
776                 suite_names.append(suite_name)
777
778         suites = session.query(Suite).filter(Suite.suite_name.in_(suite_names))
779         return suites
780
781     def _check_new_binary_overrides(self, suite):
782         new = False
783
784         binaries = self.changes.binaries
785         source = self.changes.source
786         if source is not None and not source.package_list.fallback:
787             packages = source.package_list.packages_for_suite(suite)
788             binaries = [ entry for entry in packages ]
789
790         for b in binaries:
791             override = self._binary_override(suite, b)
792             if override is None:
793                 self.warnings.append('binary:{0} is NEW.'.format(b.name))
794                 new = True
795
796         return new
797
798     def _check_new(self, suite):
799         """Check if upload is NEW
800
801         An upload is NEW if it has binary or source packages that do not have
802         an override in C{suite} OR if it references files ONLY in a tainted
803         archive (eg. when it references files in NEW).
804
805         @rtype:  bool
806         @return: C{True} if the upload is NEW, C{False} otherwise
807         """
808         session = self.session
809         new = False
810
811         # Check for missing overrides
812         if self._check_new_binary_overrides(suite):
813             new = True
814         if self.changes.source is not None:
815             override = self._source_override(suite, self.changes.source)
816             if override is None:
817                 self.warnings.append('source:{0} is NEW.'.format(self.changes.source.dsc['Source']))
818                 new = True
819
820         # Check if we reference a file only in a tainted archive
821         files = self.changes.files.values()
822         if self.changes.source is not None:
823             files.extend(self.changes.source.files.values())
824         for f in files:
825             query = session.query(ArchiveFile).join(PoolFile).filter(PoolFile.sha1sum == f.sha1sum)
826             query_untainted = query.join(Archive).filter(Archive.tainted == False)
827
828             in_archive = (query.first() is not None)
829             in_untainted_archive = (query_untainted.first() is not None)
830
831             if in_archive and not in_untainted_archive:
832                 self.warnings.append('{0} is only available in NEW.'.format(f.filename))
833                 new = True
834
835         return new
836
837     def _final_suites(self):
838         session = self.session
839
840         mapped_suites = self._mapped_suites()
841         final_suites = set()
842
843         for suite in mapped_suites:
844             overridesuite = suite
845             if suite.overridesuite is not None:
846                 overridesuite = session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
847             if self._check_new(overridesuite):
848                 self.new = True
849             final_suites.add(suite)
850
851         return final_suites
852
853     def _binary_override(self, suite, binary):
854         """Get override entry for a binary
855
856         @type  suite: L{daklib.dbconn.Suite}
857         @param suite: suite to get override for
858
859         @type  binary: L{daklib.upload.Binary} or L{daklib.packagelist.PackageListEntry}
860         @param binary: binary to get override for
861
862         @rtype:  L{daklib.dbconn.Override} or C{None}
863         @return: override for the given binary or C{None}
864         """
865         if suite.overridesuite is not None:
866             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
867
868         mapped_component = get_mapped_component(binary.component)
869         if mapped_component is None:
870             return None
871
872         query = self.session.query(Override).filter_by(suite=suite, package=binary.name) \
873                 .join(Component).filter(Component.component_name == mapped_component.component_name) \
874                 .join(OverrideType).filter(OverrideType.overridetype == binary.type)
875
876         try:
877             return query.one()
878         except NoResultFound:
879             return None
880
881     def _source_override(self, suite, source):
882         """Get override entry for a source
883
884         @type  suite: L{daklib.dbconn.Suite}
885         @param suite: suite to get override for
886
887         @type  source: L{daklib.upload.Source}
888         @param source: source to get override for
889
890         @rtype:  L{daklib.dbconn.Override} or C{None}
891         @return: override for the given source or C{None}
892         """
893         if suite.overridesuite is not None:
894             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
895
896         query = self.session.query(Override).filter_by(suite=suite, package=source.dsc['Source']) \
897                 .join(OverrideType).filter(OverrideType.overridetype == 'dsc')
898
899         component = source_component_from_package_list(source.package_list, suite)
900         if component is not None:
901             query = query.filter(Override.component == component)
902
903         try:
904             return query.one()
905         except NoResultFound:
906             return None
907
908     def _binary_component(self, suite, binary, only_overrides=True):
909         """get component for a binary
910
911         By default this will only look at overrides to get the right component;
912         if C{only_overrides} is C{False} this method will also look at the
913         Section field.
914
915         @type  suite: L{daklib.dbconn.Suite}
916
917         @type  binary: L{daklib.upload.Binary}
918
919         @type  only_overrides: bool
920         @param only_overrides: only use overrides to get the right component
921
922         @rtype: L{daklib.dbconn.Component} or C{None}
923         """
924         override = self._binary_override(suite, binary)
925         if override is not None:
926             return override.component
927         if only_overrides:
928             return None
929         return get_mapped_component(binary.component, self.session)
930
931     def check(self, force=False):
932         """run checks against the upload
933
934         @type  force: bool
935         @param force: ignore failing forcable checks
936
937         @rtype:  bool
938         @return: C{True} if all checks passed, C{False} otherwise
939         """
940         # XXX: needs to be better structured.
941         assert self.changes.valid_signature
942
943         try:
944             # Validate signatures and hashes before we do any real work:
945             for chk in (
946                     checks.SignatureAndHashesCheck,
947                     checks.SignatureTimestampCheck,
948                     checks.ChangesCheck,
949                     checks.ExternalHashesCheck,
950                     checks.SourceCheck,
951                     checks.BinaryCheck,
952                     checks.BinaryTimestampCheck,
953                     checks.SingleDistributionCheck,
954                     ):
955                 chk().check(self)
956
957             final_suites = self._final_suites()
958             if len(final_suites) == 0:
959                 self.reject_reasons.append('No target suite found. Please check your target distribution and that you uploaded to the right archive.')
960                 return False
961
962             self.final_suites = final_suites
963
964             for chk in (
965                     checks.TransitionCheck,
966                     checks.ACLCheck,
967                     checks.NoSourceOnlyCheck,
968                     checks.LintianCheck,
969                     ):
970                 chk().check(self)
971
972             for chk in (
973                     checks.ACLCheck,
974                     checks.SourceFormatCheck,
975                     checks.SuiteArchitectureCheck,
976                     checks.VersionCheck,
977                     ):
978                 for suite in final_suites:
979                     chk().per_suite_check(self, suite)
980
981             if len(self.reject_reasons) != 0:
982                 return False
983
984             self._checked = True
985             return True
986         except checks.Reject as e:
987             self.reject_reasons.append(unicode(e))
988         except Exception as e:
989             self.reject_reasons.append("Processing raised an exception: {0}.\n{1}".format(e, traceback.format_exc()))
990         return False
991
992     def _install_to_suite(self, suite, source_component_func, binary_component_func, source_suites=None, extra_source_archives=None):
993         """Install upload to the given suite
994
995         @type  suite: L{daklib.dbconn.Suite}
996         @param suite: suite to install the package into. This is the real suite,
997                       ie. after any redirection to NEW or a policy queue
998
999         @param source_component_func: function to get the L{daklib.dbconn.Component}
1000                                       for a L{daklib.upload.Source} object
1001
1002         @param binary_component_func: function to get the L{daklib.dbconn.Component}
1003                                       for a L{daklib.upload.Binary} object
1004
1005         @param source_suites: see L{daklib.archive.ArchiveTransaction.install_binary}
1006
1007         @param extra_source_archives: see L{daklib.archive.ArchiveTransaction.install_binary}
1008
1009         @return: tuple with two elements. The first is a L{daklib.dbconn.DBSource}
1010                  object for the install source or C{None} if no source was
1011                  included. The second is a list of L{daklib.dbconn.DBBinary}
1012                  objects for the installed binary packages.
1013         """
1014         # XXX: move this function to ArchiveTransaction?
1015
1016         control = self.changes.changes
1017         changed_by = get_or_set_maintainer(control.get('Changed-By', control['Maintainer']), self.session)
1018
1019         if source_suites is None:
1020             source_suites = self.session.query(Suite).join((VersionCheck, VersionCheck.reference_id == Suite.suite_id)).filter(VersionCheck.check == 'Enhances').filter(VersionCheck.suite == suite).subquery()
1021
1022         source = self.changes.source
1023         if source is not None:
1024             component = source_component_func(source)
1025             db_source = self.transaction.install_source(
1026                 self.directory,
1027                 source,
1028                 suite,
1029                 component,
1030                 changed_by,
1031                 fingerprint=self.fingerprint
1032             )
1033         else:
1034             db_source = None
1035
1036         db_binaries = []
1037         for binary in self.changes.binaries:
1038             copy_to_suite = suite
1039             if utils.is_in_debug_section(binary.control) and suite.debug_suite is not None:
1040                 copy_to_suite = suite.debug_suite
1041
1042             component = binary_component_func(binary)
1043             db_binary = self.transaction.install_binary(
1044                 self.directory,
1045                 binary,
1046                 copy_to_suite,
1047                 component,
1048                 fingerprint=self.fingerprint,
1049                 source_suites=source_suites,
1050                 extra_source_archives=extra_source_archives
1051             )
1052             db_binaries.append(db_binary)
1053
1054         if suite.copychanges:
1055             src = os.path.join(self.directory, self.changes.filename)
1056             dst = os.path.join(suite.archive.path, 'dists', suite.suite_name, self.changes.filename)
1057             self.transaction.fs.copy(src, dst, mode=suite.archive.mode)
1058
1059         return (db_source, db_binaries)
1060
1061     def _install_changes(self):
1062         assert self.changes.valid_signature
1063         control = self.changes.changes
1064         session = self.transaction.session
1065         config = Config()
1066
1067         changelog_id = None
1068         # Only add changelog for sourceful uploads and binNMUs
1069         if 'source' in self.changes.architectures or re_bin_only_nmu.search(control['Version']):
1070             query = 'INSERT INTO changelogs_text (changelog) VALUES (:changelog) RETURNING id'
1071             changelog_id = session.execute(query, {'changelog': control['Changes']}).scalar()
1072             assert changelog_id is not None
1073
1074         db_changes = DBChange()
1075         db_changes.changesname = self.changes.filename
1076         db_changes.source = control['Source']
1077         db_changes.binaries = control.get('Binary', None)
1078         db_changes.architecture = control['Architecture']
1079         db_changes.version = control['Version']
1080         db_changes.distribution = control['Distribution']
1081         db_changes.urgency = control['Urgency']
1082         db_changes.maintainer = control['Maintainer']
1083         db_changes.changedby = control.get('Changed-By', control['Maintainer'])
1084         db_changes.date = control['Date']
1085         db_changes.fingerprint = self.fingerprint.fingerprint
1086         db_changes.changelog_id = changelog_id
1087         db_changes.closes = self.changes.closed_bugs
1088
1089         try:
1090             self.transaction.session.add(db_changes)
1091             self.transaction.session.flush()
1092         except sqlalchemy.exc.IntegrityError:
1093             raise ArchiveException('{0} is already known.'.format(self.changes.filename))
1094
1095         return db_changes
1096
1097     def _install_policy(self, policy_queue, target_suite, db_changes, db_source, db_binaries):
1098         u = PolicyQueueUpload()
1099         u.policy_queue = policy_queue
1100         u.target_suite = target_suite
1101         u.changes = db_changes
1102         u.source = db_source
1103         u.binaries = db_binaries
1104         self.transaction.session.add(u)
1105         self.transaction.session.flush()
1106
1107         dst = os.path.join(policy_queue.path, self.changes.filename)
1108         self.transaction.fs.copy(self.changes.path, dst, mode=policy_queue.change_perms)
1109
1110         return u
1111
1112     def try_autobyhand(self):
1113         """Try AUTOBYHAND
1114
1115         Try to handle byhand packages automatically.
1116
1117         @rtype:  list of L{daklib.upload.HashedFile}
1118         @return: list of remaining byhand files
1119         """
1120         assert len(self.reject_reasons) == 0
1121         assert self.changes.valid_signature
1122         assert self.final_suites is not None
1123         assert self._checked
1124
1125         byhand = self.changes.byhand_files
1126         if len(byhand) == 0:
1127             return True
1128
1129         suites = list(self.final_suites)
1130         assert len(suites) == 1, "BYHAND uploads must be to a single suite"
1131         suite = suites[0]
1132
1133         cnf = Config()
1134         control = self.changes.changes
1135         automatic_byhand_packages = cnf.subtree("AutomaticByHandPackages")
1136
1137         remaining = []
1138         for f in byhand:
1139             if '_' in f.filename:
1140                 parts = f.filename.split('_', 2)
1141                 if len(parts) != 3:
1142                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1143                     remaining.append(f)
1144                     continue
1145
1146                 package, version, archext = parts
1147                 arch, ext = archext.split('.', 1)
1148             else:
1149                 parts = f.filename.split('.')
1150                 if len(parts) < 2:
1151                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1152                     remaining.append(f)
1153                     continue
1154
1155                 package = parts[0]
1156                 version = '0'
1157                 arch = 'all'
1158                 ext = parts[-1]
1159
1160             try:
1161                 rule = automatic_byhand_packages.subtree(package)
1162             except KeyError:
1163                 remaining.append(f)
1164                 continue
1165
1166             if rule['Source'] != self.changes.source_name \
1167                     or rule['Section'] != f.section \
1168                     or ('Extension' in rule and rule['Extension'] != ext):
1169                 remaining.append(f)
1170                 continue
1171
1172             script = rule['Script']
1173             retcode = daklib.daksubprocess.call([script, os.path.join(self.directory, f.filename), control['Version'], arch, os.path.join(self.directory, self.changes.filename)], shell=False)
1174             if retcode != 0:
1175                 print "W: error processing {0}.".format(f.filename)
1176                 remaining.append(f)
1177
1178         return len(remaining) == 0
1179
1180     def _install_byhand(self, policy_queue_upload, hashed_file):
1181         """install byhand file
1182
1183         @type  policy_queue_upload: L{daklib.dbconn.PolicyQueueUpload}
1184
1185         @type  hashed_file: L{daklib.upload.HashedFile}
1186         """
1187         fs = self.transaction.fs
1188         session = self.transaction.session
1189         policy_queue = policy_queue_upload.policy_queue
1190
1191         byhand_file = PolicyQueueByhandFile()
1192         byhand_file.upload = policy_queue_upload
1193         byhand_file.filename = hashed_file.filename
1194         session.add(byhand_file)
1195         session.flush()
1196
1197         src = os.path.join(self.directory, hashed_file.filename)
1198         dst = os.path.join(policy_queue.path, hashed_file.filename)
1199         fs.copy(src, dst, mode=policy_queue.change_perms)
1200
1201         return byhand_file
1202
1203     def _do_bts_versiontracking(self):
1204         cnf = Config()
1205         fs = self.transaction.fs
1206
1207         btsdir = cnf.get('Dir::BTSVersionTrack')
1208         if btsdir is None or btsdir == '':
1209             return
1210
1211         base = os.path.join(btsdir, self.changes.filename[:-8])
1212
1213         # version history
1214         sourcedir = self.unpacked_source()
1215         if sourcedir is not None:
1216             fh = open(os.path.join(sourcedir, 'debian', 'changelog'), 'r')
1217             versions = fs.create("{0}.versions".format(base), mode=0o644)
1218             for line in fh.readlines():
1219                 if re_changelog_versions.match(line):
1220                     versions.write(line)
1221             fh.close()
1222             versions.close()
1223
1224         # binary -> source mapping
1225         debinfo = fs.create("{0}.debinfo".format(base), mode=0o644)
1226         for binary in self.changes.binaries:
1227             control = binary.control
1228             source_package, source_version = binary.source
1229             line = " ".join([control['Package'], control['Version'], control['Architecture'], source_package, source_version])
1230             print >>debinfo, line
1231         debinfo.close()
1232
1233     def _policy_queue(self, suite):
1234         if suite.policy_queue is not None:
1235             return suite.policy_queue
1236         return None
1237
1238     def install(self):
1239         """install upload
1240
1241         Install upload to a suite or policy queue.  This method does B{not}
1242         handle uploads to NEW.
1243
1244         You need to have called the C{check} method before calling this method.
1245         """
1246         assert len(self.reject_reasons) == 0
1247         assert self.changes.valid_signature
1248         assert self.final_suites is not None
1249         assert self._checked
1250         assert not self.new
1251
1252         db_changes = self._install_changes()
1253
1254         for suite in self.final_suites:
1255             overridesuite = suite
1256             if suite.overridesuite is not None:
1257                 overridesuite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
1258
1259             policy_queue = self._policy_queue(suite)
1260
1261             redirected_suite = suite
1262             if policy_queue is not None:
1263                 redirected_suite = policy_queue.suite
1264
1265             # source can be in the suite we install to or any suite we enhance
1266             source_suite_ids = set([suite.suite_id, redirected_suite.suite_id])
1267             for enhanced_suite_id, in self.session.query(VersionCheck.reference_id) \
1268                     .filter(VersionCheck.suite_id.in_(source_suite_ids)) \
1269                     .filter(VersionCheck.check == 'Enhances'):
1270                 source_suite_ids.add(enhanced_suite_id)
1271
1272             source_suites = self.session.query(Suite).filter(Suite.suite_id.in_(source_suite_ids)).subquery()
1273
1274             source_component_func = lambda source: self._source_override(overridesuite, source).component
1275             binary_component_func = lambda binary: self._binary_component(overridesuite, binary)
1276
1277             (db_source, db_binaries) = self._install_to_suite(redirected_suite, source_component_func, binary_component_func, source_suites=source_suites, extra_source_archives=[suite.archive])
1278
1279             if policy_queue is not None:
1280                 self._install_policy(policy_queue, suite, db_changes, db_source, db_binaries)
1281
1282             # copy to build queues
1283             if policy_queue is None or policy_queue.send_to_build_queues:
1284                 for build_queue in suite.copy_queues:
1285                     self._install_to_suite(build_queue.suite, source_component_func, binary_component_func, source_suites=source_suites, extra_source_archives=[suite.archive])
1286
1287         self._do_bts_versiontracking()
1288
1289     def install_to_new(self):
1290         """install upload to NEW
1291
1292         Install upload to NEW.  This method does B{not} handle regular uploads
1293         to suites or policy queues.
1294
1295         You need to have called the C{check} method before calling this method.
1296         """
1297         # Uploads to NEW are special as we don't have overrides.
1298         assert len(self.reject_reasons) == 0
1299         assert self.changes.valid_signature
1300         assert self.final_suites is not None
1301
1302         source = self.changes.source
1303         binaries = self.changes.binaries
1304         byhand = self.changes.byhand_files
1305
1306         # we need a suite to guess components
1307         suites = list(self.final_suites)
1308         assert len(suites) == 1, "NEW uploads must be to a single suite"
1309         suite = suites[0]
1310
1311         # decide which NEW queue to use
1312         if suite.new_queue is None:
1313             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='new').one()
1314         else:
1315             new_queue = suite.new_queue
1316         if len(byhand) > 0:
1317             # There is only one global BYHAND queue
1318             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='byhand').one()
1319         new_suite = new_queue.suite
1320
1321
1322         def binary_component_func(binary):
1323             return self._binary_component(suite, binary, only_overrides=False)
1324
1325         # guess source component
1326         # XXX: should be moved into an extra method
1327         binary_component_names = set()
1328         for binary in binaries:
1329             component = binary_component_func(binary)
1330             binary_component_names.add(component.component_name)
1331         source_component_name = None
1332         for c in self.session.query(Component).order_by(Component.component_id):
1333             guess = c.component_name
1334             if guess in binary_component_names:
1335                 source_component_name = guess
1336                 break
1337         if source_component_name is None:
1338             source_component = self.session.query(Component).order_by(Component.component_id).first()
1339         else:
1340             source_component = self.session.query(Component).filter_by(component_name=source_component_name).one()
1341         source_component_func = lambda source: source_component
1342
1343         db_changes = self._install_changes()
1344         (db_source, db_binaries) = self._install_to_suite(new_suite, source_component_func, binary_component_func, source_suites=True, extra_source_archives=[suite.archive])
1345         policy_upload = self._install_policy(new_queue, suite, db_changes, db_source, db_binaries)
1346
1347         for f in byhand:
1348             self._install_byhand(policy_upload, f)
1349
1350         self._do_bts_versiontracking()
1351
1352     def commit(self):
1353         """commit changes"""
1354         self.transaction.commit()
1355
1356     def rollback(self):
1357         """rollback changes"""
1358         self.transaction.rollback()
1359
1360     def __enter__(self):
1361         self.prepare()
1362         return self
1363
1364     def __exit__(self, type, value, traceback):
1365         if self.directory is not None:
1366             shutil.rmtree(self.directory)
1367             self.directory = None
1368         self.changes = None
1369         self.transaction.rollback()
1370         return None