5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org>
7 @copyright: 2008-2009 Mark Hymers <mhy@debian.org>
8 @copyright: 2009 Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009 Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
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.
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.
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
27 ################################################################################
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 # * mhy looks up the definition of "funny"
34 ################################################################################
38 import psycopg2.extras
41 from singleton import Singleton
42 from config import Config
44 ################################################################################
47 def __init__(self, hashfunc=None):
49 self.hashfunc = hashfunc
51 self.hashfunc = lambda x: x['value']
55 def SetValue(self, keys, value):
56 self.data[self.hashfunc(keys)] = value
58 def GetValue(self, keys):
59 return self.data.get(self.hashfunc(keys))
61 ################################################################################
63 class DBConn(Singleton):
67 def __init__(self, *args, **kwargs):
68 super(DBConn, self).__init__(*args, **kwargs)
70 def _startup(self, *args, **kwargs):
74 ## Connection functions
75 def __createconn(self):
77 connstr = "dbname=%s" % cnf["DB::Name"]
79 connstr += " host=%s" % cnf["DB::Host"]
80 if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
81 connstr += " port=%s" % cnf["DB::Port"]
83 self.db_con = psycopg2.connect(connstr)
88 except psycopg2.InterfaceError:
95 def __init_caches(self):
96 self.caches = {'suite': Cache(),
99 'override_type': Cache(),
100 'architecture': Cache(),
102 'component': Cache(),
103 'content_path_names': Cache(),
104 'content_file_names': Cache(),
105 'location': Cache(lambda x: '%s_%s_%s' % (x['location'], x['component'], x['location'])),
106 'maintainer': {}, # TODO
107 'keyring': {}, # TODO
108 'source': Cache(lambda x: '%s_%s_' % (x['source'], x['version'])),
109 'files': Cache(lambda x: '%s_%s_' % (x['filename'], x['location'])),
110 'maintainer': {}, # TODO
111 'fingerprint': {}, # TODO
114 'suite_version': Cache(lambda x: '%s_%s' % (x['source'], x['suite'])),
117 self.prepared_statements = {}
119 def prepare(self,name,statement):
120 if not self.prepared_statements.has_key(name):
123 self.prepared_statements[name] = statement
125 def clear_caches(self):
128 ## Functions to pass through to the database connector
130 return self.db_con.cursor()
133 return self.db_con.commit()
136 def __get_single_row(self, query, values):
137 c = self.db_con.cursor(cursor_factory=psycopg2.extras.DictCursor)
138 c.execute(query, values)
147 def __get_single_id(self, query, values, cachename=None):
148 # This is a bit of a hack but it's an internal function only
149 if cachename is not None:
150 res = self.caches[cachename].GetValue(values)
154 c = self.db_con.cursor()
155 c.execute(query, values)
160 res = c.fetchone()[0]
162 if cachename is not None:
163 self.caches[cachename].SetValue(values, res)
167 def __get_id(self, retfield, table, qfield, value):
168 query = "SELECT %s FROM %s WHERE %s = %%(value)s" % (retfield, table, qfield)
169 return self.__get_single_id(query, {'value': value}, cachename=table)
171 def get_suite_id(self, suite):
173 Returns database id for given C{suite}.
174 Results are kept in a cache during runtime to minimize database queries.
177 @param suite: The name of the suite
180 @return: the database id for the given suite
183 return int(self.__get_id('id', 'suite', 'suite_name', suite))
185 def get_section_id(self, section):
187 Returns database id for given C{section}.
188 Results are kept in a cache during runtime to minimize database queries.
190 @type section: string
191 @param section: The name of the section
194 @return: the database id for the given section
197 return self.__get_id('id', 'section', 'section', section)
199 def get_priority_id(self, priority):
201 Returns database id for given C{priority}.
202 Results are kept in a cache during runtime to minimize database queries.
204 @type priority: string
205 @param priority: The name of the priority
208 @return: the database id for the given priority
211 return self.__get_id('id', 'priority', 'priority', priority)
213 def get_override_type_id(self, override_type):
215 Returns database id for given override C{type}.
216 Results are kept in a cache during runtime to minimize database queries.
219 @param type: The name of the override type
222 @return: the database id for the given override type
225 return self.__get_id('id', 'override_type', 'type', override_type)
227 def get_architecture_id(self, architecture):
229 Returns database id for given C{architecture}.
230 Results are kept in a cache during runtime to minimize database queries.
232 @type architecture: string
233 @param architecture: The name of the override type
236 @return: the database id for the given architecture
239 return self.__get_id('id', 'architecture', 'arch_string', architecture)
241 def get_archive_id(self, archive):
243 returns database id for given c{archive}.
244 results are kept in a cache during runtime to minimize database queries.
246 @type archive: string
247 @param archive: the name of the override type
250 @return: the database id for the given archive
253 return self.__get_id('id', 'archive', 'lower(name)', archive)
255 def get_component_id(self, component):
257 Returns database id for given C{component}.
258 Results are kept in a cache during runtime to minimize database queries.
260 @type component: string
261 @param component: The name of the override type
264 @return: the database id for the given component
267 return self.__get_id('id', 'component', 'lower(name)', component)
269 def get_location_id(self, location, component, archive):
271 Returns database id for the location behind the given combination of
272 - B{location} - the path of the location, eg. I{/srv/ftp.debian.org/ftp/pool/}
273 - B{component} - the id of the component as returned by L{get_component_id}
274 - B{archive} - the id of the archive as returned by L{get_archive_id}
275 Results are kept in a cache during runtime to minimize database queries.
277 @type location: string
278 @param location: the path of the location
281 @param component: the id of the component
284 @param archive: the id of the archive
287 @return: the database id for the location
291 archive_id = self.get_archive_id(archive)
299 component_id = self.get_component_id(component)
301 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND component=%(component)s AND archive=%(archive)s",
302 {'location': location,
303 'archive': int(archive_id),
304 'component': component_id}, cachename='location')
306 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND archive=%(archive)d",
307 {'location': location, 'archive': archive_id, 'component': ''}, cachename='location')
311 def get_source_id(self, source, version):
313 Returns database id for the combination of C{source} and C{version}
314 - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
316 Results are kept in a cache during runtime to minimize database queries.
319 @param source: source package name
321 @type version: string
322 @param version: the source version
325 @return: the database id for the source
328 return self.__get_single_id("SELECT id FROM source s WHERE s.source=%(source)s AND s.version=%(version)s",
329 {'source': source, 'version': version}, cachename='source')
331 def get_suite(self, suite):
332 if isinstance(suite, str):
333 suite_id = self.get_suite_id(suite.lower())
334 elif type(suite) == int:
339 return self.__get_single_row("SELECT * FROM suite WHERE id = %(id)s",
342 def get_suite_version(self, source, suite):
344 Returns database id for a combination of C{source} and C{suite}.
346 - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
347 - B{suite} - a suite name, eg. I{unstable}
349 Results are kept in a cache during runtime to minimize database queries.
352 @param source: source package name
355 @param suite: the suite name
358 @return: the version for I{source} in I{suite}
361 return self.__get_single_id("""
362 SELECT s.version FROM source s, suite su, src_associations sa
365 AND su.suite_name=%(suite)s
366 AND s.source=%(source)""", {'suite': suite, 'source': source}, cachename='suite_version')
369 def get_files_id (self, filename, size, md5sum, location_id):
371 Returns -1, -2 or the file_id for filename, if its C{size} and C{md5sum} match an
374 The database is queried using the C{filename} and C{location_id}. If a file does exist
375 at that location, the existing size and md5sum are checked against the provided
376 parameters. A size or checksum mismatch returns -2. If more than one entry is
377 found within the database, a -1 is returned, no result returns None, otherwise
380 Results are kept in a cache during runtime to minimize database queries.
382 @type filename: string
383 @param filename: the filename of the file to check against the DB
386 @param size: the size of the file to check against the DB
389 @param md5sum: the md5sum of the file to check against the DB
391 @type location_id: int
392 @param location_id: the id of the location as returned by L{get_location_id}
395 @return: Various return values are possible:
396 - -2: size/checksum error
397 - -1: more than one file found in database
398 - None: no file found in database
402 values = {'filename' : filename,
403 'location' : location_id}
405 res = self.caches['files'].GetValue( values )
408 query = """SELECT id, size, md5sum
410 WHERE filename = %(filename)s AND location = %(location)s"""
412 cursor = self.db_con.cursor()
413 cursor.execute( query, values )
415 if cursor.rowcount == 0:
418 elif cursor.rowcount != 1:
422 row = cursor.fetchone()
424 if row[1] != size or row[2] != md5sum:
428 self.caches[cachename].SetValue(values, row[0])
434 def get_or_set_contents_file_id(self, filename):
436 Returns database id for given filename.
438 Results are kept in a cache during runtime to minimize database queries.
439 If no matching file is found, a row is inserted.
441 @type filename: string
442 @param filename: The filename
445 @return: the database id for the given component
448 values={'value': filename}
449 query = "SELECT id FROM content_file_names WHERE file = %(value)s"
450 id = self.__get_single_id(query, values, cachename='content_file_names')
452 c = self.db_con.cursor()
453 c.execute( "INSERT INTO content_file_names VALUES (DEFAULT, %(value)s) RETURNING id",
457 self.caches['content_file_names'].SetValue(values, id)
461 traceback.print_exc()
464 def get_or_set_contents_path_id(self, path):
466 Returns database id for given path.
468 Results are kept in a cache during runtime to minimize database queries.
469 If no matching file is found, a row is inserted.
472 @param path: The filename
475 @return: the database id for the given component
478 values={'value': path}
479 query = "SELECT id FROM content_file_paths WHERE path = %(value)s"
480 id = self.__get_single_id(query, values, cachename='content_path_names')
482 c = self.db_con.cursor()
483 c.execute( "INSERT INTO content_file_paths VALUES (DEFAULT, %(value)s) RETURNING id",
487 self.caches['content_path_names'].SetValue(values, id)
491 traceback.print_exc()
494 def get_suite_architectures(self, suite):
496 Returns list of architectures for C{suite}.
498 @type suite: string, int
499 @param suite: the suite name or the suite_id
502 @return: the list of architectures for I{suite}
506 if type(suite) == str:
507 suite_id = self.get_suite_id(suite)
508 elif type(suite) == int:
513 c = self.db_con.cursor()
514 c.execute( """SELECT a.arch_string FROM suite_architectures sa
515 JOIN architecture a ON (a.id = sa.architecture)
516 WHERE suite='%s'""" % suite_id )
518 return map(lambda x: x[0], c.fetchall())
520 def insert_content_paths(self, bin_id, fullpaths):
522 Make sure given path is associated with given binary id
525 @param bin_id: the id of the binary
526 @type fullpath: string
527 @param fullpath: the path of the file being associated with the binary
529 @return True upon success
532 c = self.db_con.cursor()
534 c.execute("BEGIN WORK")
537 for fullpath in fullpaths:
538 (path, file) = os.path.split(fullpath)
540 # Get the necessary IDs ...
541 file_id = self.get_or_set_contents_file_id(file)
542 path_id = self.get_or_set_contents_path_id(path)
544 c.execute("""INSERT INTO content_associations
545 (binary_pkg, filepath, filename)
546 VALUES ( '%d', '%d', '%d')""" % (bin_id, path_id, file_id) )
551 traceback.print_exc()
552 c.execute("ROLLBACK")
555 def insert_pending_content_paths(self, package, fullpaths):
557 Make sure given paths are temporarily associated with given
561 @param package: the package to associate with should have been read in from the binary control file
562 @type fullpaths: list
563 @param fullpaths: the list of paths of the file being associated with the binary
565 @return True upon success
568 c = self.db_con.cursor()
570 c.execute("BEGIN WORK")
573 # Remove any already existing recorded files for this package
574 c.execute("""DELETE FROM pending_content_associations
575 WHERE package=%(Package)s
576 AND version=%(Version)s""", package )
578 for fullpath in fullpaths:
579 (path, file) = os.path.split(fullpath)
581 if path.startswith( "./" ):
583 # Get the necessary IDs ...
584 file_id = self.get_or_set_contents_file_id(file)
585 path_id = self.get_or_set_contents_path_id(path)
587 c.execute("""INSERT INTO pending_content_associations
588 (package, version, filepath, filename)
589 VALUES (%%(Package)s, %%(Version)s, '%d', '%d')""" % (path_id, file_id),
594 traceback.print_exc()
595 c.execute("ROLLBACK")
598 ################################################################################
601 # This should be kept in sync with the suites table;
602 # we should probably just do introspection on the table
603 # (or maybe use an ORM)
604 _fieldnames = ['announce', 'changelogbase', 'codename', 'commentsdir',
605 'copychanges', 'copydotdak', 'description', 'id',
606 'label', 'notautomatic', 'origin', 'overridecodename',
607 'overridesuite', 'policy_engine', 'priority', 'suite_name',
608 'untouchable', 'validtime', 'version']
610 def __init_fields(self):
611 for k in self._fieldnames:
612 setattr(self, k, None)
614 def __init__(self, suite):
616 if suite is not None:
618 suite_data = db_conn.get_suite(suite)
620 if suite_data is not None:
621 for k in suite_data.keys():
622 setattr(self, k, suite_data[k])