]> git.decadent.org.uk Git - dak.git/blob - katie.py
prepend in reject() to avoid double new lines
[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.7 2002-02-22 01:03:13 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", "changedby822", "changedbyname",
162                    "maintainername", "maintaineremail", "closes" ]:
163             d_changes[i] = changes[i];
164         # Optional changes fields
165         for i in [ "changed-by", "maintainer822", "filecontents", "format" ]:
166             if changes.has_key(i):
167                 d_changes[i] = changes[i];
168         ## dsc
169         for i in [ "source", "version", "maintainer", "fingerprint", "uploaders" ]:
170             if dsc.has_key(i):
171                 d_dsc[i] = dsc[i];
172         ## dsc_files
173         for file in dsc_files.keys():
174             d_dsc_files[file] = {};
175             # Mandatory dsc_files fields
176             for i in [ "size", "md5sum" ]:
177                 d_dsc_files[file][i] = dsc_files[file][i];
178             # Optional dsc_files fields
179             for i in [ "files id" ]:
180                 if dsc_files[file].has_key(i):
181                     d_dsc_files[file][i] = dsc_files[file][i];
182
183         for i in [ d_changes, d_dsc, d_files, d_dsc_files,
184                    legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
185             p.dump(i);
186         dump_file.close();
187
188     ###########################################################################
189
190     # Set up the per-package template substitution mappings
191
192     def update_subst (self, reject_message = ""):
193         Subst = self.Subst;
194         changes = self.pkg.changes;
195         # If jennifer crashed out in the right place, architecture may still be a string.
196         if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
197             changes["architecture"] = { "Unknown" : "" };
198         # and maintainer822 may not exist.
199         if not changes.has_key("maintainer822"):
200             changes["maintainer822"] = self.Cnf["Dinstall::MyEmailAddress"];
201
202         Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
203         Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file);
204         Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
205
206         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
207         if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
208             Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
209             Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
210             Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
211         else:
212             Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
213             Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
214             Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
215         if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
216             Subst["__MAINTAINER_TO__"] = Subst["__MAINTAINER_TO__"] + "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
217
218         Subst["__REJECT_MESSAGE__"] = reject_message;
219         Subst["__SOURCE__"] = changes.get("source", "Unknown");
220         Subst["__VERSION__"] = changes.get("version", "Unknown");
221
222     ###########################################################################
223
224     def build_summaries(self):
225         changes = self.pkg.changes;
226         files = self.pkg.files;
227
228         byhand = summary = new = "";
229
230         # changes["distribution"] may not exist in corner cases
231         # (e.g. unreadable changes files)
232         if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
233             changes["distribution"] = {};
234
235         file_keys = files.keys();
236         file_keys.sort();
237         for file in file_keys:
238             if files[file].has_key("byhand"):
239                 byhand = 1
240                 summary = summary + file + " byhand\n"
241             elif files[file].has_key("new"):
242                 new = 1
243                 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
244                 if files[file].has_key("othercomponents"):
245                     summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
246                 if files[file]["type"] == "deb":
247                     summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)))["Description"] + '\n';
248             else:
249                 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
250                 destination = self.Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
251                 summary = summary + file + "\n  to " + destination + "\n"
252
253         short_summary = summary;
254
255         # This is for direport's benefit...
256         f = re_fdnic.sub("\n .\n", changes.get("changes",""));
257
258         if byhand or new:
259             summary = summary + "Changes: " + f;
260
261         summary = summary + self.announce(short_summary, 0)
262
263         return (summary, short_summary);
264
265     ###########################################################################
266
267     def announce (self, short_summary, action):
268         Subst = self.Subst;
269         Cnf = self.Cnf;
270         changes = self.pkg.changes;
271         dsc = self.pkg.dsc;
272
273         # Only do announcements for source uploads with a recent dpkg-dev installed
274         if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
275             return ""
276
277         lists_done = {}
278         summary = ""
279         Subst["__SHORT_SUMMARY__"] = short_summary;
280
281         for dist in changes["distribution"].keys():
282             list = Cnf.Find("Suite::%s::Announce" % (dist))
283             if list == "" or lists_done.has_key(list):
284                 continue
285             lists_done[list] = 1
286             summary = summary + "Announcing to %s\n" % (list)
287
288             if action:
289                 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
290                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.announce","r").read());
291                 utils.send_mail (mail_message, "")
292
293         bugs = changes["closes"].keys()
294         bugs.sort()
295         if not self.nmu.is_an_nmu(self.pkg):
296             summary = summary + "Closing bugs: "
297             for bug in bugs:
298                 summary = summary + "%s " % (bug)
299                 if action:
300                     Subst["__BUG_NUMBER__"] = bug;
301                     if changes["distribution"].has_key("stable"):
302                         Subst["__STABLE_WARNING__"] = """
303     Note that this package is not part of the released stable Debian
304     distribution.  It may have dependencies on other unreleased software,
305     or other instabilities.  Please take care if you wish to install it.
306     The update will eventually make its way into the next released Debian
307     distribution."""
308                     else:
309                         Subst["__STABLE_WARNING__"] = "";
310                     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-close","r").read());
311                     utils.send_mail (mail_message, "")
312             if action:
313                 self.Logger.log(["closing bugs"]+bugs);
314         else:                     # NMU
315             summary = summary + "Setting bugs to severity fixed: "
316             control_message = ""
317             for bug in bugs:
318                 summary = summary + "%s " % (bug)
319                 control_message = control_message + "tag %s + fixed\n" % (bug)
320             if action and control_message != "":
321                 Subst["__CONTROL_MESSAGE__"] = control_message;
322                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-nmu-fixed","r").read());
323                 utils.send_mail (mail_message, "")
324             if action:
325                 self.Logger.log(["setting bugs to fixed"]+bugs);
326         summary = summary + "\n"
327
328         return summary
329
330     ###########################################################################
331
332     def accept (self, summary, short_summary):
333         Cnf = self.Cnf;
334         Subst = self.Subst;
335         files = self.pkg.files;
336
337         print "Accepting."
338         self.Logger.log(["Accepting changes",self.pkg.changes_file]);
339
340         self.dump_vars(Cnf["Dir::QueueAcceptedDir"]);
341
342         # Move all the files into the accepted directory
343         utils.move(self.pkg.changes_file, Cnf["Dir::QueueAcceptedDir"]);
344         file_keys = files.keys();
345         for file in file_keys:
346             utils.move(file, Cnf["Dir::QueueAcceptedDir"]);
347             self.accept_bytes = self.accept_bytes + float(files[file]["size"])
348         self.accept_count = self.accept_count + 1;
349
350         # Send accept mail, announce to lists, close bugs and check for
351         # override disparities
352         if not Cnf["Dinstall::Options::No-Mail"]:
353             Subst["__SUITE__"] = "";
354             Subst["__SUMMARY__"] = summary;
355             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.accepted","r").read());
356             utils.send_mail(mail_message, "")
357             self.announce(short_summary, 1)
358
359     ###########################################################################
360
361     def check_override (self):
362         Subst = self.Subst;
363         changes = self.pkg.changes;
364         files = self.pkg.files;
365
366         # Only check section & priority on sourceful uploads
367         if not changes["architecture"].has_key("source"):
368             return;
369
370         summary = "";
371         for file in files.keys():
372             if not files[file].has_key("new") and files[file]["type"] == "deb":
373                 section = files[file]["section"];
374                 override_section = files[file]["override section"];
375                 if lower(section) != lower(override_section) and section != "-":
376                     # Ignore this; it's a common mistake and not worth whining about
377                     if lower(section) == "non-us/main" and lower(override_section) == "non-us":
378                         continue;
379                     summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
380                 priority = files[file]["priority"];
381                 override_priority = files[file]["override priority"];
382                 if priority != override_priority and priority != "-":
383                     summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
384
385         if summary == "":
386             return;
387
388         Subst["__SUMMARY__"] = summary;
389         mail_message = utils.TemplateSubst(Subst,utils.open_file(self.Cnf["Dir::TemplatesDir"]+"/jennifer.override-disparity").read());
390         utils.send_mail (mail_message, "");
391
392     ###########################################################################
393
394     def force_move (self, files):
395         """Forcefully move files from the current directory to the reject
396            directory.  If any file already exists it will be moved to the
397            morgue to make way for the new file."""
398
399         Cnf = self.Cnf
400
401         for file in files:
402             # Skip any files which don't exist or which we don't have permission to copy.
403             if os.access(file,os.R_OK) == 0:
404                 continue;
405             dest_file = os.path.join(Cnf["Dir::QueueRejectDir"], file);
406             try:
407                 os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
408             except OSError, e:
409                 # File exists?  Let's try and move it to the morgue
410                 if errno.errorcode[e.errno] == 'EEXIST':
411                     morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueRejectDir"],file);
412                     try:
413                         morgue_file = utils.find_next_free(morgue_file);
414                     except utils.tried_too_hard_exc:
415                         # Something's either gone badly Pete Tong, or
416                         # someone is trying to exploit us.
417                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file));
418                         return;
419                     utils.move(dest_file, morgue_file);
420                     try:
421                         os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
422                     except OSError, e:
423                         # Likewise
424                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file));
425                         return;
426                 else:
427                     raise;
428             # If we got here, we own the destination file, so we can
429             # safely overwrite it.
430             utils.move(file, dest_file, 1);
431
432     ###########################################################################
433
434     def do_reject (self, manual = 0, reject_message = ""):
435         print "Rejecting.\n"
436
437         Cnf = self.Cnf;
438         Subst = self.Subst;
439         pkg = self.pkg;
440
441         reason_filename = pkg.changes_file[:-8] + ".reason";
442         reject_filename = Cnf["Dir::QueueRejectDir"] + '/' + reason_filename;
443
444         # Move all the files into the reject directory
445         reject_files = pkg.files.keys() + [pkg.changes_file];
446         self.force_move(reject_files);
447
448         # If we fail here someone is probably trying to exploit the race
449         # so let's just raise an exception ...
450         if os.path.exists(reject_filename):
451             os.unlink(reject_filename);
452         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
453
454         if not manual:
455             Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
456             Subst["__MANUAL_REJECT_MESSAGE__"] = "";
457             Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
458             os.write(fd, reject_message);
459             os.close(fd);
460         else:
461             # Build up the rejection email
462             user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"]);
463
464             Subst["__REJECTOR_ADDRESS__"] = user_email_address;
465             Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message;
466             Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
467             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
468
469             # Write the rejection email out as the <foo>.reason file
470             os.write(fd, reject_mail_message);
471             os.close(fd);
472
473             # If we weren't given a manual rejection message, spawn an
474             # editor so the user can add one in...
475             if reject_message == "":
476                 editor = os.environ.get("EDITOR","vi")
477                 result = os.system("%s +6 %s" % (editor, reject_filename))
478                 if result != 0:
479                     utils.fubar("editor invocation failed for '%s'!" % (reject_filename), result);
480
481         # Send the rejection mail if appropriate
482         if not Cnf["Dinstall::Options::No-Mail"]:
483             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
484             utils.send_mail (reject_mail_message, "");
485
486         self.Logger.log(["rejected", pkg.changes_file]);
487
488     ################################################################################
489
490     # Ensure that source exists somewhere in the archive for the binary
491     # upload being processed.
492     #
493     # (1) exact match                      => 1.0-3
494     # (2) Bin-only NMU of an MU            => 1.0-3.0.1
495     # (3) Bin-only NMU of a sourceful-NMU  => 1.0-3.1.1
496
497     def source_exists (self, package, source_version):
498         q = self.projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
499
500         # Reduce the query results to a list of version numbers
501         ql = map(lambda x: x[0], q.getresult());
502
503         # Try (1)
504         if ql.count(source_version):
505             return 1;
506
507         # Try (2)
508         orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
509         if ql.count(orig_source_version):
510             return 1;
511
512         # Try (3)
513         orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
514         if ql.count(orig_source_version):
515             return 1;
516
517         # No source found...
518         return 0;
519
520     ################################################################################
521
522     def in_override_p (self, package, component, suite, binary_type, file):
523         files = self.pkg.files;
524
525         if binary_type == "": # must be source
526             type = "dsc";
527         else:
528             type = binary_type;
529
530         # Override suite name; used for example with proposed-updates
531         if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
532             suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)];
533
534         # Avoid <undef> on unknown distributions
535         suite_id = db_access.get_suite_id(suite);
536         if suite_id == -1:
537             return None;
538         component_id = db_access.get_component_id(component);
539         type_id = db_access.get_override_type_id(type);
540
541         # FIXME: nasty non-US speficic hack
542         if lower(component[:7]) == "non-us/":
543             component = component[7:];
544
545         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"
546                            % (package, suite_id, component_id, type_id));
547         result = q.getresult();
548         # If checking for a source package fall back on the binary override type
549         if type == "dsc" and not result:
550             type_id = db_access.get_override_type_id("deb");
551             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"
552                                % (package, suite_id, component_id, type_id));
553             result = q.getresult();
554
555         # Remember the section and priority so we can check them later if appropriate
556         if result != []:
557             files[file]["override section"] = result[0][0];
558             files[file]["override priority"] = result[0][1];
559
560         return result;
561
562     ################################################################################
563
564     def reject (self, str, prefix="Rejected: "):
565         if str:
566             # Unlike other rejects we add new lines first to avoid trailing
567             # new lines when this message is passed back up to a caller.
568             if self.reject_message:
569                 self.reject_message = self.reject_message + "\n";
570             self.reject_message = self.reject_message + prefix + str;
571
572     def check_binaries_against_db(self, file, suite):
573         self.reject_message = "";
574         files = self.pkg.files;
575
576         # Find any old binary packages
577         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"
578                            % (files[file]["package"], suite, files[file]["architecture"]))
579         for oldfile in q.dictresult():
580             files[file]["oldfiles"][suite] = oldfile;
581             # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
582             if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
583                 self.reject("%s: old version (%s) >= new version (%s)." % (file, oldfile["version"], files[file]["version"]));
584         # Check for any existing copies of the file
585         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"]))
586         if q.getresult() != []:
587             self.reject("can not overwrite existing copy of '%s' already in the archive." % (file));
588
589         return self.reject_message;
590
591     ################################################################################
592
593     def check_source_against_db(self, file):
594         """Ensure source is newer than existing source in target suites."""
595         self.reject_message = "";
596         changes = self.pkg.changes;
597         dsc = self.pkg.dsc;
598
599         package = dsc.get("source");
600         new_version = dsc.get("version");
601         for suite in changes["distribution"].keys():
602             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"
603                                % (package, suite));
604             ql = map(lambda x: x[0], q.getresult());
605             for old_version in ql:
606                 if apt_pkg.VersionCompare(new_version, old_version) != 1:
607                     self.reject("%s: Old version `%s' >= new version `%s'." % (file, old_version, new_version));
608         return self.reject_message;
609
610     ################################################################################
611
612     def check_dsc_against_db(self, file):
613         self.reject_message = "";
614         files = self.pkg.files;
615         dsc_files = self.pkg.dsc_files;
616         legacy_source_untouchable = self.pkg.legacy_source_untouchable;
617         orig_tar_gz = None;
618         found = None;
619
620         # Try and find all files mentioned in the .dsc.  This has
621         # to work harder to cope with the multiple possible
622         # locations of an .orig.tar.gz.
623         for dsc_file in dsc_files.keys():
624             if files.has_key(dsc_file):
625                 actual_md5 = files[dsc_file]["md5sum"];
626                 actual_size = int(files[dsc_file]["size"]);
627                 found = "%s in incoming" % (dsc_file)
628                 # Check the file does not already exist in the archive
629                 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));
630
631                 # "It has not broken them.  It has fixed a
632                 # brokenness.  Your crappy hack exploited a bug in
633                 # the old dinstall.
634                 #
635                 # "(Come on!  I thought it was always obvious that
636                 # one just doesn't release different files with
637                 # the same name and version.)"
638                 #                        -- ajk@ on d-devel@l.d.o
639
640                 if q.getresult() != []:
641                     self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file));
642             elif dsc_file[-12:] == ".orig.tar.gz":
643                 # Check in the pool
644                 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));
645                 ql = q.getresult();
646
647                 if ql != []:
648                     # Unfortunately, we make get more than one
649                     # match here if, for example, the package was
650                     # in potato but had a -sa upload in woody.  So
651                     # we need to choose the right one.
652
653                     x = ql[0]; # default to something sane in case we don't match any or have only one
654
655                     if len(ql) > 1:
656                         for i in ql:
657                             old_file = i[0] + i[1];
658                             actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
659                             actual_size = os.stat(old_file)[stat.ST_SIZE];
660                             if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
661                                 x = i;
662                             else:
663                                 legacy_source_untouchable[i[3]] = "";
664
665                     old_file = x[0] + x[1];
666                     actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
667                     actual_size = os.stat(old_file)[stat.ST_SIZE];
668                     found = old_file;
669                     suite_type = x[2];
670                     dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
671                     # See install() in katie...
672                     self.pkg.orig_tar_id = x[3];
673                     if suite_type == "legacy" or suite_type == "legacy-mixed":
674                         self.pkg.orig_tar_location = "legacy";
675                     else:
676                         self.pkg.orig_tar_location = x[4];
677                 else:
678                     # Not there? Check the queue directories...
679
680                     in_unchecked = os.path.join(self.Cnf["Dir::QueueUncheckedDir"],dsc_file);
681                     # See process_it() in jennifer for explanation of this
682                     if os.path.exists(in_unchecked):
683                         return (self.reject_message, in_unchecked);
684                     else:
685                         for dir in [ "Accepted", "New", "Byhand" ]:
686                             in_otherdir = os.path.join(self.Cnf["Dir::Queue%sDir" % (dir)],dsc_file);
687                             if os.path.exists(in_otherdir):
688                                 actual_md5 = apt_pkg.md5sum(utils.open_file(in_otherdir));
689                                 actual_size = os.stat(in_otherdir)[stat.ST_SIZE];
690                                 found = in_otherdir;
691
692                     if not found:
693                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file));
694                         continue;
695             else:
696                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file));
697                 continue;
698             if actual_md5 != dsc_files[dsc_file]["md5sum"]:
699                 self.reject("md5sum for %s doesn't match %s." % (found, file));
700             if actual_size != int(dsc_files[dsc_file]["size"]):
701                 self.reject("size for %s doesn't match %s." % (found, file));
702
703         return (self.reject_message, orig_tar_gz);