]> git.decadent.org.uk Git - dak.git/blob - katie.py
fix .orig.tar.gz handling to check byhand, new and accepted too. fix various miscall...
[dak.git] / katie.py
1 #!/usr/bin/env python
2
3 # Utility functions for katie
4 # Copyright (C) 2001  James Troup <james@nocrew.org>
5 # $Id: katie.py,v 1.4 2002-02-15 04:01:14 troup Exp $
6
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 ###############################################################################
22
23 import cPickle, errno, os, pg, re, stat, string;
24 import utils, db_access;
25 import apt_inst, apt_pkg;
26
27 from types import *;
28 from string import lower;
29
30 ###############################################################################
31
32 re_isanum = re.compile (r"^\d+$");
33 re_default_answer = re.compile(r"\[(.*)\]");
34 re_fdnic = re.compile("\n\n");
35 re_bin_only_nmu_of_mu = re.compile("\.\d+\.\d+$");
36 re_bin_only_nmu_of_nmu = re.compile("\.\d+$");
37
38 ###############################################################################
39
40 # Convenience wrapper to carry around all the package information in
41
42 class Pkg:
43     def __init__(self, **kwds):
44         self.__dict__.update(kwds);
45
46     def update(self, **kwds):
47         self.__dict__.update(kwds);
48
49 ###############################################################################
50
51 class nmu_p:
52     # Read in the group maintainer override file
53     def __init__ (self, Cnf):
54         self.group_maint = {};
55         self.Cnf = Cnf;
56         if Cnf.get("Dinstall::GroupOverrideFilename"):
57             filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
58             file = utils.open_file(filename);
59             for line in file.readlines():
60                 line = lower(string.strip(utils.re_comments.sub('', line)));
61                 if line != "":
62                     self.group_maint[line] = 1;
63             file.close();
64
65     def is_an_nmu (self, pkg):
66         Cnf = self.Cnf;
67         changes = pkg.changes;
68         dsc = pkg.dsc;
69
70         (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (lower(dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"])));
71         # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
72         if dsc_name == lower(changes["maintainername"]) and \
73            (changes["changedby822"] == "" or lower(changes["changedbyname"]) == dsc_name):
74             return 0;
75
76         if dsc.has_key("uploaders"):
77             uploaders = string.split(lower(dsc["uploaders"]), ",");
78             uploadernames = {};
79             for i in uploaders:
80                 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
81                 uploadernames[name] = "";
82             if uploadernames.has_key(lower(changes["changedbyname"])):
83                 return 0;
84
85         # Some group maintained packages (e.g. Debian QA) are never NMU's
86         if self.group_maint.has_key(lower(changes["maintaineremail"])):
87             return 0;
88
89         return 1;
90
91 ###############################################################################
92
93 class Katie:
94
95     def __init__(self, Cnf):
96         self.Cnf = Cnf;
97         self.values = {};
98         # Read in the group-maint override file
99         self.nmu = nmu_p(Cnf);
100         self.accept_count = 0;
101         self.accept_bytes = 0L;
102         self.pkg = Pkg(changes = {}, dsc = {}, dsc_files = {}, files = {},
103                        legacy_source_untouchable = {});
104
105         # Initialize the substitution template mapping global
106         Subst = self.Subst = {};
107         Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
108         Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
109         Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
110         Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
111
112         self.projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
113         db_access.init(Cnf, self.projectB);
114
115     ###########################################################################
116
117     def init_vars (self):
118         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
119             exec "self.pkg.%s.clear();" % (i);
120         self.pkg.orig_tar_id = None;
121         self.pkg.orig_tar_location = "";
122
123     ###########################################################################
124
125     def update_vars (self):
126         dump_filename = self.pkg.changes_file[:-8]+".katie";
127         dump_file = utils.open_file(dump_filename);
128         p = cPickle.Unpickler(dump_file);
129         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
130             exec "self.pkg.%s.update(p.load());" % (i);
131         for i in [ "orig_tar_id", "orig_tar_location" ]:
132             exec "self.pkg.%s = p.load();" % (i);
133         dump_file.close();
134
135     ###########################################################################
136
137     # This could just dump the dictionaries as is, but I'd like to avoid
138     # this so there's some idea of what katie & lisa use from jennifer
139
140     def dump_vars(self, dest_dir):
141         for i in [ "changes", "dsc", "files", "dsc_files",
142                    "legacy_source_untouchable", "orig_tar_id", "orig_tar_location" ]:
143             exec "%s = self.pkg.%s;" % (i,i);
144         dump_filename = os.path.join(dest_dir,self.pkg.changes_file[:-8] + ".katie");
145         dump_file = utils.open_file(dump_filename, 'w');
146         p = cPickle.Pickler(dump_file, 1);
147         for i in [ "d_changes", "d_dsc", "d_files", "d_dsc_files" ]:
148             exec "%s = {}" % i;
149         ## files
150         for file in files.keys():
151             d_files[file] = {};
152             for i in [ "package", "version", "architecture", "type", "size",
153                        "md5sum", "component", "location id", "source package",
154                        "source version", "maintainer", "dbtype", "files id",
155                        "new", "section", "priority", "oldfiles", "othercomponents" ]:
156                 if files[file].has_key(i):
157                     d_files[file][i] = files[file][i];
158         ## changes
159         # Mandatory changes fields
160         for i in [ "distribution", "source", "architecture", "version", "maintainer",
161                    "urgency", "fingerprint" ]:
162             d_changes[i] = changes[i];
163         # Optional changes fields
164         for i in [ "changed-by", "changedby822", "maintainer822", "filecontents" ]:
165             if d_changes.has_key(i):
166                 d_changes[i] = changes[i];
167         ## dsc
168         for i in [ "source", "version", "maintainer", "fingerprint" ]:
169             if dsc.has_key(i):
170                 d_dsc[i] = dsc[i];
171         ## dsc_files
172         for file in dsc_files.keys():
173             d_dsc_files[file] = {};
174             # Mandatory dsc_files fields
175             for i in [ "size", "md5sum" ]:
176                 d_dsc_files[file][i] = dsc_files[file][i];
177             # Optional dsc_files fields
178             for i in [ "files id" ]:
179                 if dsc_files[file].has_key(i):
180                     d_dsc_files[file][i] = dsc_files[file][i];
181
182         for i in [ d_changes, d_dsc, d_files, d_dsc_files,
183                    legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
184             p.dump(i);
185         dump_file.close();
186
187     ###########################################################################
188
189     # Set up the per-package template substitution mappings
190
191     def update_subst (self, reject_message = ""):
192         Subst = self.Subst;
193         changes = self.pkg.changes;
194         # If jennifer crashed out in the right place, architecture may still be a string.
195         if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
196             changes["architecture"] = { "Unknown" : "" };
197         # and maintainer822 may not exist.
198         if not changes.has_key("maintainer822"):
199             changes["maintainer822"] = self.Cnf["Dinstall::MyEmailAddress"];
200
201         Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
202         Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file);
203         Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
204
205         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
206         if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
207             Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
208             Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
209             Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
210         else:
211             Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
212             Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
213             Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
214         if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
215             Subst["__MAINTAINER_TO__"] = Subst["__MAINTAINER_TO__"] + "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
216
217         Subst["__REJECT_MESSAGE__"] = reject_message;
218         Subst["__SOURCE__"] = changes.get("source", "Unknown");
219         Subst["__VERSION__"] = changes.get("version", "Unknown");
220
221     ###########################################################################
222
223     def build_summaries(self):
224         changes = self.pkg.changes;
225         files = self.pkg.files;
226
227         byhand = summary = new = "";
228
229         # changes["distribution"] may not exist in corner cases
230         # (e.g. unreadable changes files)
231         if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
232             changes["distribution"] = {};
233
234         file_keys = files.keys();
235         file_keys.sort();
236         for file in file_keys:
237             if files[file].has_key("byhand"):
238                 byhand = 1
239                 summary = summary + file + " byhand\n"
240             elif files[file].has_key("new"):
241                 new = 1
242                 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
243                 if files[file].has_key("othercomponents"):
244                     summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
245                 if files[file]["type"] == "deb":
246                     summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)))["Description"] + '\n';
247             else:
248                 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
249                 destination = self.Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
250                 summary = summary + file + "\n  to " + destination + "\n"
251
252         short_summary = summary;
253
254         # This is for direport's benefit...
255         f = re_fdnic.sub("\n .\n", changes.get("changes",""));
256
257         if byhand or new:
258             summary = summary + "Changes: " + f;
259
260         summary = summary + self.announce(short_summary, 0)
261
262         return (summary, short_summary);
263
264     ###########################################################################
265
266     def announce (self, short_summary, action):
267         Subst = self.Subst;
268         Cnf = self.Cnf;
269         changes = self.pkg.changes;
270         dsc = self.pkg.dsc;
271
272         # Only do announcements for source uploads with a recent dpkg-dev installed
273         if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
274             return ""
275
276         lists_done = {}
277         summary = ""
278         Subst["__SHORT_SUMMARY__"] = short_summary;
279
280         for dist in changes["distribution"].keys():
281             list = Cnf.Find("Suite::%s::Announce" % (dist))
282             if list == "" or lists_done.has_key(list):
283                 continue
284             lists_done[list] = 1
285             summary = summary + "Announcing to %s\n" % (list)
286
287             if action:
288                 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
289                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.announce","r").read());
290                 utils.send_mail (mail_message, "")
291
292         bugs = changes["closes"].keys()
293         bugs.sort()
294         if not self.nmu.is_an_nmu(self.pkg):
295             summary = summary + "Closing bugs: "
296             for bug in bugs:
297                 summary = summary + "%s " % (bug)
298                 if action:
299                     Subst["__BUG_NUMBER__"] = bug;
300                     if changes["distribution"].has_key("stable"):
301                         Subst["__STABLE_WARNING__"] = """
302     Note that this package is not part of the released stable Debian
303     distribution.  It may have dependencies on other unreleased software,
304     or other instabilities.  Please take care if you wish to install it.
305     The update will eventually make its way into the next released Debian
306     distribution."""
307                     else:
308                         Subst["__STABLE_WARNING__"] = "";
309                     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-close","r").read());
310                     utils.send_mail (mail_message, "")
311             if action:
312                 self.Logger.log(["closing bugs"]+bugs);
313         else:                     # NMU
314             summary = summary + "Setting bugs to severity fixed: "
315             control_message = ""
316             for bug in bugs:
317                 summary = summary + "%s " % (bug)
318                 control_message = control_message + "tag %s + fixed\n" % (bug)
319             if action and control_message != "":
320                 Subst["__CONTROL_MESSAGE__"] = control_message;
321                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-nmu-fixed","r").read());
322                 utils.send_mail (mail_message, "")
323             if action:
324                 self.Logger.log(["setting bugs to fixed"]+bugs);
325         summary = summary + "\n"
326
327         return summary
328
329     ###########################################################################
330
331     def accept (self, summary, short_summary):
332         Cnf = self.Cnf;
333         Subst = self.Subst;
334         files = self.pkg.files;
335
336         print "Accepting."
337         self.Logger.log(["Accepting changes",self.pkg.changes_file]);
338
339         self.dump_vars(Cnf["Dir::QueueAcceptedDir"]);
340
341         # Move all the files into the accepted directory
342         utils.move(self.pkg.changes_file, Cnf["Dir::QueueAcceptedDir"]);
343         file_keys = files.keys();
344         for file in file_keys:
345             utils.move(file, Cnf["Dir::QueueAcceptedDir"]);
346             self.accept_bytes = self.accept_bytes + float(files[file]["size"])
347         self.accept_count = self.accept_count + 1;
348
349         # Send accept mail, announce to lists, close bugs and check for
350         # override disparities
351         if not Cnf["Dinstall::Options::No-Mail"]:
352             Subst["__SUITE__"] = "";
353             Subst["__SUMMARY__"] = summary;
354             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.accepted","r").read());
355             utils.send_mail(mail_message, "")
356             self.announce(short_summary, 1)
357
358     ###########################################################################
359
360     def check_override (self):
361         Subst = self.Subst;
362         changes = self.pkg.changes;
363         files = self.pkg.files;
364
365         # Only check section & priority on sourceful uploads
366         if not changes["architecture"].has_key("source"):
367             return;
368
369         summary = "";
370         for file in files.keys():
371             if not files[file].has_key("new") and files[file]["type"] == "deb":
372                 section = files[file]["section"];
373                 override_section = files[file]["override section"];
374                 if lower(section) != lower(override_section) and section != "-":
375                     # Ignore this; it's a common mistake and not worth whining about
376                     if lower(section) == "non-us/main" and lower(override_section) == "non-us":
377                         continue;
378                     summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
379                 priority = files[file]["priority"];
380                 override_priority = files[file]["override priority"];
381                 if priority != override_priority and priority != "-":
382                     summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
383
384         if summary == "":
385             return;
386
387         Subst["__SUMMARY__"] = summary;
388         mail_message = utils.TemplateSubst(Subst,utils.open_file(self.Cnf["Dir::TemplatesDir"]+"/jennifer.override-disparity").read());
389         utils.send_mail (mail_message, "");
390
391     ###########################################################################
392
393     def force_move (self, files):
394         """Forcefully move files from the current directory to the reject
395            directory.  If any file already exists it will be moved to the
396            morgue to make way for the new file."""
397
398         Cnf = self.Cnf
399
400         for file in files:
401             # Skip any files which don't exist or which we don't have permission to copy.
402             if os.access(file,os.R_OK) == 0:
403                 continue;
404             dest_file = os.path.join(Cnf["Dir::QueueRejectDir"], file);
405             try:
406                 os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
407             except OSError, e:
408                 # File exists?  Let's try and move it to the morgue
409                 if errno.errorcode[e.errno] == 'EEXIST':
410                     morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueRejectDir"],file);
411                     try:
412                         morgue_file = utils.find_next_free(morgue_file);
413                     except utils.tried_too_hard_exc:
414                         # Something's either gone badly Pete Tong, or
415                         # someone is trying to exploit us.
416                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file));
417                         return;
418                     utils.move(dest_file, morgue_file);
419                     try:
420                         os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
421                     except OSError, e:
422                         # Likewise
423                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file));
424                         return;
425                 else:
426                     raise;
427             # If we got here, we own the destination file, so we can
428             # safely overwrite it.
429             utils.move(file, dest_file, 1);
430
431     ###########################################################################
432
433     def do_reject (self, manual = 0, reject_message = ""):
434         print "Rejecting.\n"
435
436         Cnf = self.Cnf;
437         Subst = self.Subst;
438         pkg = self.pkg;
439
440         reason_filename = pkg.changes_file[:-8] + ".reason";
441         reject_filename = Cnf["Dir::QueueRejectDir"] + '/' + reason_filename;
442
443         # Move all the files into the reject directory
444         reject_files = pkg.files.keys() + [pkg.changes_file];
445         self.force_move(reject_files);
446
447         # If we fail here someone is probably trying to exploit the race
448         # so let's just raise an exception ...
449         if os.path.exists(reject_filename):
450             os.unlink(reject_filename);
451         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
452
453         if not manual:
454             Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
455             Subst["__MANUAL_REJECT_MESSAGE__"] = "";
456             Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
457             os.write(fd, reject_message);
458             os.close(fd);
459         else:
460             # Build up the rejection email
461             user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"]);
462
463             Subst["__REJECTOR_ADDRESS__"] = user_email_address;
464             Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message;
465             Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
466             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
467
468             # Write the rejection email out as the <foo>.reason file
469             os.write(fd, reject_mail_message);
470             os.close(fd);
471
472             # If we weren't given a manual rejection message, spawn an
473             # editor so the user can add one in...
474             if reject_message == "":
475                 editor = os.environ.get("EDITOR","vi")
476                 result = os.system("%s +6 %s" % (editor, reject_filename))
477                 if result != 0:
478                     utils.fubar("editor invocation failed for '%s'!" % (reject_filename), result);
479
480         # Send the rejection mail if appropriate
481         if not Cnf["Dinstall::Options::No-Mail"]:
482             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
483             utils.send_mail (reject_mail_message, "");
484
485         self.Logger.log(["rejected", pkg.changes_file]);
486
487     ################################################################################
488
489     # Ensure that source exists somewhere in the archive for the binary
490     # upload being processed.
491     #
492     # (1) exact match                      => 1.0-3
493     # (2) Bin-only NMU of an MU            => 1.0-3.0.1
494     # (3) Bin-only NMU of a sourceful-NMU  => 1.0-3.1.1
495
496     def source_exists (self, package, source_version):
497         q = self.projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
498
499         # Reduce the query results to a list of version numbers
500         ql = map(lambda x: x[0], q.getresult());
501
502         # Try (1)
503         if ql.count(source_version):
504             return 1;
505
506         # Try (2)
507         orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
508         if ql.count(orig_source_version):
509             return 1;
510
511         # Try (3)
512         orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
513         if ql.count(orig_source_version):
514             return 1;
515
516         # No source found...
517         return 0;
518
519     ################################################################################
520
521     def in_override_p (self, package, component, suite, binary_type, file):
522         files = self.pkg.files;
523
524         if binary_type == "": # must be source
525             type = "dsc";
526         else:
527             type = binary_type;
528
529         # Override suite name; used for example with proposed-updates
530         if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
531             suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)];
532
533         # Avoid <undef> on unknown distributions
534         suite_id = db_access.get_suite_id(suite);
535         if suite_id == -1:
536             return None;
537         component_id = db_access.get_component_id(component);
538         type_id = db_access.get_override_type_id(type);
539
540         # FIXME: nasty non-US speficic hack
541         if lower(component[:7]) == "non-us/":
542             component = component[7:];
543
544         q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
545                            % (package, suite_id, component_id, type_id));
546         result = q.getresult();
547         # If checking for a source package fall back on the binary override type
548         if type == "dsc" and not result:
549             type_id = db_access.get_override_type_id("deb");
550             q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
551                                % (package, suite_id, component_id, type_id));
552             result = q.getresult();
553
554         # Remember the section and priority so we can check them later if appropriate
555         if result != []:
556             files[file]["override section"] = result[0][0];
557             files[file]["override priority"] = result[0][1];
558
559         return result;
560
561     ################################################################################
562
563     def reject (self, str, prefix="Rejected: "):
564         if str:
565             self.reject_message = self.reject_message + prefix + str + "\n";
566
567     def check_binaries_against_db(self, file, suite):
568         self.reject_message = "";
569         files = self.pkg.files;
570
571         # Find any old binary packages
572         q = self.projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name  FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
573                            % (files[file]["package"], suite, files[file]["architecture"]))
574         for oldfile in q.dictresult():
575             files[file]["oldfiles"][suite] = oldfile;
576             # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
577             if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
578                 self.reject("%s Old version `%s' >= new version `%s'." % (file, oldfile["version"], files[file]["version"]));
579         # Check for any existing copies of the file
580         q = self.projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
581         if q.getresult() != []:
582             self.reject("can not overwrite existing copy of '%s' already in the archive." % (file));
583
584         return self.reject_message;
585
586     ################################################################################
587
588     def check_source_against_db(self, file):
589         """Ensure source is newer than existing source in target suites."""
590         self.reject_message = "";
591         changes = self.pkg.changes;
592         dsc = self.pkg.dsc;
593
594         package = dsc.get("source");
595         new_version = dsc.get("version");
596         for suite in changes["distribution"].keys():
597             q = self.projectB.query("SELECT s.version FROM source s, src_associations sa, suite su WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id"
598                                % (package, suite));
599             ql = map(lambda x: x[0], q.getresult());
600             for old_version in ql:
601                 if apt_pkg.VersionCompare(new_version, old_version) != 1:
602                     self.reject("%s: Old version `%s' >= new version `%s'." % (file, old_version, new_version));
603         return self.reject_message;
604
605     ################################################################################
606
607     def check_dsc_against_db(self, file):
608         self.reject_message = "";
609         files = self.pkg.files;
610         dsc_files = self.pkg.dsc_files;
611         legacy_source_untouchable = self.pkg.legacy_source_untouchable;
612         orig_tar_gz = None;
613         found = None;
614
615         # Try and find all files mentioned in the .dsc.  This has
616         # to work harder to cope with the multiple possible
617         # locations of an .orig.tar.gz.
618         for dsc_file in dsc_files.keys():
619             if files.has_key(dsc_file):
620                 actual_md5 = files[dsc_file]["md5sum"];
621                 actual_size = int(files[dsc_file]["size"]);
622                 found = "%s in incoming" % (dsc_file)
623                 # Check the file does not already exist in the archive
624                 q = self.projectB.query("SELECT f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
625
626                 # "It has not broken them.  It has fixed a
627                 # brokenness.  Your crappy hack exploited a bug in
628                 # the old dinstall.
629                 #
630                 # "(Come on!  I thought it was always obvious that
631                 # one just doesn't release different files with
632                 # the same name and version.)"
633                 #                        -- ajk@ on d-devel@l.d.o
634
635                 if q.getresult() != []:
636                     self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file));
637             elif dsc_file[-12:] == ".orig.tar.gz":
638                 # Check in the pool
639                 q = self.projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
640                 ql = q.getresult();
641
642                 if ql != []:
643                     # Unfortunately, we make get more than one
644                     # match here if, for example, the package was
645                     # in potato but had a -sa upload in woody.  So
646                     # we need to choose the right one.
647
648                     x = ql[0]; # default to something sane in case we don't match any or have only one
649
650                     if len(ql) > 1:
651                         for i in ql:
652                             old_file = i[0] + i[1];
653                             actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
654                             actual_size = os.stat(old_file)[stat.ST_SIZE];
655                             if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
656                                 x = i;
657                             else:
658                                 legacy_source_untouchable[i[3]] = "";
659
660                     old_file = x[0] + x[1];
661                     actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
662                     actual_size = os.stat(old_file)[stat.ST_SIZE];
663                     found = old_file;
664                     suite_type = x[2];
665                     dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
666                     # See install() in katie...
667                     self.pkg.orig_tar_id = x[3];
668                     if suite_type == "legacy" or suite_type == "legacy-mixed":
669                         self.pkg.orig_tar_location = "legacy";
670                     else:
671                         self.pkg.orig_tar_location = x[4];
672                 else:
673                     # Not there? Check the queue directories...
674
675                     in_unchecked = os.path.join(self.Cnf["Dir::QueueUncheckedDir"],dsc_file);
676                     # See process_it() in jennifer for explanation of this
677                     if os.path.exists(in_unchecked):
678                         return (self.reject_message, orig_tar_gz);
679                     else:
680                         for dir in [ "Accepted", "New", "Byhand" ]:
681                             in_otherdir = os.path.join(self.Cnf["Dir::Queue%sDir" % (dir)],dsc_file);
682                             if os.path.exists(in_otherdir):
683                                 actual_md5 = apt_pkg.md5sum(utils.open_file(in_otherdir));
684                                 actual_size = os.stat(in_otherdir)[stat.ST_SIZE];
685                                 found = in_otherdir;
686
687                     if not found:
688                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file));
689                         continue;
690             else:
691                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file));
692                 continue;
693             if actual_md5 != dsc_files[dsc_file]["md5sum"]:
694                 self.reject("md5sum for %s doesn't match %s." % (found, file));
695             if actual_size != int(dsc_files[dsc_file]["size"]):
696                 self.reject("size for %s doesn't match %s." % (found, file));
697
698         return (self.reject_message, orig_tar_gz);