]> git.decadent.org.uk Git - dak.git/blob - daklib/queue.py
Merge commit 'ftpmaster/master'
[dak.git] / daklib / queue.py
1 #!/usr/bin/env python
2 # vim:set et sw=4:
3
4 """
5 Queue utility functions for dak
6
7 @contact: Debian FTP Master <ftpmaster@debian.org>
8 @copyright: 2001 - 2006 James Troup <james@nocrew.org>
9 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ###############################################################################
28
29 import errno
30 import os
31 import pg
32 import stat
33 import sys
34 import time
35 import apt_inst
36 import apt_pkg
37 import utils
38 import commands
39 import shutil
40 import textwrap
41 import tempfile
42 from types import *
43
44 import yaml
45
46 from dak_exceptions import *
47 from changes import *
48 from regexes import *
49 from config import Config
50 from holding import Holding
51 from dbconn import *
52 from summarystats import SummaryStats
53 from utils import parse_changes, check_dsc_files
54 from textutils import fix_maintainer
55 from binary import Binary
56
57 ###############################################################################
58
59 def get_type(f, session):
60     """
61     Get the file type of C{f}
62
63     @type f: dict
64     @param f: file entry from Changes object
65
66     @type session: SQLA Session
67     @param session: SQL Alchemy session object
68
69     @rtype: string
70     @return: filetype
71
72     """
73     # Determine the type
74     if f.has_key("dbtype"):
75         file_type = f["dbtype"]
76     elif re_source_ext.match(f["type"]):
77         file_type = "dsc"
78     else:
79         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (file_type))
80
81     # Validate the override type
82     type_id = get_override_type(file_type, session)
83     if type_id is None:
84         utils.fubar("invalid type (%s) for new.  Say wha?" % (file_type))
85
86     return file_type
87
88 ################################################################################
89
90 # Determine what parts in a .changes are NEW
91
92 def determine_new(changes, files, warn=1):
93     """
94     Determine what parts in a C{changes} file are NEW.
95
96     @type changes: Upload.Pkg.changes dict
97     @param changes: Changes dictionary
98
99     @type files: Upload.Pkg.files dict
100     @param files: Files dictionary
101
102     @type warn: bool
103     @param warn: Warn if overrides are added for (old)stable
104
105     @rtype: dict
106     @return: dictionary of NEW components.
107
108     """
109     new = {}
110
111     session = DBConn().session()
112
113     # Build up a list of potentially new things
114     for name, f in files.items():
115         # Skip byhand elements
116         if f["type"] == "byhand":
117             continue
118         pkg = f["package"]
119         priority = f["priority"]
120         section = f["section"]
121         file_type = get_type(f, session)
122         component = f["component"]
123
124         if file_type == "dsc":
125             priority = "source"
126
127         if not new.has_key(pkg):
128             new[pkg] = {}
129             new[pkg]["priority"] = priority
130             new[pkg]["section"] = section
131             new[pkg]["type"] = file_type
132             new[pkg]["component"] = component
133             new[pkg]["files"] = []
134         else:
135             old_type = new[pkg]["type"]
136             if old_type != file_type:
137                 # source gets trumped by deb or udeb
138                 if old_type == "dsc":
139                     new[pkg]["priority"] = priority
140                     new[pkg]["section"] = section
141                     new[pkg]["type"] = file_type
142                     new[pkg]["component"] = component
143
144         new[pkg]["files"].append(name)
145
146         if f.has_key("othercomponents"):
147             new[pkg]["othercomponents"] = f["othercomponents"]
148
149     for suite in changes["suite"].keys():
150         for pkg in new.keys():
151             ql = get_override(pkg, suite, new[pkg]["component"], new[pkg]["type"], session)
152             if len(ql) > 0:
153                 for file_entry in new[pkg]["files"]:
154                     if files[file_entry].has_key("new"):
155                         del files[file_entry]["new"]
156                 del new[pkg]
157
158     if warn:
159         for s in ['stable', 'oldstable']:
160             if changes["suite"].has_key(s):
161                 print "WARNING: overrides will be added for %s!" % s
162         for pkg in new.keys():
163             if new[pkg].has_key("othercomponents"):
164                 print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])
165
166     session.close()
167
168     return new
169
170 ################################################################################
171
172 def check_valid(new):
173     """
174     Check if section and priority for NEW packages exist in database.
175     Additionally does sanity checks:
176       - debian-installer packages have to be udeb (or source)
177       - non debian-installer packages can not be udeb
178       - source priority can only be assigned to dsc file types
179
180     @type new: dict
181     @param new: Dict of new packages with their section, priority and type.
182
183     """
184     for pkg in new.keys():
185         section_name = new[pkg]["section"]
186         priority_name = new[pkg]["priority"]
187         file_type = new[pkg]["type"]
188
189         section = get_section(section_name)
190         if section is None:
191             new[pkg]["section id"] = -1
192         else:
193             new[pkg]["section id"] = section.section_id
194
195         priority = get_priority(priority_name)
196         if priority is None:
197             new[pkg]["priority id"] = -1
198         else:
199             new[pkg]["priority id"] = priority.priority_id
200
201         # Sanity checks
202         di = section_name.find("debian-installer") != -1
203
204         # If d-i, we must be udeb and vice-versa
205         if     (di and file_type not in ("udeb", "dsc")) or \
206            (not di and file_type == "udeb"):
207             new[pkg]["section id"] = -1
208
209         # If dsc we need to be source and vice-versa
210         if (priority == "source" and file_type != "dsc") or \
211            (priority != "source" and file_type == "dsc"):
212             new[pkg]["priority id"] = -1
213
214 ###############################################################################
215
216 def lookup_uid_from_fingerprint(fpr, session):
217     uid = None
218     uid_name = ""
219     # This is a stupid default, but see the comments below
220     is_dm = False
221
222     user = get_uid_from_fingerprint(fpr, session)
223
224     if user is not None:
225         uid = user.uid
226         if user.name is None:
227             uid_name = ''
228         else:
229             uid_name = user.name
230
231         # Check the relevant fingerprint (which we have to have)
232         for f in user.fingerprint:
233             if f.fingerprint == fpr:
234                 is_dm = f.keyring.debian_maintainer
235                 break
236
237     return (uid, uid_name, is_dm)
238
239 ###############################################################################
240
241 def check_status(files):
242     new = byhand = 0
243     for f in files.keys():
244         if files[f]["type"] == "byhand":
245             byhand = 1
246         elif files[f].has_key("new"):
247             new = 1
248     return (new, byhand)
249
250 ###############################################################################
251
252 # Used by Upload.check_timestamps
253 class TarTime(object):
254     def __init__(self, future_cutoff, past_cutoff):
255         self.reset()
256         self.future_cutoff = future_cutoff
257         self.past_cutoff = past_cutoff
258
259     def reset(self):
260         self.future_files = {}
261         self.ancient_files = {}
262
263     def callback(self, Kind, Name, Link, Mode, UID, GID, Size, MTime, Major, Minor):
264         if MTime > self.future_cutoff:
265             self.future_files[Name] = MTime
266         if MTime < self.past_cutoff:
267             self.ancient_files[Name] = MTime
268
269 ###############################################################################
270
271 class Upload(object):
272     """
273     Everything that has to do with an upload processed.
274
275     """
276     def __init__(self):
277         self.logger = None
278         self.pkg = Changes()
279         self.reset()
280
281     ###########################################################################
282
283     def reset (self):
284         """ Reset a number of internal variables."""
285
286         # Initialize the substitution template map
287         cnf = Config()
288         self.Subst = {}
289         self.Subst["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"]
290         self.Subst["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
291         self.Subst["__DISTRO__"] = cnf["Dinstall::MyDistribution"]
292         self.Subst["__DAK_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
293
294         self.rejects = []
295         self.warnings = []
296         self.notes = []
297
298         self.pkg.reset()
299
300     def package_info(self):
301         msg = ''
302
303         if len(self.rejects) > 0:
304             msg += "Reject Reasons:\n"
305             msg += "\n".join(self.rejects)
306
307         if len(self.warnings) > 0:
308             msg += "Warnings:\n"
309             msg += "\n".join(self.warnings)
310
311         if len(self.notes) > 0:
312             msg += "Notes:\n"
313             msg += "\n".join(self.notes)
314
315         return msg
316
317     ###########################################################################
318     def update_subst(self):
319         """ Set up the per-package template substitution mappings """
320
321         cnf = Config()
322
323         # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
324         if not self.pkg.changes.has_key("architecture") or not \
325            isinstance(self.pkg.changes["architecture"], DictType):
326             self.pkg.changes["architecture"] = { "Unknown" : "" }
327
328         # and maintainer2047 may not exist.
329         if not self.pkg.changes.has_key("maintainer2047"):
330             self.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
331
332         self.Subst["__ARCHITECTURE__"] = " ".join(self.pkg.changes["architecture"].keys())
333         self.Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
334         self.Subst["__FILE_CONTENTS__"] = self.pkg.changes.get("filecontents", "")
335
336         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
337         if self.pkg.changes["architecture"].has_key("source") and \
338            self.pkg.changes["changedby822"] != "" and \
339            (self.pkg.changes["changedby822"] != self.pkg.changes["maintainer822"]):
340
341             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["changedby2047"]
342             self.Subst["__MAINTAINER_TO__"] = "%s, %s" % (self.pkg.changes["changedby2047"], self.pkg.changes["maintainer2047"])
343             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("changed-by", "Unknown")
344         else:
345             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["maintainer2047"]
346             self.Subst["__MAINTAINER_TO__"] = self.pkg.changes["maintainer2047"]
347             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("maintainer", "Unknown")
348
349         if "sponsoremail" in self.pkg.changes:
350             self.Subst["__MAINTAINER_TO__"] += ", %s" % self.pkg.changes["sponsoremail"]
351
352         if cnf.has_key("Dinstall::TrackingServer") and self.pkg.changes.has_key("source"):
353             self.Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
354
355         # Apply any global override of the Maintainer field
356         if cnf.get("Dinstall::OverrideMaintainer"):
357             self.Subst["__MAINTAINER_TO__"] = cnf["Dinstall::OverrideMaintainer"]
358             self.Subst["__MAINTAINER_FROM__"] = cnf["Dinstall::OverrideMaintainer"]
359
360         self.Subst["__REJECT_MESSAGE__"] = self.package_info()
361         self.Subst["__SOURCE__"] = self.pkg.changes.get("source", "Unknown")
362         self.Subst["__VERSION__"] = self.pkg.changes.get("version", "Unknown")
363
364     ###########################################################################
365     def load_changes(self, filename):
366         """
367         @rtype: boolean
368         @rvalue: whether the changes file was valid or not.  We may want to
369                  reject even if this is True (see what gets put in self.rejects).
370                  This is simply to prevent us even trying things later which will
371                  fail because we couldn't properly parse the file.
372         """
373         Cnf = Config()
374         self.pkg.changes_file = filename
375
376         # Parse the .changes field into a dictionary
377         try:
378             self.pkg.changes.update(parse_changes(filename))
379         except CantOpenError:
380             self.rejects.append("%s: can't read file." % (filename))
381             return False
382         except ParseChangesError, line:
383             self.rejects.append("%s: parse error, can't grok: %s." % (filename, line))
384             return False
385         except ChangesUnicodeError:
386             self.rejects.append("%s: changes file not proper utf-8" % (filename))
387             return False
388
389         # Parse the Files field from the .changes into another dictionary
390         try:
391             self.pkg.files.update(utils.build_file_list(self.pkg.changes))
392         except ParseChangesError, line:
393             self.rejects.append("%s: parse error, can't grok: %s." % (filename, line))
394             return False
395         except UnknownFormatError, format:
396             self.rejects.append("%s: unknown format '%s'." % (filename, format))
397             return False
398
399         # Check for mandatory fields
400         for i in ("distribution", "source", "binary", "architecture",
401                   "version", "maintainer", "files", "changes", "description"):
402             if not self.pkg.changes.has_key(i):
403                 # Avoid undefined errors later
404                 self.rejects.append("%s: Missing mandatory field `%s'." % (filename, i))
405                 return False
406
407         # Strip a source version in brackets from the source field
408         if re_strip_srcver.search(self.pkg.changes["source"]):
409             self.pkg.changes["source"] = re_strip_srcver.sub('', self.pkg.changes["source"])
410
411         # Ensure the source field is a valid package name.
412         if not re_valid_pkg_name.match(self.pkg.changes["source"]):
413             self.rejects.append("%s: invalid source name '%s'." % (filename, self.pkg.changes["source"]))
414
415         # Split multi-value fields into a lower-level dictionary
416         for i in ("architecture", "distribution", "binary", "closes"):
417             o = self.pkg.changes.get(i, "")
418             if o != "":
419                 del self.pkg.changes[i]
420
421             self.pkg.changes[i] = {}
422
423             for j in o.split():
424                 self.pkg.changes[i][j] = 1
425
426         # Fix the Maintainer: field to be RFC822/2047 compatible
427         try:
428             (self.pkg.changes["maintainer822"],
429              self.pkg.changes["maintainer2047"],
430              self.pkg.changes["maintainername"],
431              self.pkg.changes["maintaineremail"]) = \
432                    fix_maintainer (self.pkg.changes["maintainer"])
433         except ParseMaintError, msg:
434             self.rejects.append("%s: Maintainer field ('%s') failed to parse: %s" \
435                    % (filename, changes["maintainer"], msg))
436
437         # ...likewise for the Changed-By: field if it exists.
438         try:
439             (self.pkg.changes["changedby822"],
440              self.pkg.changes["changedby2047"],
441              self.pkg.changes["changedbyname"],
442              self.pkg.changes["changedbyemail"]) = \
443                    fix_maintainer (self.pkg.changes.get("changed-by", ""))
444         except ParseMaintError, msg:
445             self.pkg.changes["changedby822"] = ""
446             self.pkg.changes["changedby2047"] = ""
447             self.pkg.changes["changedbyname"] = ""
448             self.pkg.changes["changedbyemail"] = ""
449
450             self.rejects.append("%s: Changed-By field ('%s') failed to parse: %s" \
451                    % (filename, changes["changed-by"], msg))
452
453         # Ensure all the values in Closes: are numbers
454         if self.pkg.changes.has_key("closes"):
455             for i in self.pkg.changes["closes"].keys():
456                 if re_isanum.match (i) == None:
457                     self.rejects.append(("%s: `%s' from Closes field isn't a number." % (filename, i)))
458
459         # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
460         self.pkg.changes["chopversion"] = re_no_epoch.sub('', self.pkg.changes["version"])
461         self.pkg.changes["chopversion2"] = re_no_revision.sub('', self.pkg.changes["chopversion"])
462
463         # Check there isn't already a changes file of the same name in one
464         # of the queue directories.
465         base_filename = os.path.basename(filename)
466         for d in [ "Accepted", "Byhand", "Done", "New", "ProposedUpdates", "OldProposedUpdates" ]:
467             if os.path.exists(os.path.join(Cnf["Dir::Queue::%s" % (d) ], base_filename)):
468                 self.rejects.append("%s: a file with this name already exists in the %s directory." % (base_filename, d))
469
470         # Check the .changes is non-empty
471         if not self.pkg.files:
472             self.rejects.append("%s: nothing to do (Files field is empty)." % (base_filename))
473             return False
474
475         # Changes was syntactically valid even if we'll reject
476         return True
477
478     ###########################################################################
479
480     def check_distributions(self):
481         "Check and map the Distribution field"
482
483         Cnf = Config()
484
485         # Handle suite mappings
486         for m in Cnf.ValueList("SuiteMappings"):
487             args = m.split()
488             mtype = args[0]
489             if mtype == "map" or mtype == "silent-map":
490                 (source, dest) = args[1:3]
491                 if self.pkg.changes["distribution"].has_key(source):
492                     del self.pkg.changes["distribution"][source]
493                     self.pkg.changes["distribution"][dest] = 1
494                     if mtype != "silent-map":
495                         self.notes.append("Mapping %s to %s." % (source, dest))
496                 if self.pkg.changes.has_key("distribution-version"):
497                     if self.pkg.changes["distribution-version"].has_key(source):
498                         self.pkg.changes["distribution-version"][source]=dest
499             elif mtype == "map-unreleased":
500                 (source, dest) = args[1:3]
501                 if self.pkg.changes["distribution"].has_key(source):
502                     for arch in self.pkg.changes["architecture"].keys():
503                         if arch not in [ a.arch_string for a in get_suite_architectures(source) ]:
504                             self.notes.append("Mapping %s to %s for unreleased architecture %s." % (source, dest, arch))
505                             del self.pkg.changes["distribution"][source]
506                             self.pkg.changes["distribution"][dest] = 1
507                             break
508             elif mtype == "ignore":
509                 suite = args[1]
510                 if self.pkg.changes["distribution"].has_key(suite):
511                     del self.pkg.changes["distribution"][suite]
512                     self.warnings.append("Ignoring %s as a target suite." % (suite))
513             elif mtype == "reject":
514                 suite = args[1]
515                 if self.pkg.changes["distribution"].has_key(suite):
516                     self.rejects.append("Uploads to %s are not accepted." % (suite))
517             elif mtype == "propup-version":
518                 # give these as "uploaded-to(non-mapped) suites-to-add-when-upload-obsoletes"
519                 #
520                 # changes["distribution-version"] looks like: {'testing': 'testing-proposed-updates'}
521                 if self.pkg.changes["distribution"].has_key(args[1]):
522                     self.pkg.changes.setdefault("distribution-version", {})
523                     for suite in args[2:]:
524                         self.pkg.changes["distribution-version"][suite] = suite
525
526         # Ensure there is (still) a target distribution
527         if len(self.pkg.changes["distribution"].keys()) < 1:
528             self.rejects.append("No valid distribution remaining.")
529
530         # Ensure target distributions exist
531         for suite in self.pkg.changes["distribution"].keys():
532             if not Cnf.has_key("Suite::%s" % (suite)):
533                 self.rejects.append("Unknown distribution `%s'." % (suite))
534
535     ###########################################################################
536
537     def binary_file_checks(self, f, session):
538         cnf = Config()
539         entry = self.pkg.files[f]
540
541         # Extract package control information
542         deb_file = utils.open_file(f)
543         try:
544             control = apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))
545         except:
546             self.rejects.append("%s: debExtractControl() raised %s." % (f, sys.exc_type))
547             deb_file.close()
548             # Can't continue, none of the checks on control would work.
549             return
550
551         # Check for mandantory "Description:"
552         deb_file.seek(0)
553         try:
554             apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))["Description"] + '\n'
555         except:
556             self.rejects.append("%s: Missing Description in binary package" % (f))
557             return
558
559         deb_file.close()
560
561         # Check for mandatory fields
562         for field in [ "Package", "Architecture", "Version" ]:
563             if control.Find(field) == None:
564                 # Can't continue
565                 self.rejects.append("%s: No %s field in control." % (f, field))
566                 return
567
568         # Ensure the package name matches the one give in the .changes
569         if not self.pkg.changes["binary"].has_key(control.Find("Package", "")):
570             self.rejects.append("%s: control file lists name as `%s', which isn't in changes file." % (f, control.Find("Package", "")))
571
572         # Validate the package field
573         package = control.Find("Package")
574         if not re_valid_pkg_name.match(package):
575             self.rejects.append("%s: invalid package name '%s'." % (f, package))
576
577         # Validate the version field
578         version = control.Find("Version")
579         if not re_valid_version.match(version):
580             self.rejects.append("%s: invalid version number '%s'." % (f, version))
581
582         # Ensure the architecture of the .deb is one we know about.
583         default_suite = cnf.get("Dinstall::DefaultSuite", "Unstable")
584         architecture = control.Find("Architecture")
585         upload_suite = self.pkg.changes["distribution"].keys()[0]
586
587         if      architecture not in [a.arch_string for a in get_suite_architectures(default_suite, session)] \
588             and architecture not in [a.arch_string for a in get_suite_architectures(upload_suite, session)]:
589             self.rejects.append("Unknown architecture '%s'." % (architecture))
590
591         # Ensure the architecture of the .deb is one of the ones
592         # listed in the .changes.
593         if not self.pkg.changes["architecture"].has_key(architecture):
594             self.rejects.append("%s: control file lists arch as `%s', which isn't in changes file." % (f, architecture))
595
596         # Sanity-check the Depends field
597         depends = control.Find("Depends")
598         if depends == '':
599             self.rejects.append("%s: Depends field is empty." % (f))
600
601         # Sanity-check the Provides field
602         provides = control.Find("Provides")
603         if provides:
604             provide = re_spacestrip.sub('', provides)
605             if provide == '':
606                 self.rejects.append("%s: Provides field is empty." % (f))
607             prov_list = provide.split(",")
608             for prov in prov_list:
609                 if not re_valid_pkg_name.match(prov):
610                     self.rejects.append("%s: Invalid Provides field content %s." % (f, prov))
611
612         # Check the section & priority match those given in the .changes (non-fatal)
613         if     control.Find("Section") and entry["section"] != "" \
614            and entry["section"] != control.Find("Section"):
615             self.warnings.append("%s control file lists section as `%s', but changes file has `%s'." % \
616                                 (f, control.Find("Section", ""), entry["section"]))
617         if control.Find("Priority") and entry["priority"] != "" \
618            and entry["priority"] != control.Find("Priority"):
619             self.warnings.append("%s control file lists priority as `%s', but changes file has `%s'." % \
620                                 (f, control.Find("Priority", ""), entry["priority"]))
621
622         entry["package"] = package
623         entry["architecture"] = architecture
624         entry["version"] = version
625         entry["maintainer"] = control.Find("Maintainer", "")
626
627         if f.endswith(".udeb"):
628             self.pkg.files[f]["dbtype"] = "udeb"
629         elif f.endswith(".deb"):
630             self.pkg.files[f]["dbtype"] = "deb"
631         else:
632             self.rejects.append("%s is neither a .deb or a .udeb." % (f))
633
634         entry["source"] = control.Find("Source", entry["package"])
635
636         # Get the source version
637         source = entry["source"]
638         source_version = ""
639
640         if source.find("(") != -1:
641             m = re_extract_src_version.match(source)
642             source = m.group(1)
643             source_version = m.group(2)
644
645         if not source_version:
646             source_version = self.pkg.files[f]["version"]
647
648         entry["source package"] = source
649         entry["source version"] = source_version
650
651         # Ensure the filename matches the contents of the .deb
652         m = re_isadeb.match(f)
653
654         #  package name
655         file_package = m.group(1)
656         if entry["package"] != file_package:
657             self.rejects.append("%s: package part of filename (%s) does not match package name in the %s (%s)." % \
658                                 (f, file_package, entry["dbtype"], entry["package"]))
659         epochless_version = re_no_epoch.sub('', control.Find("Version"))
660
661         #  version
662         file_version = m.group(2)
663         if epochless_version != file_version:
664             self.rejects.append("%s: version part of filename (%s) does not match package version in the %s (%s)." % \
665                                 (f, file_version, entry["dbtype"], epochless_version))
666
667         #  architecture
668         file_architecture = m.group(3)
669         if entry["architecture"] != file_architecture:
670             self.rejects.append("%s: architecture part of filename (%s) does not match package architecture in the %s (%s)." % \
671                                 (f, file_architecture, entry["dbtype"], entry["architecture"]))
672
673         # Check for existent source
674         source_version = entry["source version"]
675         source_package = entry["source package"]
676         if self.pkg.changes["architecture"].has_key("source"):
677             if source_version != self.pkg.changes["version"]:
678                 self.rejects.append("source version (%s) for %s doesn't match changes version %s." % \
679                                     (source_version, f, self.pkg.changes["version"]))
680         else:
681             # Check in the SQL database
682             if not source_exists(source_package, source_version, self.pkg.changes["distribution"].keys(), session):
683                 # Check in one of the other directories
684                 source_epochless_version = re_no_epoch.sub('', source_version)
685                 dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
686                 if os.path.exists(os.path.join(cnf["Dir::Queue::Byhand"], dsc_filename)):
687                     entry["byhand"] = 1
688                 elif os.path.exists(os.path.join(cnf["Dir::Queue::New"], dsc_filename)):
689                     entry["new"] = 1
690                 else:
691                     dsc_file_exists = False
692                     for myq in ["Accepted", "Embargoed", "Unembargoed", "ProposedUpdates", "OldProposedUpdates"]:
693                         if cnf.has_key("Dir::Queue::%s" % (myq)):
694                             if os.path.exists(os.path.join(cnf["Dir::Queue::" + myq], dsc_filename)):
695                                 dsc_file_exists = True
696                                 break
697
698                     if not dsc_file_exists:
699                         self.rejects.append("no source found for %s %s (%s)." % (source_package, source_version, f))
700
701         # Check the version and for file overwrites
702         self.check_binary_against_db(f, session)
703
704         # Temporarily disable contents generation until we change the table storage layout
705         #b = Binary(f)
706         #b.scan_package()
707         #if len(b.rejects) > 0:
708         #    for j in b.rejects:
709         #        self.rejects.append(j)
710
711     def source_file_checks(self, f, session):
712         entry = self.pkg.files[f]
713
714         m = re_issource.match(f)
715         if not m:
716             return
717
718         entry["package"] = m.group(1)
719         entry["version"] = m.group(2)
720         entry["type"] = m.group(3)
721
722         # Ensure the source package name matches the Source filed in the .changes
723         if self.pkg.changes["source"] != entry["package"]:
724             self.rejects.append("%s: changes file doesn't say %s for Source" % (f, entry["package"]))
725
726         # Ensure the source version matches the version in the .changes file
727         if re_is_orig_source.match(f):
728             changes_version = self.pkg.changes["chopversion2"]
729         else:
730             changes_version = self.pkg.changes["chopversion"]
731
732         if changes_version != entry["version"]:
733             self.rejects.append("%s: should be %s according to changes file." % (f, changes_version))
734
735         # Ensure the .changes lists source in the Architecture field
736         if not self.pkg.changes["architecture"].has_key("source"):
737             self.rejects.append("%s: changes file doesn't list `source' in Architecture field." % (f))
738
739         # Check the signature of a .dsc file
740         if entry["type"] == "dsc":
741             # check_signature returns either:
742             #  (None, [list, of, rejects]) or (signature, [])
743             (self.pkg.dsc["fingerprint"], rejects) = utils.check_signature(f)
744             for j in rejects:
745                 self.rejects.append(j)
746
747         entry["architecture"] = "source"
748
749     def per_suite_file_checks(self, f, suite, session):
750         cnf = Config()
751         entry = self.pkg.files[f]
752         archive = utils.where_am_i()
753
754         # Skip byhand
755         if entry.has_key("byhand"):
756             return
757
758         # Check we have fields we need to do these checks
759         oktogo = True
760         for m in ['component', 'package', 'priority', 'size', 'md5sum']:
761             if not entry.has_key(m):
762                 self.rejects.append("file '%s' does not have field %s set" % (f, m))
763                 oktogo = False
764
765         if not oktogo:
766             return
767
768         # Handle component mappings
769         for m in cnf.ValueList("ComponentMappings"):
770             (source, dest) = m.split()
771             if entry["component"] == source:
772                 entry["original component"] = source
773                 entry["component"] = dest
774
775         # Ensure the component is valid for the target suite
776         if cnf.has_key("Suite:%s::Components" % (suite)) and \
777            entry["component"] not in cnf.ValueList("Suite::%s::Components" % (suite)):
778             self.rejects.append("unknown component `%s' for suite `%s'." % (entry["component"], suite))
779             return
780
781         # Validate the component
782         if not get_component(entry["component"], session):
783             self.rejects.append("file '%s' has unknown component '%s'." % (f, component))
784             return
785
786         # See if the package is NEW
787         if not self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), f, session):
788             entry["new"] = 1
789
790         # Validate the priority
791         if entry["priority"].find('/') != -1:
792             self.rejects.append("file '%s' has invalid priority '%s' [contains '/']." % (f, entry["priority"]))
793
794         # Determine the location
795         location = cnf["Dir::Pool"]
796         l = get_location(location, entry["component"], archive, session)
797         if l is None:
798             self.rejects.append("[INTERNAL ERROR] couldn't determine location (Component: %s, Archive: %s)" % (component, archive))
799             entry["location id"] = -1
800         else:
801             entry["location id"] = l.location_id
802
803         # Check the md5sum & size against existing files (if any)
804         entry["pool name"] = utils.poolify(self.pkg.changes["source"], entry["component"])
805
806         found, poolfile = check_poolfile(os.path.join(entry["pool name"], f),
807                                          entry["size"], entry["md5sum"], entry["location id"])
808
809         if found is None:
810             self.rejects.append("INTERNAL ERROR, get_files_id() returned multiple matches for %s." % (f))
811         elif found is False and poolfile is not None:
812             self.rejects.append("md5sum and/or size mismatch on existing copy of %s." % (f))
813         else:
814             if poolfile is None:
815                 entry["files id"] = None
816             else:
817                 entry["files id"] = poolfile.file_id
818
819         # Check for packages that have moved from one component to another
820         entry['suite'] = suite
821         res = get_binary_components(self.pkg.files[f]['package'], suite, entry["architecture"], session)
822         if res.rowcount > 0:
823             entry["othercomponents"] = res.fetchone()[0]
824
825     def check_files(self, action=True):
826         archive = utils.where_am_i()
827         file_keys = self.pkg.files.keys()
828         holding = Holding()
829         cnf = Config()
830
831         # XXX: As far as I can tell, this can no longer happen - see
832         #      comments by AJ in old revisions - mhy
833         # if reprocess is 2 we've already done this and we're checking
834         # things again for the new .orig.tar.gz.
835         # [Yes, I'm fully aware of how disgusting this is]
836         if action and self.reprocess < 2:
837             cwd = os.getcwd()
838             os.chdir(self.pkg.directory)
839             for f in file_keys:
840                 ret = holding.copy_to_holding(f)
841                 if ret is not None:
842                     # XXX: Should we bail out here or try and continue?
843                     self.rejects.append(ret)
844
845             os.chdir(cwd)
846
847         # Check there isn't already a .changes or .dak file of the same name in
848         # the proposed-updates "CopyChanges" or "CopyDotDak" storage directories.
849         # [NB: this check must be done post-suite mapping]
850         base_filename = os.path.basename(self.pkg.changes_file)
851         dot_dak_filename = base_filename[:-8] + ".dak"
852
853         for suite in self.pkg.changes["distribution"].keys():
854             copychanges = "Suite::%s::CopyChanges" % (suite)
855             if cnf.has_key(copychanges) and \
856                    os.path.exists(os.path.join(cnf[copychanges], base_filename)):
857                 self.rejects.append("%s: a file with this name already exists in %s" \
858                            % (base_filename, cnf[copychanges]))
859
860             copy_dot_dak = "Suite::%s::CopyDotDak" % (suite)
861             if cnf.has_key(copy_dot_dak) and \
862                    os.path.exists(os.path.join(cnf[copy_dot_dak], dot_dak_filename)):
863                 self.rejects.append("%s: a file with this name already exists in %s" \
864                            % (dot_dak_filename, Cnf[copy_dot_dak]))
865
866         self.reprocess = 0
867         has_binaries = False
868         has_source = False
869
870         session = DBConn().session()
871
872         for f, entry in self.pkg.files.items():
873             # Ensure the file does not already exist in one of the accepted directories
874             for d in [ "Accepted", "Byhand", "New", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
875                 if not cnf.has_key("Dir::Queue::%s" % (d)): continue
876                 if os.path.exists(cnf["Dir::Queue::%s" % (d) ] + '/' + f):
877                     self.rejects.append("%s file already exists in the %s directory." % (f, d))
878
879             if not re_taint_free.match(f):
880                 self.rejects.append("!!WARNING!! tainted filename: '%s'." % (f))
881
882             # Check the file is readable
883             if os.access(f, os.R_OK) == 0:
884                 # When running in -n, copy_to_holding() won't have
885                 # generated the reject_message, so we need to.
886                 if action:
887                     if os.path.exists(f):
888                         self.rejects.append("Can't read `%s'. [permission denied]" % (f))
889                     else:
890                         self.rejects.append("Can't read `%s'. [file not found]" % (f))
891                 entry["type"] = "unreadable"
892                 continue
893
894             # If it's byhand skip remaining checks
895             if entry["section"] == "byhand" or entry["section"][:4] == "raw-":
896                 entry["byhand"] = 1
897                 entry["type"] = "byhand"
898
899             # Checks for a binary package...
900             elif re_isadeb.match(f):
901                 has_binaries = True
902                 entry["type"] = "deb"
903
904                 # This routine appends to self.rejects/warnings as appropriate
905                 self.binary_file_checks(f, session)
906
907             # Checks for a source package...
908             elif re_issource.match(f):
909                 has_source = True
910
911                 # This routine appends to self.rejects/warnings as appropriate
912                 self.source_file_checks(f, session)
913
914             # Not a binary or source package?  Assume byhand...
915             else:
916                 entry["byhand"] = 1
917                 entry["type"] = "byhand"
918
919             # Per-suite file checks
920             entry["oldfiles"] = {}
921             for suite in self.pkg.changes["distribution"].keys():
922                 self.per_suite_file_checks(f, suite, session)
923
924         session.close()
925
926         # If the .changes file says it has source, it must have source.
927         if self.pkg.changes["architecture"].has_key("source"):
928             if not has_source:
929                 self.rejects.append("no source found and Architecture line in changes mention source.")
930
931             if not has_binaries and cnf.FindB("Dinstall::Reject::NoSourceOnly"):
932                 self.rejects.append("source only uploads are not supported.")
933
934     ###########################################################################
935     def check_dsc(self, action=True, session=None):
936         """Returns bool indicating whether or not the source changes are valid"""
937         # Ensure there is source to check
938         if not self.pkg.changes["architecture"].has_key("source"):
939             return True
940
941         # Find the .dsc
942         dsc_filename = None
943         for f, entry in self.pkg.files.items():
944             if entry["type"] == "dsc":
945                 if dsc_filename:
946                     self.rejects.append("can not process a .changes file with multiple .dsc's.")
947                     return False
948                 else:
949                     dsc_filename = f
950
951         # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
952         if not dsc_filename:
953             self.rejects.append("source uploads must contain a dsc file")
954             return False
955
956         # Parse the .dsc file
957         try:
958             self.pkg.dsc.update(utils.parse_changes(dsc_filename, signing_rules=1))
959         except CantOpenError:
960             # if not -n copy_to_holding() will have done this for us...
961             if not action:
962                 self.rejects.append("%s: can't read file." % (dsc_filename))
963         except ParseChangesError, line:
964             self.rejects.append("%s: parse error, can't grok: %s." % (dsc_filename, line))
965         except InvalidDscError, line:
966             self.rejects.append("%s: syntax error on line %s." % (dsc_filename, line))
967         except ChangesUnicodeError:
968             self.rejects.append("%s: dsc file not proper utf-8." % (dsc_filename))
969
970         # Build up the file list of files mentioned by the .dsc
971         try:
972             self.pkg.dsc_files.update(utils.build_file_list(self.pkg.dsc, is_a_dsc=1))
973         except NoFilesFieldError:
974             self.rejects.append("%s: no Files: field." % (dsc_filename))
975             return False
976         except UnknownFormatError, format:
977             self.rejects.append("%s: unknown format '%s'." % (dsc_filename, format))
978             return False
979         except ParseChangesError, line:
980             self.rejects.append("%s: parse error, can't grok: %s." % (dsc_filename, line))
981             return False
982
983         # Enforce mandatory fields
984         for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
985             if not self.pkg.dsc.has_key(i):
986                 self.rejects.append("%s: missing mandatory field `%s'." % (dsc_filename, i))
987                 return False
988
989         # Validate the source and version fields
990         if not re_valid_pkg_name.match(self.pkg.dsc["source"]):
991             self.rejects.append("%s: invalid source name '%s'." % (dsc_filename, self.pkg.dsc["source"]))
992         if not re_valid_version.match(self.pkg.dsc["version"]):
993             self.rejects.append("%s: invalid version number '%s'." % (dsc_filename, self.pkg.dsc["version"]))
994
995         # Only a limited list of source formats are allowed in each suite
996         for dist in self.pkg.changes["distribution"].keys():
997             allowed = [ x.format_name for x in get_suite_src_formats(dist, session) ]
998             if self.pkg.dsc["format"] not in allowed:
999                 self.rejects.append("%s: source format '%s' not allowed in %s (accepted: %s) " % (dsc_filename, self.pkg.dsc["format"], dist, ", ".join(allowed)))
1000
1001         # Validate the Maintainer field
1002         try:
1003             # We ignore the return value
1004             fix_maintainer(self.pkg.dsc["maintainer"])
1005         except ParseMaintError, msg:
1006             self.rejects.append("%s: Maintainer field ('%s') failed to parse: %s" \
1007                                  % (dsc_filename, self.pkg.dsc["maintainer"], msg))
1008
1009         # Validate the build-depends field(s)
1010         for field_name in [ "build-depends", "build-depends-indep" ]:
1011             field = self.pkg.dsc.get(field_name)
1012             if field:
1013                 # Have apt try to parse them...
1014                 try:
1015                     apt_pkg.ParseSrcDepends(field)
1016                 except:
1017                     self.rejects.append("%s: invalid %s field (can not be parsed by apt)." % (dsc_filename, field_name.title()))
1018
1019         # Ensure the version number in the .dsc matches the version number in the .changes
1020         epochless_dsc_version = re_no_epoch.sub('', self.pkg.dsc["version"])
1021         changes_version = self.pkg.files[dsc_filename]["version"]
1022
1023         if epochless_dsc_version != self.pkg.files[dsc_filename]["version"]:
1024             self.rejects.append("version ('%s') in .dsc does not match version ('%s') in .changes." % (epochless_dsc_version, changes_version))
1025
1026         # Ensure the Files field contain only what's expected
1027         self.rejects.extend(check_dsc_files(dsc_filename, self.pkg.dsc, self.pkg.dsc_files))
1028
1029         # Ensure source is newer than existing source in target suites
1030         session = DBConn().session()
1031         self.check_source_against_db(dsc_filename, session)
1032         self.check_dsc_against_db(dsc_filename, session)
1033         session.close()
1034
1035         return True
1036
1037     ###########################################################################
1038
1039     def get_changelog_versions(self, source_dir):
1040         """Extracts a the source package and (optionally) grabs the
1041         version history out of debian/changelog for the BTS."""
1042
1043         cnf = Config()
1044
1045         # Find the .dsc (again)
1046         dsc_filename = None
1047         for f in self.pkg.files.keys():
1048             if self.pkg.files[f]["type"] == "dsc":
1049                 dsc_filename = f
1050
1051         # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
1052         if not dsc_filename:
1053             return
1054
1055         # Create a symlink mirror of the source files in our temporary directory
1056         for f in self.pkg.files.keys():
1057             m = re_issource.match(f)
1058             if m:
1059                 src = os.path.join(source_dir, f)
1060                 # If a file is missing for whatever reason, give up.
1061                 if not os.path.exists(src):
1062                     return
1063                 ftype = m.group(3)
1064                 if re_is_orig_source.match(f) and pkg.orig_files.has_key(f) and \
1065                    pkg.orig_files[f].has_key("path"):
1066                     continue
1067                 dest = os.path.join(os.getcwd(), f)
1068                 os.symlink(src, dest)
1069
1070         # If the orig files are not a part of the upload, create symlinks to the
1071         # existing copies.
1072         for orig_file in self.pkg.orig_files.keys():
1073             if not self.pkg.orig_files[orig_file].has_key("path"):
1074                 continue
1075             dest = os.path.join(os.getcwd(), os.path.basename(orig_file))
1076             os.symlink(self.pkg.orig_files[orig_file]["path"], dest)
1077
1078         # Extract the source
1079         cmd = "dpkg-source -sn -x %s" % (dsc_filename)
1080         (result, output) = commands.getstatusoutput(cmd)
1081         if (result != 0):
1082             self.rejects.append("'dpkg-source -x' failed for %s [return code: %s]." % (dsc_filename, result))
1083             self.rejects.append(utils.prefix_multi_line_string(output, " [dpkg-source output:] "))
1084             return
1085
1086         if not cnf.Find("Dir::Queue::BTSVersionTrack"):
1087             return
1088
1089         # Get the upstream version
1090         upstr_version = re_no_epoch.sub('', self.pkg.dsc["version"])
1091         if re_strip_revision.search(upstr_version):
1092             upstr_version = re_strip_revision.sub('', upstr_version)
1093
1094         # Ensure the changelog file exists
1095         changelog_filename = "%s-%s/debian/changelog" % (self.pkg.dsc["source"], upstr_version)
1096         if not os.path.exists(changelog_filename):
1097             self.rejects.append("%s: debian/changelog not found in extracted source." % (dsc_filename))
1098             return
1099
1100         # Parse the changelog
1101         self.pkg.dsc["bts changelog"] = ""
1102         changelog_file = utils.open_file(changelog_filename)
1103         for line in changelog_file.readlines():
1104             m = re_changelog_versions.match(line)
1105             if m:
1106                 self.pkg.dsc["bts changelog"] += line
1107         changelog_file.close()
1108
1109         # Check we found at least one revision in the changelog
1110         if not self.pkg.dsc["bts changelog"]:
1111             self.rejects.append("%s: changelog format not recognised (empty version tree)." % (dsc_filename))
1112
1113     def check_source(self):
1114         # XXX: I'm fairly sure reprocess == 2 can never happen
1115         #      AJT disabled the is_incoming check years ago - mhy
1116         #      We should probably scrap or rethink the whole reprocess thing
1117         # Bail out if:
1118         #    a) there's no source
1119         # or b) reprocess is 2 - we will do this check next time when orig
1120         #       tarball is in 'files'
1121         # or c) the orig files are MIA
1122         if not self.pkg.changes["architecture"].has_key("source") or self.reprocess == 2 \
1123            or len(self.pkg.orig_files) == 0:
1124             return
1125
1126         tmpdir = utils.temp_dirname()
1127
1128         # Move into the temporary directory
1129         cwd = os.getcwd()
1130         os.chdir(tmpdir)
1131
1132         # Get the changelog version history
1133         self.get_changelog_versions(cwd)
1134
1135         # Move back and cleanup the temporary tree
1136         os.chdir(cwd)
1137
1138         try:
1139             shutil.rmtree(tmpdir)
1140         except OSError, e:
1141             if e.errno != errno.EACCES:
1142                 print "foobar"
1143                 utils.fubar("%s: couldn't remove tmp dir for source tree." % (self.pkg.dsc["source"]))
1144
1145             self.rejects.append("%s: source tree could not be cleanly removed." % (self.pkg.dsc["source"]))
1146             # We probably have u-r or u-w directories so chmod everything
1147             # and try again.
1148             cmd = "chmod -R u+rwx %s" % (tmpdir)
1149             result = os.system(cmd)
1150             if result != 0:
1151                 utils.fubar("'%s' failed with result %s." % (cmd, result))
1152             shutil.rmtree(tmpdir)
1153         except Exception, e:
1154             print "foobar2 (%s)" % e
1155             utils.fubar("%s: couldn't remove tmp dir for source tree." % (self.pkg.dsc["source"]))
1156
1157     ###########################################################################
1158     def ensure_hashes(self):
1159         # Make sure we recognise the format of the Files: field in the .changes
1160         format = self.pkg.changes.get("format", "0.0").split(".", 1)
1161         if len(format) == 2:
1162             format = int(format[0]), int(format[1])
1163         else:
1164             format = int(float(format[0])), 0
1165
1166         # We need to deal with the original changes blob, as the fields we need
1167         # might not be in the changes dict serialised into the .dak anymore.
1168         orig_changes = utils.parse_deb822(self.pkg.changes['filecontents'])
1169
1170         # Copy the checksums over to the current changes dict.  This will keep
1171         # the existing modifications to it intact.
1172         for field in orig_changes:
1173             if field.startswith('checksums-'):
1174                 self.pkg.changes[field] = orig_changes[field]
1175
1176         # Check for unsupported hashes
1177         for j in utils.check_hash_fields(".changes", self.pkg.changes):
1178             self.rejects.append(j)
1179
1180         for j in utils.check_hash_fields(".dsc", self.pkg.dsc):
1181             self.rejects.append(j)
1182
1183         # We have to calculate the hash if we have an earlier changes version than
1184         # the hash appears in rather than require it exist in the changes file
1185         for hashname, hashfunc, version in utils.known_hashes:
1186             # TODO: Move _ensure_changes_hash into this class
1187             for j in utils._ensure_changes_hash(self.pkg.changes, format, version, self.pkg.files, hashname, hashfunc):
1188                 self.rejects.append(j)
1189             if "source" in self.pkg.changes["architecture"]:
1190                 # TODO: Move _ensure_dsc_hash into this class
1191                 for j in utils._ensure_dsc_hash(self.pkg.dsc, self.pkg.dsc_files, hashname, hashfunc):
1192                     self.rejects.append(j)
1193
1194     def check_hashes(self):
1195         for m in utils.check_hash(".changes", self.pkg.files, "md5", apt_pkg.md5sum):
1196             self.rejects.append(m)
1197
1198         for m in utils.check_size(".changes", self.pkg.files):
1199             self.rejects.append(m)
1200
1201         for m in utils.check_hash(".dsc", self.pkg.dsc_files, "md5", apt_pkg.md5sum):
1202             self.rejects.append(m)
1203
1204         for m in utils.check_size(".dsc", self.pkg.dsc_files):
1205             self.rejects.append(m)
1206
1207         self.ensure_hashes()
1208
1209     ###########################################################################
1210     def check_lintian(self):
1211         # Only check some distributions
1212         valid_dist = False
1213         for dist in ('unstable', 'experimental'):
1214             if dist in self.pkg.changes['distribution']:
1215                 valid_dist = True
1216                 break
1217
1218         if not valid_dist:
1219             return
1220
1221         cnf = Config()
1222         tagfile = cnf.get("Dinstall::LintianTags")
1223         if tagfile is None:
1224             # We don't have a tagfile, so just don't do anything.
1225             return
1226         # Parse the yaml file
1227         sourcefile = file(tagfile, 'r')
1228         sourcecontent = sourcefile.read()
1229         sourcefile.close()
1230         try:
1231             lintiantags = yaml.load(sourcecontent)['lintian']
1232         except yaml.YAMLError, msg:
1233             utils.fubar("Can not read the lintian tags file %s, YAML error: %s." % (tagfile, msg))
1234             return
1235
1236         # Now setup the input file for lintian. lintian wants "one tag per line" only,
1237         # so put it together like it. We put all types of tags in one file and then sort
1238         # through lintians output later to see if its a fatal tag we detected, or not.
1239         # So we only run lintian once on all tags, even if we might reject on some, but not
1240         # reject on others.
1241         # Additionally build up a set of tags
1242         tags = set()
1243         (fd, temp_filename) = utils.temp_filename()
1244         temptagfile = os.fdopen(fd, 'w')
1245         for tagtype in lintiantags:
1246             for tag in lintiantags[tagtype]:
1247                 temptagfile.write("%s\n" % tag)
1248                 tags.add(tag)
1249         temptagfile.close()
1250
1251         # So now we should look at running lintian at the .changes file, capturing output
1252         # to then parse it.
1253         command = "lintian --show-overrides --tags-from-file %s %s" % (temp_filename, self.pkg.changes_file)
1254         (result, output) = commands.getstatusoutput(command)
1255         # We are done with lintian, remove our tempfile
1256         os.unlink(temp_filename)
1257         if (result == 2):
1258             utils.warn("lintian failed for %s [return code: %s]." % (self.pkg.changes_file, result))
1259             utils.warn(utils.prefix_multi_line_string(output, " [possible output:] "))
1260
1261         if len(output) == 0:
1262             return
1263
1264         # We have output of lintian, this package isn't clean. Lets parse it and see if we
1265         # are having a victim for a reject.
1266         # W: tzdata: binary-without-manpage usr/sbin/tzconfig
1267         for line in output.split('\n'):
1268             m = re_parse_lintian.match(line)
1269             if m is None:
1270                 continue
1271
1272             etype = m.group(1)
1273             epackage = m.group(2)
1274             etag = m.group(3)
1275             etext = m.group(4)
1276
1277             # So lets check if we know the tag at all.
1278             if etag not in tags:
1279                 continue
1280
1281             if etype == 'O':
1282                 # We know it and it is overriden. Check that override is allowed.
1283                 if etag in lintiantags['warning']:
1284                     # The tag is overriden, and it is allowed to be overriden.
1285                     # Don't add a reject message.
1286                     pass
1287                 elif etag in lintiantags['error']:
1288                     # The tag is overriden - but is not allowed to be
1289                     self.rejects.append("%s: Overriden tag %s found, but this tag may not be overwritten." % (epackage, etag))
1290             else:
1291                 # Tag is known, it is not overriden, direct reject.
1292                 self.rejects.append("%s: Found lintian output: '%s %s', automatically rejected package." % (epackage, etag, etext))
1293                 # Now tell if they *might* override it.
1294                 if etag in lintiantags['warning']:
1295                     self.rejects.append("%s: If you have a good reason, you may override this lintian tag." % (epackage))
1296
1297     ###########################################################################
1298     def check_urgency(self):
1299         cnf = Config()
1300         if self.pkg.changes["architecture"].has_key("source"):
1301             if not self.pkg.changes.has_key("urgency"):
1302                 self.pkg.changes["urgency"] = cnf["Urgency::Default"]
1303             self.pkg.changes["urgency"] = self.pkg.changes["urgency"].lower()
1304             if self.pkg.changes["urgency"] not in cnf.ValueList("Urgency::Valid"):
1305                 self.warnings.append("%s is not a valid urgency; it will be treated as %s by testing." % \
1306                                      (self.pkg.changes["urgency"], cnf["Urgency::Default"]))
1307                 self.pkg.changes["urgency"] = cnf["Urgency::Default"]
1308
1309     ###########################################################################
1310
1311     # Sanity check the time stamps of files inside debs.
1312     # [Files in the near future cause ugly warnings and extreme time
1313     #  travel can cause errors on extraction]
1314
1315     def check_timestamps(self):
1316         Cnf = Config()
1317
1318         future_cutoff = time.time() + int(Cnf["Dinstall::FutureTimeTravelGrace"])
1319         past_cutoff = time.mktime(time.strptime(Cnf["Dinstall::PastCutoffYear"],"%Y"))
1320         tar = TarTime(future_cutoff, past_cutoff)
1321
1322         for filename, entry in self.pkg.files.items():
1323             if entry["type"] == "deb":
1324                 tar.reset()
1325                 try:
1326                     deb_file = utils.open_file(filename)
1327                     apt_inst.debExtract(deb_file, tar.callback, "control.tar.gz")
1328                     deb_file.seek(0)
1329                     try:
1330                         apt_inst.debExtract(deb_file, tar.callback, "data.tar.gz")
1331                     except SystemError, e:
1332                         # If we can't find a data.tar.gz, look for data.tar.bz2 instead.
1333                         if not re.search(r"Cannot f[ui]nd chunk data.tar.gz$", str(e)):
1334                             raise
1335                         deb_file.seek(0)
1336                         apt_inst.debExtract(deb_file,tar.callback,"data.tar.bz2")
1337
1338                     deb_file.close()
1339
1340                     future_files = tar.future_files.keys()
1341                     if future_files:
1342                         num_future_files = len(future_files)
1343                         future_file = future_files[0]
1344                         future_date = tar.future_files[future_file]
1345                         self.rejects.append("%s: has %s file(s) with a time stamp too far into the future (e.g. %s [%s])."
1346                                % (filename, num_future_files, future_file, time.ctime(future_date)))
1347
1348                     ancient_files = tar.ancient_files.keys()
1349                     if ancient_files:
1350                         num_ancient_files = len(ancient_files)
1351                         ancient_file = ancient_files[0]
1352                         ancient_date = tar.ancient_files[ancient_file]
1353                         self.rejects.append("%s: has %s file(s) with a time stamp too ancient (e.g. %s [%s])."
1354                                % (filename, num_ancient_files, ancient_file, time.ctime(ancient_date)))
1355                 except:
1356                     self.rejects.append("%s: deb contents timestamp check failed [%s: %s]" % (filename, sys.exc_type, sys.exc_value))
1357
1358     ###########################################################################
1359     def check_transition(self, session):
1360         cnf = Config()
1361
1362         sourcepkg = self.pkg.changes["source"]
1363
1364         # No sourceful upload -> no need to do anything else, direct return
1365         # We also work with unstable uploads, not experimental or those going to some
1366         # proposed-updates queue
1367         if "source" not in self.pkg.changes["architecture"] or \
1368            "unstable" not in self.pkg.changes["distribution"]:
1369             return
1370
1371         # Also only check if there is a file defined (and existant) with
1372         # checks.
1373         transpath = cnf.get("Dinstall::Reject::ReleaseTransitions", "")
1374         if transpath == "" or not os.path.exists(transpath):
1375             return
1376
1377         # Parse the yaml file
1378         sourcefile = file(transpath, 'r')
1379         sourcecontent = sourcefile.read()
1380         try:
1381             transitions = yaml.load(sourcecontent)
1382         except yaml.YAMLError, msg:
1383             # This shouldn't happen, there is a wrapper to edit the file which
1384             # checks it, but we prefer to be safe than ending up rejecting
1385             # everything.
1386             utils.warn("Not checking transitions, the transitions file is broken: %s." % (msg))
1387             return
1388
1389         # Now look through all defined transitions
1390         for trans in transitions:
1391             t = transitions[trans]
1392             source = t["source"]
1393             expected = t["new"]
1394
1395             # Will be None if nothing is in testing.
1396             current = get_source_in_suite(source, "testing", session)
1397             if current is not None:
1398                 compare = apt_pkg.VersionCompare(current.version, expected)
1399
1400             if current is None or compare < 0:
1401                 # This is still valid, the current version in testing is older than
1402                 # the new version we wait for, or there is none in testing yet
1403
1404                 # Check if the source we look at is affected by this.
1405                 if sourcepkg in t['packages']:
1406                     # The source is affected, lets reject it.
1407
1408                     rejectmsg = "%s: part of the %s transition.\n\n" % (
1409                         sourcepkg, trans)
1410
1411                     if current is not None:
1412                         currentlymsg = "at version %s" % (current.version)
1413                     else:
1414                         currentlymsg = "not present in testing"
1415
1416                     rejectmsg += "Transition description: %s\n\n" % (t["reason"])
1417
1418                     rejectmsg += "\n".join(textwrap.wrap("""Your package
1419 is part of a testing transition designed to get %s migrated (it is
1420 currently %s, we need version %s).  This transition is managed by the
1421 Release Team, and %s is the Release-Team member responsible for it.
1422 Please mail debian-release@lists.debian.org or contact %s directly if you
1423 need further assistance.  You might want to upload to experimental until this
1424 transition is done."""
1425                             % (source, currentlymsg, expected,t["rm"], t["rm"])))
1426
1427                     self.rejects.append(rejectmsg)
1428                     return
1429
1430     ###########################################################################
1431     def check_signed_by_key(self):
1432         """Ensure the .changes is signed by an authorized uploader."""
1433         session = DBConn().session()
1434
1435         self.check_transition(session)
1436
1437         (uid, uid_name, is_dm) = lookup_uid_from_fingerprint(self.pkg.changes["fingerprint"], session=session)
1438
1439         # match claimed name with actual name:
1440         if uid is None:
1441             # This is fundamentally broken but need us to refactor how we get
1442             # the UIDs/Fingerprints in order for us to fix it properly
1443             uid, uid_email = self.pkg.changes["fingerprint"], uid
1444             may_nmu, may_sponsor = 1, 1
1445             # XXX by default new dds don't have a fingerprint/uid in the db atm,
1446             #     and can't get one in there if we don't allow nmu/sponsorship
1447         elif is_dm is False:
1448             # If is_dm is False, we allow full upload rights
1449             uid_email = "%s@debian.org" % (uid)
1450             may_nmu, may_sponsor = 1, 1
1451         else:
1452             # Assume limited upload rights unless we've discovered otherwise
1453             uid_email = uid
1454             may_nmu, may_sponsor = 0, 0
1455
1456         if uid_email in [self.pkg.changes["maintaineremail"], self.pkg.changes["changedbyemail"]]:
1457             sponsored = 0
1458         elif uid_name in [self.pkg.changes["maintainername"], self.pkg.changes["changedbyname"]]:
1459             sponsored = 0
1460             if uid_name == "": sponsored = 1
1461         else:
1462             sponsored = 1
1463             if ("source" in self.pkg.changes["architecture"] and
1464                 uid_email and utils.is_email_alias(uid_email)):
1465                 sponsor_addresses = utils.gpg_get_key_addresses(self.pkg.changes["fingerprint"])
1466                 if (self.pkg.changes["maintaineremail"] not in sponsor_addresses and
1467                     self.pkg.changes["changedbyemail"] not in sponsor_addresses):
1468                     self.pkg.changes["sponsoremail"] = uid_email
1469
1470         if sponsored and not may_sponsor:
1471             self.rejects.append("%s is not authorised to sponsor uploads" % (uid))
1472
1473         if not sponsored and not may_nmu:
1474             should_reject = True
1475             highest_sid, highest_version = None, None
1476
1477             # XXX: This reimplements in SQLA what existed before but it's fundamentally fucked
1478             #      It ignores higher versions with the dm_upload_allowed flag set to false
1479             #      I'm keeping the existing behaviour for now until I've gone back and
1480             #      checked exactly what the GR says - mhy
1481             for si in get_sources_from_name(source=self.pkg.changes['source'], dm_upload_allowed=True, session=session):
1482                 if highest_version is None or apt_pkg.VersionCompare(si.version, highest_version) == 1:
1483                      highest_sid = si.source_id
1484                      highest_version = si.version
1485
1486             if highest_sid is None:
1487                 self.rejects.append("Source package %s does not have 'DM-Upload-Allowed: yes' in its most recent version" % self.pkg.changes["source"])
1488             else:
1489                 for sup in session.query(SrcUploader).join(DBSource).filter_by(source_id=highest_sid):
1490                     (rfc822, rfc2047, name, email) = sup.maintainer.get_split_maintainer()
1491                     if email == uid_email or name == uid_name:
1492                         should_reject = False
1493                         break
1494
1495             if should_reject is True:
1496                 self.rejects.append("%s is not in Maintainer or Uploaders of source package %s" % (uid, self.pkg.changes["source"]))
1497
1498             for b in self.pkg.changes["binary"].keys():
1499                 for suite in self.pkg.changes["distribution"].keys():
1500                     q = session.query(DBSource)
1501                     q = q.join(DBBinary).filter_by(package=b)
1502                     q = q.join(BinAssociation).join(Suite).filter_by(suite_name=suite)
1503
1504                     for s in q.all():
1505                         if s.source != self.pkg.changes["source"]:
1506                             self.rejects.append("%s may not hijack %s from source package %s in suite %s" % (uid, b, s, suite))
1507
1508             for f in self.pkg.files.keys():
1509                 if self.pkg.files[f].has_key("byhand"):
1510                     self.rejects.append("%s may not upload BYHAND file %s" % (uid, f))
1511                 if self.pkg.files[f].has_key("new"):
1512                     self.rejects.append("%s may not upload NEW file %s" % (uid, f))
1513
1514         session.close()
1515
1516     ###########################################################################
1517     def build_summaries(self):
1518         """ Build a summary of changes the upload introduces. """
1519
1520         (byhand, new, summary, override_summary) = self.pkg.file_summary()
1521
1522         short_summary = summary
1523
1524         # This is for direport's benefit...
1525         f = re_fdnic.sub("\n .\n", self.pkg.changes.get("changes", ""))
1526
1527         if byhand or new:
1528             summary += "Changes: " + f
1529
1530         summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"
1531
1532         summary += self.announce(short_summary, 0)
1533
1534         return (summary, short_summary)
1535
1536     ###########################################################################
1537
1538     def close_bugs(self, summary, action):
1539         """
1540         Send mail to close bugs as instructed by the closes field in the changes file.
1541         Also add a line to summary if any work was done.
1542
1543         @type summary: string
1544         @param summary: summary text, as given by L{build_summaries}
1545
1546         @type action: bool
1547         @param action: Set to false no real action will be done.
1548
1549         @rtype: string
1550         @return: summary. If action was taken, extended by the list of closed bugs.
1551
1552         """
1553
1554         template = os.path.join(Config()["Dir::Templates"], 'process-unchecked.bug-close')
1555
1556         bugs = self.pkg.changes["closes"].keys()
1557
1558         if not bugs:
1559             return summary
1560
1561         bugs.sort()
1562         summary += "Closing bugs: "
1563         for bug in bugs:
1564             summary += "%s " % (bug)
1565             if action:
1566                 self.update_subst()
1567                 self.Subst["__BUG_NUMBER__"] = bug
1568                 if self.pkg.changes["distribution"].has_key("stable"):
1569                     self.Subst["__STABLE_WARNING__"] = """
1570 Note that this package is not part of the released stable Debian
1571 distribution.  It may have dependencies on other unreleased software,
1572 or other instabilities.  Please take care if you wish to install it.
1573 The update will eventually make its way into the next released Debian
1574 distribution."""
1575                 else:
1576                     self.Subst["__STABLE_WARNING__"] = ""
1577                 mail_message = utils.TemplateSubst(self.Subst, template)
1578                 utils.send_mail(mail_message)
1579
1580                 # Clear up after ourselves
1581                 del self.Subst["__BUG_NUMBER__"]
1582                 del self.Subst["__STABLE_WARNING__"]
1583
1584         if action and self.logger:
1585             self.logger.log(["closing bugs"] + bugs)
1586
1587         summary += "\n"
1588
1589         return summary
1590
1591     ###########################################################################
1592
1593     def announce(self, short_summary, action):
1594         """
1595         Send an announce mail about a new upload.
1596
1597         @type short_summary: string
1598         @param short_summary: Short summary text to include in the mail
1599
1600         @type action: bool
1601         @param action: Set to false no real action will be done.
1602
1603         @rtype: string
1604         @return: Textstring about action taken.
1605
1606         """
1607
1608         cnf = Config()
1609         announcetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.announce')
1610
1611         # Only do announcements for source uploads with a recent dpkg-dev installed
1612         if float(self.pkg.changes.get("format", 0)) < 1.6 or not \
1613            self.pkg.changes["architecture"].has_key("source"):
1614             return ""
1615
1616         lists_done = {}
1617         summary = ""
1618
1619         self.Subst["__SHORT_SUMMARY__"] = short_summary
1620
1621         for dist in self.pkg.changes["distribution"].keys():
1622             announce_list = cnf.Find("Suite::%s::Announce" % (dist))
1623             if announce_list == "" or lists_done.has_key(announce_list):
1624                 continue
1625
1626             lists_done[announce_list] = 1
1627             summary += "Announcing to %s\n" % (announce_list)
1628
1629             if action:
1630                 self.update_subst()
1631                 self.Subst["__ANNOUNCE_LIST_ADDRESS__"] = announce_list
1632                 if cnf.get("Dinstall::TrackingServer") and \
1633                    self.pkg.changes["architecture"].has_key("source"):
1634                     trackingsendto = "Bcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
1635                     self.Subst["__ANNOUNCE_LIST_ADDRESS__"] += "\n" + trackingsendto
1636
1637                 mail_message = utils.TemplateSubst(self.Subst, announcetemplate)
1638                 utils.send_mail(mail_message)
1639
1640                 del self.Subst["__ANNOUNCE_LIST_ADDRESS__"]
1641
1642         if cnf.FindB("Dinstall::CloseBugs"):
1643             summary = self.close_bugs(summary, action)
1644
1645         del self.Subst["__SHORT_SUMMARY__"]
1646
1647         return summary
1648
1649     ###########################################################################
1650
1651     def accept (self, summary, short_summary, targetdir=None):
1652         """
1653         Accept an upload.
1654
1655         This moves all files referenced from the .changes into the I{accepted}
1656         queue, sends the accepted mail, announces to lists, closes bugs and
1657         also checks for override disparities. If enabled it will write out
1658         the version history for the BTS Version Tracking and will finally call
1659         L{queue_build}.
1660
1661         @type summary: string
1662         @param summary: Summary text
1663
1664         @type short_summary: string
1665         @param short_summary: Short summary
1666
1667         """
1668
1669         cnf = Config()
1670         stats = SummaryStats()
1671
1672         accepttemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.accepted')
1673
1674         if targetdir is None:
1675             targetdir = cnf["Dir::Queue::Accepted"]
1676
1677         print "Accepting."
1678         if self.logger:
1679             self.logger.log(["Accepting changes", self.pkg.changes_file])
1680
1681         self.pkg.write_dot_dak(targetdir)
1682
1683         # Move all the files into the accepted directory
1684         utils.move(self.pkg.changes_file, targetdir)
1685
1686         for name, entry in sorted(self.pkg.files.items()):
1687             utils.move(name, targetdir)
1688             stats.accept_bytes += float(entry["size"])
1689
1690         stats.accept_count += 1
1691
1692         # Send accept mail, announce to lists, close bugs and check for
1693         # override disparities
1694         if not cnf["Dinstall::Options::No-Mail"]:
1695             self.update_subst()
1696             self.Subst["__SUITE__"] = ""
1697             self.Subst["__SUMMARY__"] = summary
1698             mail_message = utils.TemplateSubst(self.Subst, accepttemplate)
1699             utils.send_mail(mail_message)
1700             self.announce(short_summary, 1)
1701
1702         ## Helper stuff for DebBugs Version Tracking
1703         if cnf.Find("Dir::Queue::BTSVersionTrack"):
1704             # ??? once queue/* is cleared on *.d.o and/or reprocessed
1705             # the conditionalization on dsc["bts changelog"] should be
1706             # dropped.
1707
1708             # Write out the version history from the changelog
1709             if self.pkg.changes["architecture"].has_key("source") and \
1710                self.pkg.dsc.has_key("bts changelog"):
1711
1712                 (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
1713                 version_history = os.fdopen(fd, 'w')
1714                 version_history.write(self.pkg.dsc["bts changelog"])
1715                 version_history.close()
1716                 filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
1717                                       self.pkg.changes_file[:-8]+".versions")
1718                 os.rename(temp_filename, filename)
1719                 os.chmod(filename, 0644)
1720
1721             # Write out the binary -> source mapping.
1722             (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
1723             debinfo = os.fdopen(fd, 'w')
1724             for name, entry in sorted(self.pkg.files.items()):
1725                 if entry["type"] == "deb":
1726                     line = " ".join([entry["package"], entry["version"],
1727                                      entry["architecture"], entry["source package"],
1728                                      entry["source version"]])
1729                     debinfo.write(line+"\n")
1730             debinfo.close()
1731             filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
1732                                   self.pkg.changes_file[:-8]+".debinfo")
1733             os.rename(temp_filename, filename)
1734             os.chmod(filename, 0644)
1735
1736         # Its is Cnf["Dir::Queue::Accepted"] here, not targetdir!
1737         # <Ganneff> we do call queue_build too
1738         # <mhy> well yes, we'd have had to if we were inserting into accepted
1739         # <Ganneff> now. thats database only.
1740         # <mhy> urgh, that's going to get messy
1741         # <Ganneff> so i make the p-n call to it *also* using accepted/
1742         # <mhy> but then the packages will be in the queue_build table without the files being there
1743         # <Ganneff> as the buildd queue is only regenerated whenever unchecked runs
1744         # <mhy> ah, good point
1745         # <Ganneff> so it will work out, as unchecked move it over
1746         # <mhy> that's all completely sick
1747         # <Ganneff> yes
1748
1749         # This routine returns None on success or an error on failure
1750         res = get_or_set_queue('accepted').autobuild_upload(self.pkg, cnf["Dir::Queue::Accepted"])
1751         if res:
1752             utils.fubar(res)
1753
1754
1755     def check_override(self):
1756         """
1757         Checks override entries for validity. Mails "Override disparity" warnings,
1758         if that feature is enabled.
1759
1760         Abandons the check if
1761           - override disparity checks are disabled
1762           - mail sending is disabled
1763         """
1764
1765         cnf = Config()
1766
1767         # Abandon the check if:
1768         #  a) override disparity checks have been disabled
1769         #  b) we're not sending mail
1770         if not cnf.FindB("Dinstall::OverrideDisparityCheck") or \
1771            cnf["Dinstall::Options::No-Mail"]:
1772             return
1773
1774         summary = self.pkg.check_override()
1775
1776         if summary == "":
1777             return
1778
1779         overridetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.override-disparity')
1780
1781         self.update_subst()
1782         self.Subst["__SUMMARY__"] = summary
1783         mail_message = utils.TemplateSubst(self.Subst, overridetemplate)
1784         utils.send_mail(mail_message)
1785         del self.Subst["__SUMMARY__"]
1786
1787     ###########################################################################
1788
1789     def remove(self, dir=None):
1790         """
1791         Used (for instance) in p-u to remove the package from unchecked
1792         """
1793         if dir is None:
1794             os.chdir(self.pkg.directory)
1795         else:
1796             os.chdir(dir)
1797
1798         for f in self.pkg.files.keys():
1799             os.unlink(f)
1800         os.unlink(self.pkg.changes_file)
1801
1802     ###########################################################################
1803
1804     def move_to_dir (self, dest, perms=0660, changesperms=0664):
1805         """
1806         Move files to dest with certain perms/changesperms
1807         """
1808         utils.move(self.pkg.changes_file, dest, perms=changesperms)
1809         for f in self.pkg.files.keys():
1810             utils.move(f, dest, perms=perms)
1811
1812     ###########################################################################
1813
1814     def force_reject(self, reject_files):
1815         """
1816         Forcefully move files from the current directory to the
1817         reject directory.  If any file already exists in the reject
1818         directory it will be moved to the morgue to make way for
1819         the new file.
1820
1821         @type files: dict
1822         @param files: file dictionary
1823
1824         """
1825
1826         cnf = Config()
1827
1828         for file_entry in reject_files:
1829             # Skip any files which don't exist or which we don't have permission to copy.
1830             if os.access(file_entry, os.R_OK) == 0:
1831                 continue
1832
1833             dest_file = os.path.join(cnf["Dir::Queue::Reject"], file_entry)
1834
1835             try:
1836                 dest_fd = os.open(dest_file, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0644)
1837             except OSError, e:
1838                 # File exists?  Let's try and move it to the morgue
1839                 if e.errno == errno.EEXIST:
1840                     morgue_file = os.path.join(cnf["Dir::Morgue"], cnf["Dir::MorgueReject"], file_entry)
1841                     try:
1842                         morgue_file = utils.find_next_free(morgue_file)
1843                     except NoFreeFilenameError:
1844                         # Something's either gone badly Pete Tong, or
1845                         # someone is trying to exploit us.
1846                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file_entry))
1847                         return
1848                     utils.move(dest_file, morgue_file, perms=0660)
1849                     try:
1850                         dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
1851                     except OSError, e:
1852                         # Likewise
1853                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file_entry))
1854                         return
1855                 else:
1856                     raise
1857             # If we got here, we own the destination file, so we can
1858             # safely overwrite it.
1859             utils.move(file_entry, dest_file, 1, perms=0660)
1860             os.close(dest_fd)
1861
1862     ###########################################################################
1863     def do_reject (self, manual=0, reject_message="", note=""):
1864         """
1865         Reject an upload. If called without a reject message or C{manual} is
1866         true, spawn an editor so the user can write one.
1867
1868         @type manual: bool
1869         @param manual: manual or automated rejection
1870
1871         @type reject_message: string
1872         @param reject_message: A reject message
1873
1874         @return: 0
1875
1876         """
1877         # If we weren't given a manual rejection message, spawn an
1878         # editor so the user can add one in...
1879         if manual and not reject_message:
1880             (fd, temp_filename) = utils.temp_filename()
1881             temp_file = os.fdopen(fd, 'w')
1882             if len(note) > 0:
1883                 for line in note:
1884                     temp_file.write(line)
1885             temp_file.close()
1886             editor = os.environ.get("EDITOR","vi")
1887             answer = 'E'
1888             while answer == 'E':
1889                 os.system("%s %s" % (editor, temp_filename))
1890                 temp_fh = utils.open_file(temp_filename)
1891                 reject_message = "".join(temp_fh.readlines())
1892                 temp_fh.close()
1893                 print "Reject message:"
1894                 print utils.prefix_multi_line_string(reject_message,"  ",include_blank_lines=1)
1895                 prompt = "[R]eject, Edit, Abandon, Quit ?"
1896                 answer = "XXX"
1897                 while prompt.find(answer) == -1:
1898                     answer = utils.our_raw_input(prompt)
1899                     m = re_default_answer.search(prompt)
1900                     if answer == "":
1901                         answer = m.group(1)
1902                     answer = answer[:1].upper()
1903             os.unlink(temp_filename)
1904             if answer == 'A':
1905                 return 1
1906             elif answer == 'Q':
1907                 sys.exit(0)
1908
1909         print "Rejecting.\n"
1910
1911         cnf = Config()
1912
1913         reason_filename = self.pkg.changes_file[:-8] + ".reason"
1914         reason_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
1915
1916         # Move all the files into the reject directory
1917         reject_files = self.pkg.files.keys() + [self.pkg.changes_file]
1918         self.force_reject(reject_files)
1919
1920         # If we fail here someone is probably trying to exploit the race
1921         # so let's just raise an exception ...
1922         if os.path.exists(reason_filename):
1923             os.unlink(reason_filename)
1924         reason_fd = os.open(reason_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
1925
1926         rej_template = os.path.join(cnf["Dir::Templates"], "queue.rejected")
1927
1928         self.update_subst()
1929         if not manual:
1930             self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
1931             self.Subst["__MANUAL_REJECT_MESSAGE__"] = ""
1932             self.Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
1933             os.write(reason_fd, reject_message)
1934             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
1935         else:
1936             # Build up the rejection email
1937             user_email_address = utils.whoami() + " <%s>" % (cnf["Dinstall::MyAdminAddress"])
1938             self.Subst["__REJECTOR_ADDRESS__"] = user_email_address
1939             self.Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
1940             self.Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
1941             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
1942             # Write the rejection email out as the <foo>.reason file
1943             os.write(reason_fd, reject_mail_message)
1944
1945         del self.Subst["__REJECTOR_ADDRESS__"]
1946         del self.Subst["__MANUAL_REJECT_MESSAGE__"]
1947         del self.Subst["__CC__"]
1948
1949         os.close(reason_fd)
1950
1951         # Send the rejection mail if appropriate
1952         if not cnf["Dinstall::Options::No-Mail"]:
1953             utils.send_mail(reject_mail_message)
1954
1955         if self.logger:
1956             self.logger.log(["rejected", self.pkg.changes_file])
1957
1958         return 0
1959
1960     ################################################################################
1961     def in_override_p(self, package, component, suite, binary_type, file, session):
1962         """
1963         Check if a package already has override entries in the DB
1964
1965         @type package: string
1966         @param package: package name
1967
1968         @type component: string
1969         @param component: database id of the component
1970
1971         @type suite: int
1972         @param suite: database id of the suite
1973
1974         @type binary_type: string
1975         @param binary_type: type of the package
1976
1977         @type file: string
1978         @param file: filename we check
1979
1980         @return: the database result. But noone cares anyway.
1981
1982         """
1983
1984         cnf = Config()
1985
1986         if binary_type == "": # must be source
1987             file_type = "dsc"
1988         else:
1989             file_type = binary_type
1990
1991         # Override suite name; used for example with proposed-updates
1992         if cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
1993             suite = cnf["Suite::%s::OverrideSuite" % (suite)]
1994
1995         result = get_override(package, suite, component, file_type, session)
1996
1997         # If checking for a source package fall back on the binary override type
1998         if file_type == "dsc" and len(result) < 1:
1999             result = get_override(package, suite, component, ['deb', 'udeb'], session)
2000
2001         # Remember the section and priority so we can check them later if appropriate
2002         if len(result) > 0:
2003             result = result[0]
2004             self.pkg.files[file]["override section"] = result.section.section
2005             self.pkg.files[file]["override priority"] = result.priority.priority
2006             return result
2007
2008         return None
2009
2010     ################################################################################
2011     def get_anyversion(self, sv_list, suite):
2012         """
2013         @type sv_list: list
2014         @param sv_list: list of (suite, version) tuples to check
2015
2016         @type suite: string
2017         @param suite: suite name
2018
2019         Description: TODO
2020         """
2021         Cnf = Config()
2022         anyversion = None
2023         anysuite = [suite] + Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
2024         for (s, v) in sv_list:
2025             if s in [ x.lower() for x in anysuite ]:
2026                 if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
2027                     anyversion = v
2028
2029         return anyversion
2030
2031     ################################################################################
2032
2033     def cross_suite_version_check(self, sv_list, file, new_version, sourceful=False):
2034         """
2035         @type sv_list: list
2036         @param sv_list: list of (suite, version) tuples to check
2037
2038         @type file: string
2039         @param file: XXX
2040
2041         @type new_version: string
2042         @param new_version: XXX
2043
2044         Ensure versions are newer than existing packages in target
2045         suites and that cross-suite version checking rules as
2046         set out in the conf file are satisfied.
2047         """
2048
2049         cnf = Config()
2050
2051         # Check versions for each target suite
2052         for target_suite in self.pkg.changes["distribution"].keys():
2053             must_be_newer_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
2054             must_be_older_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]
2055
2056             # Enforce "must be newer than target suite" even if conffile omits it
2057             if target_suite not in must_be_newer_than:
2058                 must_be_newer_than.append(target_suite)
2059
2060             for (suite, existent_version) in sv_list:
2061                 vercmp = apt_pkg.VersionCompare(new_version, existent_version)
2062
2063                 if suite in must_be_newer_than and sourceful and vercmp < 1:
2064                     self.rejects.append("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
2065
2066                 if suite in must_be_older_than and vercmp > -1:
2067                     cansave = 0
2068
2069                     if self.pkg.changes.get('distribution-version', {}).has_key(suite):
2070                         # we really use the other suite, ignoring the conflicting one ...
2071                         addsuite = self.pkg.changes["distribution-version"][suite]
2072
2073                         add_version = self.get_anyversion(sv_list, addsuite)
2074                         target_version = self.get_anyversion(sv_list, target_suite)
2075
2076                         if not add_version:
2077                             # not add_version can only happen if we map to a suite
2078                             # that doesn't enhance the suite we're propup'ing from.
2079                             # so "propup-ver x a b c; map a d" is a problem only if
2080                             # d doesn't enhance a.
2081                             #
2082                             # i think we could always propagate in this case, rather
2083                             # than complaining. either way, this isn't a REJECT issue
2084                             #
2085                             # And - we really should complain to the dorks who configured dak
2086                             self.warnings.append("%s is mapped to, but not enhanced by %s - adding anyways" % (suite, addsuite))
2087                             self.pkg.changes.setdefault("propdistribution", {})
2088                             self.pkg.changes["propdistribution"][addsuite] = 1
2089                             cansave = 1
2090                         elif not target_version:
2091                             # not targets_version is true when the package is NEW
2092                             # we could just stick with the "...old version..." REJECT
2093                             # for this, I think.
2094                             self.rejects.append("Won't propogate NEW packages.")
2095                         elif apt_pkg.VersionCompare(new_version, add_version) < 0:
2096                             # propogation would be redundant. no need to reject though.
2097                             self.warnings.append("ignoring versionconflict: %s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
2098                             cansave = 1
2099                         elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
2100                              apt_pkg.VersionCompare(add_version, target_version) >= 0:
2101                             # propogate!!
2102                             self.warnings.append("Propogating upload to %s" % (addsuite))
2103                             self.pkg.changes.setdefault("propdistribution", {})
2104                             self.pkg.changes["propdistribution"][addsuite] = 1
2105                             cansave = 1
2106
2107                     if not cansave:
2108                         self.reject.append("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
2109
2110     ################################################################################
2111     def check_binary_against_db(self, file, session):
2112         # Ensure version is sane
2113         q = session.query(BinAssociation)
2114         q = q.join(DBBinary).filter(DBBinary.package==self.pkg.files[file]["package"])
2115         q = q.join(Architecture).filter(Architecture.arch_string.in_([self.pkg.files[file]["architecture"], 'all']))
2116
2117         self.cross_suite_version_check([ (x.suite.suite_name, x.binary.version) for x in q.all() ],
2118                                        file, self.pkg.files[file]["version"], sourceful=False)
2119
2120         # Check for any existing copies of the file
2121         q = session.query(DBBinary).filter_by(package=self.pkg.files[file]["package"])
2122         q = q.filter_by(version=self.pkg.files[file]["version"])
2123         q = q.join(Architecture).filter_by(arch_string=self.pkg.files[file]["architecture"])
2124
2125         if q.count() > 0:
2126             self.rejects.append("%s: can not overwrite existing copy already in the archive." % (file))
2127
2128     ################################################################################
2129
2130     def check_source_against_db(self, file, session):
2131         """
2132         """
2133         source = self.pkg.dsc.get("source")
2134         version = self.pkg.dsc.get("version")
2135
2136         # Ensure version is sane
2137         q = session.query(SrcAssociation)
2138         q = q.join(DBSource).filter(DBSource.source==source)
2139
2140         self.cross_suite_version_check([ (x.suite.suite_name, x.source.version) for x in q.all() ],
2141                                        file, version, sourceful=True)
2142
2143     ################################################################################
2144     def check_dsc_against_db(self, file, session):
2145         """
2146
2147         @warning: NB: this function can remove entries from the 'files' index [if
2148          the orig tarball is a duplicate of the one in the archive]; if
2149          you're iterating over 'files' and call this function as part of
2150          the loop, be sure to add a check to the top of the loop to
2151          ensure you haven't just tried to dereference the deleted entry.
2152
2153         """
2154
2155         Cnf = Config()
2156         self.pkg.orig_files = {} # XXX: do we need to clear it?
2157         orig_files = self.pkg.orig_files
2158
2159         # Try and find all files mentioned in the .dsc.  This has
2160         # to work harder to cope with the multiple possible
2161         # locations of an .orig.tar.gz.
2162         # The ordering on the select is needed to pick the newest orig
2163         # when it exists in multiple places.
2164         for dsc_name, dsc_entry in self.pkg.dsc_files.items():
2165             found = None
2166             if self.pkg.files.has_key(dsc_name):
2167                 actual_md5 = self.pkg.files[dsc_name]["md5sum"]
2168                 actual_size = int(self.pkg.files[dsc_name]["size"])
2169                 found = "%s in incoming" % (dsc_name)
2170
2171                 # Check the file does not already exist in the archive
2172                 ql = get_poolfile_like_name(dsc_name, session)
2173
2174                 # Strip out anything that isn't '%s' or '/%s$'
2175                 for i in ql:
2176                     if not i.filename.endswith(dsc_name):
2177                         ql.remove(i)
2178
2179                 # "[dak] has not broken them.  [dak] has fixed a
2180                 # brokenness.  Your crappy hack exploited a bug in
2181                 # the old dinstall.
2182                 #
2183                 # "(Come on!  I thought it was always obvious that
2184                 # one just doesn't release different files with
2185                 # the same name and version.)"
2186                 #                        -- ajk@ on d-devel@l.d.o
2187
2188                 if len(ql) > 0:
2189                     # Ignore exact matches for .orig.tar.gz
2190                     match = 0
2191                     if re_is_orig_source.match(dsc_name):
2192                         for i in ql:
2193                             if self.pkg.files.has_key(dsc_name) and \
2194                                int(self.pkg.files[dsc_name]["size"]) == int(i.filesize) and \
2195                                self.pkg.files[dsc_name]["md5sum"] == i.md5sum:
2196                                 self.warnings.append("ignoring %s, since it's already in the archive." % (dsc_name))
2197                                 # TODO: Don't delete the entry, just mark it as not needed
2198                                 # This would fix the stupidity of changing something we often iterate over
2199                                 # whilst we're doing it
2200                                 del self.pkg.files[dsc_name]
2201                                 if not orig_files.has_key(dsc_name):
2202                                     orig_files[dsc_name] = {}
2203                                 orig_files[dsc_name]["path"] = os.path.join(i.location.path, i.filename)
2204                                 match = 1
2205
2206                     if not match:
2207                         self.rejects.append("can not overwrite existing copy of '%s' already in the archive." % (dsc_name))
2208
2209             elif re_is_orig_source.match(dsc_name):
2210                 # Check in the pool
2211                 ql = get_poolfile_like_name(dsc_name, session)
2212
2213                 # Strip out anything that isn't '%s' or '/%s$'
2214                 # TODO: Shouldn't we just search for things which end with our string explicitly in the SQL?
2215                 for i in ql:
2216                     if not i.filename.endswith(dsc_name):
2217                         ql.remove(i)
2218
2219                 if len(ql) > 0:
2220                     # Unfortunately, we may get more than one match here if,
2221                     # for example, the package was in potato but had an -sa
2222                     # upload in woody.  So we need to choose the right one.
2223
2224                     # default to something sane in case we don't match any or have only one
2225                     x = ql[0]
2226
2227                     if len(ql) > 1:
2228                         for i in ql:
2229                             old_file = os.path.join(i.location.path, i.filename)
2230                             old_file_fh = utils.open_file(old_file)
2231                             actual_md5 = apt_pkg.md5sum(old_file_fh)
2232                             old_file_fh.close()
2233                             actual_size = os.stat(old_file)[stat.ST_SIZE]
2234                             if actual_md5 == dsc_entry["md5sum"] and actual_size == int(dsc_entry["size"]):
2235                                 x = i
2236
2237                     old_file = os.path.join(i.location.path, i.filename)
2238                     old_file_fh = utils.open_file(old_file)
2239                     actual_md5 = apt_pkg.md5sum(old_file_fh)
2240                     old_file_fh.close()
2241                     actual_size = os.stat(old_file)[stat.ST_SIZE]
2242                     found = old_file
2243                     suite_type = x.location.archive_type
2244                     # need this for updating dsc_files in install()
2245                     dsc_entry["files id"] = x.file_id
2246                     # See install() in process-accepted...
2247                     if not orig_files.has_key(dsc_name):
2248                         orig_files[dsc_name] = {}
2249                     orig_files[dsc_name]["id"] = x.file_id
2250                     orig_files[dsc_name]["path"] = old_file
2251                     orig_files[dsc_name]["location"] = x.location.location_id
2252                 else:
2253                     # TODO: Record the queues and info in the DB so we don't hardcode all this crap
2254                     # Not there? Check the queue directories...
2255                     for directory in [ "Accepted", "New", "Byhand", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
2256                         if not Cnf.has_key("Dir::Queue::%s" % (directory)):
2257                             continue
2258                         in_otherdir = os.path.join(Cnf["Dir::Queue::%s" % (directory)], dsc_name)
2259                         if os.path.exists(in_otherdir):
2260                             in_otherdir_fh = utils.open_file(in_otherdir)
2261                             actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
2262                             in_otherdir_fh.close()
2263                             actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
2264                             found = in_otherdir
2265                             if not orig_files.has_key(dsc_name):
2266                                 orig_files[dsc_name] = {}
2267                             orig_files[dsc_name]["path"] = in_otherdir
2268
2269                     if not found:
2270                         self.rejects.append("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_name))
2271                         continue
2272             else:
2273                 self.rejects.append("%s refers to %s, but I can't find it in the queue." % (file, dsc_name))
2274                 continue
2275             if actual_md5 != dsc_entry["md5sum"]:
2276                 self.rejects.append("md5sum for %s doesn't match %s." % (found, file))
2277             if actual_size != int(dsc_entry["size"]):
2278                 self.rejects.append("size for %s doesn't match %s." % (found, file))
2279
2280     ################################################################################
2281     # This is used by process-new and process-holding to recheck a changes file
2282     # at the time we're running.  It mainly wraps various other internal functions
2283     # and is similar to accepted_checks - these should probably be tidied up
2284     # and combined
2285     def recheck(self, session):
2286         cnf = Config()
2287         for f in self.pkg.files.keys():
2288             # The .orig.tar.gz can disappear out from under us is it's a
2289             # duplicate of one in the archive.
2290             if not self.pkg.files.has_key(f):
2291                 continue
2292
2293             entry = self.pkg.files[f]
2294
2295             # Check that the source still exists
2296             if entry["type"] == "deb":
2297                 source_version = entry["source version"]
2298                 source_package = entry["source package"]
2299                 if not self.pkg.changes["architecture"].has_key("source") \
2300                    and not source_exists(source_package, source_version, self.pkg.changes["distribution"].keys(), session):
2301                     source_epochless_version = re_no_epoch.sub('', source_version)
2302                     dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
2303                     found = False
2304                     for q in ["Accepted", "Embargoed", "Unembargoed", "Newstage"]:
2305                         if cnf.has_key("Dir::Queue::%s" % (q)):
2306                             if os.path.exists(cnf["Dir::Queue::%s" % (q)] + '/' + dsc_filename):
2307                                 found = True
2308                     if not found:
2309                         self.rejects.append("no source found for %s %s (%s)." % (source_package, source_version, f))
2310
2311             # Version and file overwrite checks
2312             if entry["type"] == "deb":
2313                 self.check_binary_against_db(f, session)
2314             elif entry["type"] == "dsc":
2315                 self.check_source_against_db(f, session)
2316                 self.check_dsc_against_db(f, session)
2317
2318     ################################################################################
2319     def accepted_checks(self, overwrite_checks, session):
2320         # Recheck anything that relies on the database; since that's not
2321         # frozen between accept and our run time when called from p-a.
2322
2323         # overwrite_checks is set to False when installing to stable/oldstable
2324
2325         propogate={}
2326         nopropogate={}
2327
2328         # Find the .dsc (again)
2329         dsc_filename = None
2330         for f in self.pkg.files.keys():
2331             if self.pkg.files[f]["type"] == "dsc":
2332                 dsc_filename = f
2333
2334         for checkfile in self.pkg.files.keys():
2335             # The .orig.tar.gz can disappear out from under us is it's a
2336             # duplicate of one in the archive.
2337             if not self.pkg.files.has_key(checkfile):
2338                 continue
2339
2340             entry = self.pkg.files[checkfile]
2341
2342             # Check that the source still exists
2343             if entry["type"] == "deb":
2344                 source_version = entry["source version"]
2345                 source_package = entry["source package"]
2346                 if not self.pkg.changes["architecture"].has_key("source") \
2347                    and not source_exists(source_package, source_version,  self.pkg.changes["distribution"].keys()):
2348                     self.rejects.append("no source found for %s %s (%s)." % (source_package, source_version, checkfile))
2349
2350             # Version and file overwrite checks
2351             if overwrite_checks:
2352                 if entry["type"] == "deb":
2353                     self.check_binary_against_db(checkfile, session)
2354                 elif entry["type"] == "dsc":
2355                     self.check_source_against_db(checkfile, session)
2356                     self.check_dsc_against_db(dsc_filename, session)
2357
2358             # propogate in the case it is in the override tables:
2359             for suite in self.pkg.changes.get("propdistribution", {}).keys():
2360                 if self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), checkfile, session):
2361                     propogate[suite] = 1
2362                 else:
2363                     nopropogate[suite] = 1
2364
2365         for suite in propogate.keys():
2366             if suite in nopropogate:
2367                 continue
2368             self.pkg.changes["distribution"][suite] = 1
2369
2370         for checkfile in self.pkg.files.keys():
2371             # Check the package is still in the override tables
2372             for suite in self.pkg.changes["distribution"].keys():
2373                 if not self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), checkfile, session):
2374                     self.rejects.append("%s is NEW for %s." % (checkfile, suite))
2375
2376     ################################################################################
2377     # This is not really a reject, but an unaccept, but since a) the code for
2378     # that is non-trivial (reopen bugs, unannounce etc.), b) this should be
2379     # extremely rare, for now we'll go with whining at our admin folks...
2380
2381     def do_unaccept(self):
2382         cnf = Config()
2383
2384         self.update_subst()
2385         self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
2386         self.Subst["__REJECT_MESSAGE__"] = self.package_info()
2387         self.Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
2388         self.Subst["__BCC__"] = "X-DAK: dak process-accepted\nX-Katie: $Revision: 1.18 $"
2389         if cnf.has_key("Dinstall::Bcc"):
2390             self.Subst["__BCC__"] += "\nBcc: %s" % (cnf["Dinstall::Bcc"])
2391
2392         template = os.path.join(cnf["Dir::Templates"], "process-accepted.unaccept")
2393
2394         reject_mail_message = utils.TemplateSubst(self.Subst, template)
2395
2396         # Write the rejection email out as the <foo>.reason file
2397         reason_filename = os.path.basename(self.pkg.changes_file[:-8]) + ".reason"
2398         reject_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
2399
2400         # If we fail here someone is probably trying to exploit the race
2401         # so let's just raise an exception ...
2402         if os.path.exists(reject_filename):
2403             os.unlink(reject_filename)
2404
2405         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
2406         os.write(fd, reject_mail_message)
2407         os.close(fd)
2408
2409         utils.send_mail(reject_mail_message)
2410
2411         del self.Subst["__REJECTOR_ADDRESS__"]
2412         del self.Subst["__REJECT_MESSAGE__"]
2413         del self.Subst["__CC__"]
2414
2415     ################################################################################
2416     # If any file of an upload has a recent mtime then chances are good
2417     # the file is still being uploaded.
2418
2419     def upload_too_new(self):
2420         cnf = Config()
2421         too_new = False
2422         # Move back to the original directory to get accurate time stamps
2423         cwd = os.getcwd()
2424         os.chdir(self.pkg.directory)
2425         file_list = self.pkg.files.keys()
2426         file_list.extend(self.pkg.dsc_files.keys())
2427         file_list.append(self.pkg.changes_file)
2428         for f in file_list:
2429             try:
2430                 last_modified = time.time()-os.path.getmtime(f)
2431                 if last_modified < int(cnf["Dinstall::SkipTime"]):
2432                     too_new = True
2433                     break
2434             except:
2435                 pass
2436
2437         os.chdir(cwd)
2438         return too_new