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