]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Contents generation should be working now
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
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
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 # < 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"
33
34 ################################################################################
35
36 import os
37 import psycopg2
38 import traceback
39
40 from singleton import Singleton
41 from config import Config
42
43 ################################################################################
44
45 class Cache(object):
46     def __init__(self, hashfunc=None):
47         if hashfunc:
48             self.hashfunc = hashfunc
49         else:
50             self.hashfunc = lambda x: x['value']
51
52         self.data = {}
53
54     def SetValue(self, keys, value):
55         self.data[self.hashfunc(keys)] = value
56
57     def GetValue(self, keys):
58         return self.data.get(self.hashfunc(keys))
59
60 ################################################################################
61
62 class DBConn(Singleton):
63     """
64     database module init.
65     """
66     def __init__(self, *args, **kwargs):
67         super(DBConn, self).__init__(*args, **kwargs)
68
69     def _startup(self, *args, **kwargs):
70         self.__createconn()
71         self.__init_caches()
72
73     ## Connection functions
74     def __createconn(self):
75         cnf = Config()
76         connstr = "dbname=%s" % cnf["DB::Name"]
77         if cnf["DB::Host"]:
78            connstr += " host=%s" % cnf["DB::Host"]
79         if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
80            connstr += " port=%s" % cnf["DB::Port"]
81
82         self.db_con = psycopg2.connect(connstr)
83
84     def reconnect(self):
85         try:
86             self.db_con.close()
87         except psycopg2.InterfaceError:
88             pass
89
90         self.db_con = None
91         self.__createconn()
92
93     ## Cache functions
94     def __init_caches(self):
95         self.caches = {'suite':         Cache(),
96                        'section':       Cache(),
97                        'priority':      Cache(),
98                        'override_type': Cache(),
99                        'architecture':  Cache(),
100                        'archive':       Cache(),
101                        'component':     Cache(),
102                        'content_path_names':     Cache(),
103                        'content_file_names':     Cache(),
104                        'location':      Cache(lambda x: '%s_%s_%s' % (x['location'], x['component'], x['location'])),
105                        'maintainer':    {}, # TODO
106                        'keyring':       {}, # TODO
107                        'source':        Cache(lambda x: '%s_%s_' % (x['source'], x['version'])),
108                        'files':         Cache(lambda x: '%s_%s_' % (x['filename'], x['location'])),
109                        'maintainer':    {}, # TODO
110                        'fingerprint':   {}, # TODO
111                        'queue':         {}, # TODO
112                        'uid':           {}, # TODO
113                        'suite_version': Cache(lambda x: '%s_%s' % (x['source'], x['suite'])),
114                       }
115
116     def clear_caches(self):
117         self.__init_caches()
118
119     ## Functions to pass through to the database connector
120     def cursor(self):
121         return self.db_con.cursor()
122
123     def commit(self):
124         return self.db_con.commit()
125
126     ## Get functions
127     def __get_single_id(self, query, values, cachename=None):
128         # This is a bit of a hack but it's an internal function only
129         if cachename is not None:
130             res = self.caches[cachename].GetValue(values)
131             if res:
132                 return res
133
134         c = self.db_con.cursor()
135         c.execute(query, values)
136
137         if c.rowcount != 1:
138             return None
139
140         res = c.fetchone()[0]
141
142         if cachename is not None:
143             self.caches[cachename].SetValue(values, res)
144
145         return res
146
147     def __get_id(self, retfield, table, qfield, value):
148         query = "SELECT %s FROM %s WHERE %s = %%(value)s" % (retfield, table, qfield)
149         return self.__get_single_id(query, {'value': value}, cachename=table)
150
151     def get_suite_id(self, suite):
152         """
153         Returns database id for given C{suite}.
154         Results are kept in a cache during runtime to minimize database queries.
155
156         @type suite: string
157         @param suite: The name of the suite
158
159         @rtype: int
160         @return: the database id for the given suite
161
162         """
163         return self.__get_id('id', 'suite', 'suite_name', suite)
164
165     def get_section_id(self, section):
166         """
167         Returns database id for given C{section}.
168         Results are kept in a cache during runtime to minimize database queries.
169
170         @type section: string
171         @param section: The name of the section
172
173         @rtype: int
174         @return: the database id for the given section
175
176         """
177         return self.__get_id('id', 'section', 'section', section)
178
179     def get_priority_id(self, priority):
180         """
181         Returns database id for given C{priority}.
182         Results are kept in a cache during runtime to minimize database queries.
183
184         @type priority: string
185         @param priority: The name of the priority
186
187         @rtype: int
188         @return: the database id for the given priority
189
190         """
191         return self.__get_id('id', 'priority', 'priority', priority)
192
193     def get_override_type_id(self, override_type):
194         """
195         Returns database id for given override C{type}.
196         Results are kept in a cache during runtime to minimize database queries.
197
198         @type type: string
199         @param type: The name of the override type
200
201         @rtype: int
202         @return: the database id for the given override type
203
204         """
205         return self.__get_id('id', 'override_type', 'override_type', override_type)
206
207     def get_architecture_id(self, architecture):
208         """
209         Returns database id for given C{architecture}.
210         Results are kept in a cache during runtime to minimize database queries.
211
212         @type architecture: string
213         @param architecture: The name of the override type
214
215         @rtype: int
216         @return: the database id for the given architecture
217
218         """
219         return self.__get_id('id', 'architecture', 'arch_string', architecture)
220
221     def get_archive_id(self, archive):
222         """
223         returns database id for given c{archive}.
224         results are kept in a cache during runtime to minimize database queries.
225
226         @type archive: string
227         @param archive: the name of the override type
228
229         @rtype: int
230         @return: the database id for the given archive
231
232         """
233         return self.__get_id('id', 'archive', 'lower(name)', archive)
234
235     def get_component_id(self, component):
236         """
237         Returns database id for given C{component}.
238         Results are kept in a cache during runtime to minimize database queries.
239
240         @type component: string
241         @param component: The name of the override type
242
243         @rtype: int
244         @return: the database id for the given component
245
246         """
247         return self.__get_id('id', 'component', 'lower(name)', component)
248
249     def get_location_id(self, location, component, archive):
250         """
251         Returns database id for the location behind the given combination of
252           - B{location} - the path of the location, eg. I{/srv/ftp.debian.org/ftp/pool/}
253           - B{component} - the id of the component as returned by L{get_component_id}
254           - B{archive} - the id of the archive as returned by L{get_archive_id}
255         Results are kept in a cache during runtime to minimize database queries.
256
257         @type location: string
258         @param location: the path of the location
259
260         @type component: int
261         @param component: the id of the component
262
263         @type archive: int
264         @param archive: the id of the archive
265
266         @rtype: int
267         @return: the database id for the location
268
269         """
270
271         archive_id = self.get_archive_id(archive)
272
273         if not archive_id:
274             return None
275
276         res = None
277
278         if component:
279             component_id = self.get_component_id(component)
280             if component_id:
281                 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND component=%(component)s AND archive=%(archive)s",
282                         {'location': location,
283                          'archive': int(archive_id),
284                          'component': component_id}, cachename='location')
285         else:
286             res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND archive=%(archive)d",
287                     {'location': location, 'archive': archive_id, 'component': ''}, cachename='location')
288
289         return res
290
291     def get_source_id(self, source, version):
292         """
293         Returns database id for the combination of C{source} and C{version}
294           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
295           - B{version}
296         Results are kept in a cache during runtime to minimize database queries.
297
298         @type source: string
299         @param source: source package name
300
301         @type version: string
302         @param version: the source version
303
304         @rtype: int
305         @return: the database id for the source
306
307         """
308         return self.__get_single_id("SELECT id FROM source s WHERE s.source=%(source)s AND s.version=%(version)s",
309                                  {'source': source, 'version': version}, cachename='source')
310
311     def get_suite_version(self, source, suite):
312         """
313         Returns database id for a combination of C{source} and C{suite}.
314
315           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
316           - B{suite} - a suite name, eg. I{unstable}
317
318         Results are kept in a cache during runtime to minimize database queries.
319
320         @type source: string
321         @param source: source package name
322
323         @type suite: string
324         @param suite: the suite name
325
326         @rtype: string
327         @return: the version for I{source} in I{suite}
328
329         """
330         return self.__get_single_id("""
331         SELECT s.version FROM source s, suite su, src_associations sa
332         WHERE sa.source=s.id
333           AND sa.suite=su.id
334           AND su.suite_name=%(suite)s
335           AND s.source=%(source)""", {'suite': suite, 'source': source}, cachename='suite_version')
336
337
338     def get_files_id (self, filename, size, md5sum, location_id):
339         """
340         Returns -1, -2 or the file_id for filename, if its C{size} and C{md5sum} match an
341         existing copy.
342
343         The database is queried using the C{filename} and C{location_id}. If a file does exist
344         at that location, the existing size and md5sum are checked against the provided
345         parameters. A size or checksum mismatch returns -2. If more than one entry is
346         found within the database, a -1 is returned, no result returns None, otherwise
347         the file id.
348
349         Results are kept in a cache during runtime to minimize database queries.
350
351         @type filename: string
352         @param filename: the filename of the file to check against the DB
353
354         @type size: int
355         @param size: the size of the file to check against the DB
356
357         @type md5sum: string
358         @param md5sum: the md5sum of the file to check against the DB
359
360         @type location_id: int
361         @param location_id: the id of the location as returned by L{get_location_id}
362
363         @rtype: int / None
364         @return: Various return values are possible:
365                    - -2: size/checksum error
366                    - -1: more than one file found in database
367                    - None: no file found in database
368                    - int: file id
369
370         """
371         values = {'filename' : filename,
372                   'location' : location_id}
373
374         res = self.caches['files'].GetValue( values )
375
376         if not res:
377             query = """SELECT id, size, md5sum
378                        FROM files
379                        WHERE filename = %(filename)s AND location = %(location)s"""
380
381             cursor = self.db_con.cursor()
382             cursor.execute( query, values )
383
384             if cursor.rowcount == 0:
385                 res = None
386
387             elif cursor.rowcount != 1:
388                 res = -1
389
390             else:
391                 row = cursor.fetchone()
392
393                 if row[1] != size or row[2] != md5sum:
394                     res =  -2
395
396                 else:
397                     self.caches[cachename].SetValue(values, row[0])
398                     res = row[0]
399
400         return res
401
402
403     def get_or_set_contents_file_id(self, filename):
404         """
405         Returns database id for given filename.
406
407         Results are kept in a cache during runtime to minimize database queries.
408         If no matching file is found, a row is inserted.
409
410         @type filename: string
411         @param filename: The filename
412
413         @rtype: int
414         @return: the database id for the given component
415         """
416         values={'value': filename}
417         query = "SELECT id FROM content_file_names WHERE file = %(value)s"
418         id = self.__get_single_id(query, values, cachename='content_file_names')
419         if not id:
420             c = self.db_con.cursor()
421             c.execute( "INSERT INTO content_file_names VALUES (DEFAULT, %(value)s) RETURNING id",
422                        values )
423
424             id = c.fetchone()[0]
425             self.caches['content_file_names'].SetValue(values, id)
426
427         return id
428
429     def get_or_set_contents_path_id(self, path):
430         """
431         Returns database id for given path.
432
433         Results are kept in a cache during runtime to minimize database queries.
434         If no matching file is found, a row is inserted.
435
436         @type path: string
437         @param path: The filename
438
439         @rtype: int
440         @return: the database id for the given component
441         """
442         values={'value': path}
443         query = "SELECT id FROM content_file_paths WHERE path = %(value)s"
444         id = self.__get_single_id(query, values, cachename='content_path_names')
445         if not id:
446             c = self.db_con.cursor()
447             c.execute( "INSERT INTO content_file_paths VALUES (DEFAULT, %(value)s) RETURNING id",
448                        values )
449
450             id = c.fetchone()[0]
451             self.caches['content_path_names'].SetValue(values, id)
452
453         return id
454
455     def insert_content_paths(self, package, fullpaths):
456         """
457         Make sure given path is associated with given binary id
458
459         @type bin_id: int
460         @param bin_id: the id of the binary
461         @type fullpath: string
462         @param fullpath: the path of the file being associated with the binary
463
464         @return True upon success
465         """
466
467         c = self.db_con.cursor()
468
469         c.execute("BEGIN WORK")
470         try:
471
472                 # Remove any already existing recorded files for this package
473             c.execute("""DELETE FROM temp_content_associations
474                          WHERE package=%(Package)s
475                          AND version=%(Version)s""", package )
476
477             for fullpath in fullpaths:
478                 (path, file) = os.path.split(fullpath)
479
480                 # Get the necessary IDs ...
481                 file_id = self.get_or_set_contents_file_id(file)
482                 path_id = self.get_or_set_contents_path_id(path)
483
484                 c.execute("""INSERT INTO temp_content_associations
485                                (package, version, filepath, filename)
486                            VALUES (%%(Package)s, %%(Version)s, '%d', '%d')""" % (path_id, file_id),
487                           package )
488             c.execute("COMMIT")
489             return True
490         except:
491             traceback.print_exc()
492             c.execute("ROLLBACK")
493             return False