root / trunk / twisted / news / database.py

Revision 25457, 30.9 kB (checked in by exarkun, 8 months ago)

Merge hashlib-2763-3

Author: wsanchez, exarkun
Reviewer: exarkun, mwhudson
Fixes: #2763

Replace uses of md5 and sha modules in Twisted with use of a new twisted.python.hashlib
module which transparently uses the new hashlib standard library module if it is available
or falls back to md5 and sha if not.

Line 
1 # -*- test-case-name: twisted.news.test.test_news -*-
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 News server backend implementations
8
9 Maintainer: Jp Calderone
10
11 Future Plans: A PyFramer-based backend and a new backend interface that is
12 less NNTP specific
13 """
14
15
16 import getpass, pickle, time, socket
17 import os
18 import StringIO
19
20 from zope.interface import implements, Interface
21
22 from twisted.news.nntp import NNTPError
23 from twisted.mail import smtp
24 from twisted.internet import defer
25 from twisted.enterprise import adbapi
26 from twisted.persisted import dirdbm
27 from twisted.python.hashlib import md5
28
29
30
31 ERR_NOGROUP, ERR_NOARTICLE = range(2, 4)  # XXX - put NNTP values here (I guess?)
32
33 OVERVIEW_FMT = [
34     'Subject', 'From', 'Date', 'Message-ID', 'References',
35     'Bytes', 'Lines', 'Xref'
36 ]
37
38 def hexdigest(md5): #XXX: argh. 1.5.2 doesn't have this.
39     return ''.join(map(lambda x: hex(ord(x))[2:], md5.digest()))
40
41 class Article:
42     def __init__(self, head, body):
43         self.body = body
44         self.headers = {}
45         header = None
46         for line in head.split('\r\n'):
47             if line[0] in ' \t':
48                 i = list(self.headers[header])
49                 i[1] += '\r\n' + line
50             else:
51                 i = line.split(': ', 1)
52                 header = i[0].lower()
53             self.headers[header] = tuple(i)
54
55         if not self.getHeader('Message-ID'):
56             s = str(time.time()) + self.body
57             id = hexdigest(md5(s)) + '@' + socket.gethostname()
58             self.putHeader('Message-ID', '<%s>' % id)
59
60         if not self.getHeader('Bytes'):
61             self.putHeader('Bytes', str(len(self.body)))
62
63         if not self.getHeader('Lines'):
64             self.putHeader('Lines', str(self.body.count('\n')))
65
66         if not self.getHeader('Date'):
67             self.putHeader('Date', time.ctime(time.time()))
68
69
70     def getHeader(self, header):
71         h = header.lower()
72         if self.headers.has_key(h):
73             return self.headers[h][1]
74         else:
75             return ''
76
77
78     def putHeader(self, header, value):
79         self.headers[header.lower()] = (header, value)
80
81
82     def textHeaders(self):
83         headers = []
84         for i in self.headers.values():
85             headers.append('%s: %s' % i)
86         return '\r\n'.join(headers) + '\r\n'
87
88     def overview(self):
89         xover = []
90         for i in OVERVIEW_FMT:
91             xover.append(self.getHeader(i))
92         return xover
93
94
95 class NewsServerError(Exception):
96     pass
97
98
99 class INewsStorage(Interface):
100     """
101     An interface for storing and requesting news articles
102     """
103
104     def listRequest():
105         """
106         Returns a deferred whose callback will be passed a list of 4-tuples
107         containing (name, max index, min index, flags) for each news group
108         """
109
110
111     def subscriptionRequest():
112         """
113         Returns a deferred whose callback will be passed the list of
114         recommended subscription groups for new server users
115         """
116
117
118     def postRequest(message):
119         """
120         Returns a deferred whose callback will be invoked if 'message'
121         is successfully posted to one or more specified groups and
122         whose errback will be invoked otherwise.
123         """
124
125
126     def overviewRequest():
127         """
128         Returns a deferred whose callback will be passed the a list of
129         headers describing this server's overview format.
130         """
131
132
133     def xoverRequest(group, low, high):
134         """
135         Returns a deferred whose callback will be passed a list of xover
136         headers for the given group over the given range.  If low is None,
137         the range starts at the first article.  If high is None, the range
138         ends at the last article.
139         """
140
141
142     def xhdrRequest(group, low, high, header):
143         """
144         Returns a deferred whose callback will be passed a list of XHDR data
145         for the given group over the given range.  If low is None,
146         the range starts at the first article.  If high is None, the range
147         ends at the last article.
148         """
149
150
151     def listGroupRequest(group):
152         """
153         Returns a deferred whose callback will be passed a two-tuple of
154         (group name, [article indices])
155         """
156
157
158     def groupRequest(group):
159         """
160         Returns a deferred whose callback will be passed a five-tuple of
161         (group name, article count, highest index, lowest index, group flags)
162         """
163
164
165     def articleExistsRequest(id):
166         """
167         Returns a deferred whose callback will be passed with a true value
168         if a message with the specified Message-ID exists in the database
169         and with a false value otherwise.
170         """
171
172
173     def articleRequest(group, index, id = None):
174         """
175         Returns a deferred whose callback will be passed a file-like object
176         containing the full article text (headers and body) for the article
177         of the specified index in the specified group, and whose errback
178         will be invoked if the article or group does not exist.  If id is
179         not None, index is ignored and the article with the given Message-ID
180         will be returned instead, along with its index in the specified
181         group.
182         """
183
184
185     def headRequest(group, index):
186         """
187         Returns a deferred whose callback will be passed the header for
188         the article of the specified index in the specified group, and
189         whose errback will be invoked if the article or group does not
190         exist.
191         """
192
193
194     def bodyRequest(group, index):
195         """
196         Returns a deferred whose callback will be passed the body for
197         the article of the specified index in the specified group, and
198         whose errback will be invoked if the article or group does not
199         exist.
200         """
201
202 class NewsStorage:
203     """
204     Backwards compatibility class -- There is no reason to inherit from this,
205     just implement INewsStorage instead.
206     """
207     def listRequest(self):
208         raise NotImplementedError()
209     def subscriptionRequest(self):
210         raise NotImplementedError()
211     def postRequest(self, message):
212         raise NotImplementedError()
213     def overviewRequest(self):
214         return defer.succeed(OVERVIEW_FMT)
215     def xoverRequest(self, group, low, high):
216         raise NotImplementedError()
217     def xhdrRequest(self, group, low, high, header):
218         raise NotImplementedError()
219     def listGroupRequest(self, group):
220         raise NotImplementedError()
221     def groupRequest(self, group):
222         raise NotImplementedError()
223     def articleExistsRequest(self, id):
224         raise NotImplementedError()
225     def articleRequest(self, group, index, id = None):
226         raise NotImplementedError()
227     def headRequest(self, group, index):
228         raise NotImplementedError()
229     def bodyRequest(self, group, index):
230         raise NotImplementedError()
231
232
233 class PickleStorage:
234     """A trivial NewsStorage implementation using pickles
235
236     Contains numerous flaws and is generally unsuitable for any
237     real applications.  Consider yourself warned!
238     """
239
240     implements(INewsStorage)
241
242     sharedDBs = {}
243
244     def __init__(self, filename, groups = None, moderators = ()):
245         self.datafile = filename
246         self.load(filename, groups, moderators)
247
248
249     def getModerators(self, groups):
250         # first see if any groups are moderated.  if so, nothing gets posted,
251         # but the whole messages gets forwarded to the moderator address
252         moderators = []
253         for group in groups:
254             moderators.append(self.db['moderators'].get(group, None))
255         return filter(None, moderators)
256
257
258     def notifyModerators(self, moderators, article):
259         # Moderated postings go through as long as they have an Approved
260         # header, regardless of what the value is
261         article.putHeader('To', ', '.join(moderators))
262         return smtp.sendEmail(
263             'twisted@' + socket.gethostname(),
264             moderators,
265             article.body,
266             dict(article.headers.values())
267         )
268
269
270     def listRequest(self):
271         "Returns a list of 4-tuples: (name, max index, min index, flags)"
272         l = self.db['groups']
273         r = []
274         for i in l:
275             if len(self.db[i].keys()):
276                 low = min(self.db[i].keys())
277                 high = max(self.db[i].keys()) + 1
278             else:
279                 low = high = 0
280             if self.db['moderators'].has_key(i):
281                 flags = 'm'
282             else:
283                 flags = 'y'
284             r.append((i, high, low, flags))
285         return defer.succeed(r)
286
287     def subscriptionRequest(self):
288         return defer.succeed(['alt.test'])
289
290     def postRequest(self, message):
291         cleave = message.find('\r\n\r\n')
292         headers, article = message[:cleave], message[cleave + 4:]
293
294         a = Article(headers, article)
295         groups = a.getHeader('Newsgroups').split()
296         xref = []
297
298         # Check moderated status
299         moderators = self.getModerators(groups)
300         if moderators and not a.getHeader('Approved'):
301             return self.notifyModerators(moderators, a)
302
303         for group in groups:
304             if self.db.has_key(group):
305                 if len(self.db[group].keys()):
306                     index = max(self.db[group].keys()) + 1
307                 else:
308                     index = 1
309                 xref.append((group, str(index)))
310                 self.db[group][index] = a
311
312         if len(xref) == 0:
313             return defer.fail(None)
314
315         a.putHeader('Xref', '%s %s' % (
316             socket.gethostname().split()[0],
317             ''.join(map(lambda x: ':'.join(x), xref))
318         ))
319
320         self.flush()
321         return defer.succeed(None)
322
323
324     def overviewRequest(self):
325         return defer.succeed(OVERVIEW_FMT)
326
327
328     def xoverRequest(self, group, low, high):
329         if not self.db.has_key(group):
330             return defer.succeed([])
331         r = []
332         for i in self.db[group].keys():
333             if (low is None or i >= low) and (high is None or i <= high):
334                 r.append([str(i)] + self.db[group][i].overview())
335         return defer.succeed(r)
336
337
338     def xhdrRequest(self, group, low, high, header):
339         if not self.db.has_key(group):
340             return defer.succeed([])
341         r = []
342         for i in self.db[group].keys():
343             if low is None or i >= low and high is None or i <= high:
344                 r.append((i, self.db[group][i].getHeader(header)))
345         return defer.succeed(r)
346
347
348     def listGroupRequest(self, group):
349         if self.db.has_key(group):
350             return defer.succeed((group, self.db[group].keys()))
351         else:
352             return defer.fail(None)
353
354     def groupRequest(self, group):
355         if self.db.has_key(group):
356             if len(self.db[group].keys()):
357                 num = len(self.db[group].keys())
358                 low = min(self.db[group].keys())
359                 high = max(self.db[group].keys())
360             else:
361                 num = low = high = 0
362             flags = 'y'
363             return defer.succeed((group, num, high, low, flags))
364         else:
365             return defer.fail(ERR_NOGROUP)
366
367
368     def articleExistsRequest(self, id):
369         for g in self.db.values():
370             for a in g.values():
371                 if a.getHeader('Message-ID') == id:
372                     return defer.succeed(1)
373         return defer.succeed(0)
374
375
376     def articleRequest(self, group, index, id = None):
377         if id is not None:
378             raise NotImplementedError
379
380         if self.db.has_key(group):
381             if self.db[group].has_key(index):
382                 a = self.db[group][index]
383                 return defer.succeed((
384                     index,
385                     a.getHeader('Message-ID'),
386                     StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
387                 ))
388             else:
389                 return defer.fail(ERR_NOARTICLE)
390         else:
391             return defer.fail(ERR_NOGROUP)
392
393
394     def headRequest(self, group, index):
395         if self.db.has_key(group):
396             if self.db[group].has_key(index):
397                 a = self.db[group][index]
398                 return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
399             else:
400                 return defer.fail(ERR_NOARTICLE)
401         else:
402             return defer.fail(ERR_NOGROUP)
403
404
405     def bodyRequest(self, group, index):
406         if self.db.has_key(group):
407             if self.db[group].has_key(index):
408                 a = self.db[group][index]
409                 return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
410             else:
411                 return defer.fail(ERR_NOARTICLE)
412         else:
413             return defer.fail(ERR_NOGROUP)
414
415
416     def flush(self):
417         f = open(self.datafile, 'w')
418         pickle.dump(self.db, f)
419         f.close()
420
421
422     def load(self, filename, groups = None, moderators = ()):
423         if PickleStorage.sharedDBs.has_key(filename):
424             self.db = PickleStorage.sharedDBs[filename]
425         else:
426             try:
427                 self.db = pickle.load(open(filename))
428                 PickleStorage.sharedDBs[filename] = self.db
429             except IOError, e:
430                 self.db = PickleStorage.sharedDBs[filename] = {}
431                 self.db['groups'] = groups
432                 if groups is not None:
433                     for i in groups:
434                         self.db[i] = {}
435                 self.db['moderators'] = dict(moderators)
436                 self.flush()
437
438
439 class Group:
440     name = None
441     flags = ''
442     minArticle = 1
443     maxArticle = 0
444     articles = None
445
446     def __init__(self, name, flags = 'y'):
447         self.name = name
448         self.flags = flags
449         self.articles = {}
450
451
452 class NewsShelf:
453     """
454     A NewStorage implementation using Twisted's dirdbm persistence module.
455     """
456
457     implements(INewsStorage)
458
459     def __init__(self, mailhost, path):
460         self.path = path
461         self.mailhost = mailhost
462
463         if not os.path.exists(path):
464             os.mkdir(path)
465
466         self.dbm = dirdbm.Shelf(os.path.join(path, "newsshelf"))
467         if not len(self.dbm.keys()):
468             self.initialize()
469
470
471     def initialize(self):
472         # A dictionary of group name/Group instance items
473         self.dbm['groups'] = dirdbm.Shelf(os.path.join(self.path, 'groups'))
474
475         # A dictionary of group name/email address
476         self.dbm['moderators'] = dirdbm.Shelf(os.path.join(self.path, 'moderators'))
477
478         # A list of group names
479         self.dbm['subscriptions'] = []
480
481         # A dictionary of MessageID strings/xref lists
482         self.dbm['Message-IDs'] = dirdbm.Shelf(os.path.join(self.path, 'Message-IDs'))
483
484
485     def addGroup(self, name, flags):
486         self.dbm['groups'][name] = Group(name, flags)
487
488
489     def addSubscription(self, name):
490         self.dbm['subscriptions'] = self.dbm['subscriptions'] + [name]
491
492
493     def addModerator(self, group, email):
494         self.dbm['moderators'][group] = email
495
496
497     def listRequest(self):
498         result = []
499         for g in self.dbm['groups'].values():
500             result.append((g.name, g.maxArticle, g.minArticle, g.flags))
501         return defer.succeed(result)
502
503
504     def subscriptionRequest(self):
505         return defer.succeed(self.dbm['subscriptions'])
506
507
508     def getModerator(self, groups):
509         # first see if any groups are moderated.  if so, nothing gets posted,
510         # but the whole messages gets forwarded to the moderator address
511         for group in groups:
512             try:
513                 return self.dbm['moderators'][group]
514             except KeyError:
515                 pass
516         return None
517
518
519     def notifyModerator(self, moderator, article):
520         # Moderated postings go through as long as they have an Approved
521         # header, regardless of what the value is
522         print 'To is ', moderator
523         article.putHeader('To', moderator)
524         return smtp.sendEmail(
525             self.mailhost,
526             'twisted-news@' + socket.gethostname(),
527             moderator,
528             article.body,
529             dict(article.headers.values())
530         )
531
532
533     def postRequest(self, message):
534         cleave = message.find('\r\n\r\n')
535         headers, article = message[:cleave], message[cleave + 4:]
536
537         article = Article(headers, article)
538         groups = article.getHeader('Newsgroups').split()
539         xref = []
540
541         # Check for moderated status
542         moderator = self.getModerator(groups)
543         if moderator and not article.getHeader('Approved'):
544             return self.notifyModerator(moderator, article)
545
546
547         for group in groups:
548             try:
549                 g = self.dbm['groups'][group]
550             except KeyError:
551                 pass
552             else:
553                 index = g.maxArticle + 1
554                 g.maxArticle += 1
555                 g.articles[index] = article
556                 xref.append((group, str(index)))
557                 self.dbm['groups'][group] = g
558
559         if not xref:
560             return defer.fail(NewsServerError("No groups carried: " + ' '.join(groups)))
561
562         article.putHeader('Xref', '%s %s' % (socket.gethostname().split()[0], ' '.join(map(lambda x: ':'.join(x), xref))))
563         self.dbm['Message-IDs'][article.getHeader('Message-ID')] = xref
564         return defer.succeed(None)
565
566
567     def overviewRequest(self):
568         return defer.succeed(OVERVIEW_FMT)
569
570
571     def xoverRequest(self, group, low, high):
572         if not self.dbm['groups'].has_key(group):
573             return defer.succeed([])
574
575         if low is None:
576             low = 0
577         if high is None:
578             high = self.dbm['groups'][group].maxArticle
579         r = []
580         for i in range(low, high + 1):
581             if self.dbm['groups'][group].articles.has_key(i):
582                 r.append([str(i)] + self.dbm['groups'][group].articles[i].overview())
583         return defer.succeed(r)
584
585
586     def xhdrRequest(self, group, low, high, header):
587         if group not in self.dbm['groups']:
588             return defer.succeed([])
589
590         if low is None:
591             low = 0
592         if high is None:
593             high = self.dbm['groups'][group].maxArticle
594         r = []
595         for i in range(low, high + 1):
596             if self.dbm['groups'][group].articles.has_key(i):
597                 r.append((i, self.dbm['groups'][group].articles[i].getHeader(header)))
598         return defer.succeed(r)
599
600
601     def listGroupRequest(self, group):
602         if self.dbm['groups'].has_key(group):
603             return defer.succeed((group, self.dbm['groups'][group].articles.keys()))
604         return defer.fail(NewsServerError("No such group: " + group))
605
606
607     def groupRequest(self, group):
608         try:
609             g = self.dbm['groups'][group]
610         except KeyError:
611             return defer.fail(NewsServerError("No such group: " + group))
612         else:
613             flags = g.flags
614             low = g.minArticle
615             high = g.maxArticle
616             num = high - low + 1
617             return defer.succeed((group, num, high, low, flags))
618
619
620     def articleExistsRequest(self, id):
621         return defer.succeed(id in self.dbm['Message-IDs'])
622
623
624     def articleRequest(self, group, index, id = None):
625         if id is not None:
626             try:
627                 xref = self.dbm['Message-IDs'][id]
628             except KeyError:
629                 return defer.fail(NewsServerError("No such article: " + id))
630             else:
631                 group, index = xref[0]
632                 index = int(index)
633
634         try:
635             a = self.dbm['groups'][group].articles[index]
636         except KeyError:
637             return defer.fail(NewsServerError("No such group: " + group))
638         else:
639             return defer.succeed((
640                 index,
641                 a.getHeader('Message-ID'),
642                 StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
643             ))
644
645
646     def headRequest(self, group, index, id = None):
647         if id is not None:
648             try:
649                 xref = self.dbm['Message-IDs'][id]
650             except KeyError:
651                 return defer.fail(NewsServerError("No such article: " + id))
652             else:
653                 group, index = xref[0]
654                 index = int(index)
655
656         try:
657             a = self.dbm['groups'][group].articles[index]
658         except KeyError:
659             return defer.fail(NewsServerError("No such group: " + group))
660         else:
661             return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
662
663
664     def bodyRequest(self, group, index, id = None):
665         if id is not None:
666             try:
667                 xref = self.dbm['Message-IDs'][id]
668             except KeyError:
669                 return defer.fail(NewsServerError("No such article: " + id))
670             else:
671                 group, index = xref[0]
672                 index = int(index)
673
674         try:
675             a = self.dbm['groups'][group].articles[index]
676         except KeyError:
677             return defer.fail(NewsServerError("No such group: " + group))
678         else:
679             return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
680
681
682 class NewsStorageAugmentation:
683     """
684     A NewsStorage implementation using Twisted's asynchronous DB-API
685     """
686
687     implements(INewsStorage)
688
689     schema = """
690
691     CREATE TABLE groups (
692         group_id      SERIAL,
693         name          VARCHAR(80) NOT NULL,
694
695         flags         INTEGER DEFAULT 0 NOT NULL
696     );
697
698     CREATE UNIQUE INDEX group_id_index ON groups (group_id);
699     CREATE UNIQUE INDEX name_id_index ON groups (name);
700
701     CREATE TABLE articles (
702         article_id    SERIAL,
703         message_id    TEXT,
704
705         header        TEXT,
706         body          TEXT
707     );
708
709     CREATE UNIQUE INDEX article_id_index ON articles (article_id);
710     CREATE UNIQUE INDEX article_message_index ON articles (message_id);
711
712     CREATE TABLE postings (
713         group_id      INTEGER,
714         article_id    INTEGER,
715         article_index INTEGER NOT NULL
716     );
717
718     CREATE UNIQUE INDEX posting_article_index ON postings (article_id);
719
720     CREATE TABLE subscriptions (
721         group_id    INTEGER
722     );
723
724     CREATE TABLE overview (
725         header      TEXT
726     );
727     """
728
729     def __init__(self, info):
730         self.info = info
731         self.dbpool = adbapi.ConnectionPool(**self.info)
732
733
734     def __setstate__(self, state):
735         self.__dict__ = state
736         self.info['password'] = getpass.getpass('Database password for %s: ' % (self.info['user'],))
737         self.dbpool = adbapi.ConnectionPool(**self.info)
738         del self.info['password']
739
740
741     def listRequest(self):
742         # COALESCE may not be totally portable
743         # it is shorthand for
744         # CASE WHEN (first parameter) IS NOT NULL then (first parameter) ELSE (second parameter) END
745         sql = """
746             SELECT groups.name,
747                 COALESCE(MAX(postings.article_index), 0),
748                 COALESCE(MIN(postings.article_index), 0),
749                 groups.flags
750             FROM groups LEFT OUTER JOIN postings
751             ON postings.group_id = groups.group_id
752             GROUP BY groups.name, groups.flags
753             ORDER BY groups.name
754         """
755         return self.dbpool.runQuery(sql)
756
757
758     def subscriptionRequest(self):
759         sql = """
760             SELECT groups.name FROM groups,subscriptions WHERE groups.group_id = subscriptions.group_id
761         """
762         return self.dbpool.runQuery(sql)
763
764
765     def postRequest(self, message):
766         cleave = message.find('\r\n\r\n')
767         headers, article = message[:cleave], message[cleave + 4:]
768         article = Article(headers, article)
769         return self.dbpool.runInteraction(self._doPost, article)
770
771
772     def _doPost(self, transaction, article):
773         # Get the group ids
774         groups = article.getHeader('Newsgroups').split()
775         if not len(groups):
776             raise NNTPError('Missing Newsgroups header')
777
778         sql = """
779             SELECT name, group_id FROM groups
780             WHERE name IN (%s)
781         """ % (', '.join([("'%s'" % (adbapi.safe(group),)) for group in groups]),)
782
783         transaction.execute(sql)
784         result = transaction.fetchall()
785
786         # No relevant groups, bye bye!
787         if not len(result):
788             raise NNTPError('None of groups in Newsgroup header carried')
789
790         # Got some groups, now find the indices this article will have in each
791         sql = """
792             SELECT groups.group_id, COALESCE(MAX(postings.article_index), 0) + 1
793             FROM groups LEFT OUTER JOIN postings
794             ON postings.group_id = groups.group_id
795             WHERE groups.group_id IN (%s)
796             GROUP BY groups.group_id
797         """ % (', '.join([("%d" % (id,)) for (group, id) in result]),)
798
799         transaction.execute(sql)
800         indices = transaction.fetchall()
801
802         if not len(indices):
803             raise NNTPError('Internal server error - no indices found')
804
805         # Associate indices with group names
806         gidToName = dict([(b, a) for (a, b) in result])
807         gidToIndex = dict(indices)
808
809         nameIndex = []
810         for i in gidToName:
811             nameIndex.append((gidToName[i], gidToIndex[i]))
812
813         # Build xrefs
814         xrefs = socket.gethostname().split()[0]
815         xrefs = xrefs + ' ' + ' '.join([('%s:%d' % (group, id)) for (group, id) in nameIndex])
816         article.putHeader('Xref', xrefs)
817
818         # Hey!  The article is ready to be posted!  God damn f'in finally.
819         sql = """
820             INSERT INTO articles (message_id, header, body)
821             VALUES ('%s', '%s', '%s')
822         """ % (
823             adbapi.safe(article.getHeader('Message-ID')),
824             adbapi.safe(article.textHeaders()),
825             adbapi.safe(article.body)
826         )
827
828         transaction.execute(sql)
829
830         # Now update the posting to reflect the groups to which this belongs
831         for gid in gidToName:
832             sql = """
833                 INSERT INTO postings (group_id, article_id, article_index)
834                 VALUES (%d, (SELECT last_value FROM articles_article_id_seq), %d)
835             """ % (gid, gidToIndex[gid])
836             transaction.execute(sql)
837
838         return len(nameIndex)
839
840
841     def overviewRequest(self):
842         sql = """
843             SELECT header FROM overview
844         """
845         return self.dbpool.runQuery(sql).addCallback(lambda result: [header[0] for header in result])
846
847
848     def xoverRequest(self, group, low, high):
849         sql = """
850             SELECT postings.article_index, articles.header
851             FROM articles,postings,groups
852             WHERE postings.group_id = groups.group_id
853             AND groups.name = '%s'
854             AND postings.article_id = articles.article_id
855             %s
856             %s
857         """ % (
858             adbapi.safe(group),
859             low is not None and "AND postings.article_index >= %d" % (low,) or "",
860             high is not None and "AND postings.article_index <= %d" % (high,) or ""
861         )
862
863         return self.dbpool.runQuery(sql).addCallback(
864             lambda results: [
865                 [id] + Article(header, None).overview() for (id, header) in results
866             ]
867         )
868
869
870     def xhdrRequest(self, group, low, high, header):
871         sql = """
872             SELECT articles.header
873             FROM groups,postings,articles
874             WHERE groups.name = '%s' AND postings.group_id = groups.group_id
875             AND postings.article_index >= %d
876             AND postings.article_index <= %d
877         """ % (adbapi.safe(group), low, high)
878
879         return self.dbpool.runQuery(sql).addCallback(
880             lambda results: [
881                 (i, Article(h, None).getHeader(h)) for (i, h) in results
882             ]
883         )
884
885
886     def listGroupRequest(self, group):
887         sql = """
888             SELECT postings.article_index FROM postings,groups
889             WHERE postings.group_id = groups.group_id
890             AND groups.name = '%s'
891         """ % (adbapi.safe(group),)
892
893         return self.dbpool.runQuery(sql).addCallback(
894             lambda results, group = group: (group, [res[0] for res in results])
895         )
896
897
898     def groupRequest(self, group):
899         sql = """
900             SELECT groups.name,
901                 COUNT(postings.article_index),
902                 COALESCE(MAX(postings.article_index), 0),
903                 COALESCE(MIN(postings.article_index), 0),
904                 groups.flags
905             FROM groups LEFT OUTER JOIN postings
906             ON postings.group_id = groups.group_id
907             WHERE groups.name = '%s'
908             GROUP BY groups.name, groups.flags
909         """ % (adbapi.safe(group),)
910
911         return self.dbpool.runQuery(sql).addCallback(
912             lambda results: tuple(results[0])
913         )
914
915
916     def articleExistsRequest(self, id):
917         sql = """
918             SELECT COUNT(message_id) FROM articles
919             WHERE message_id = '%s'
920         """ % (adbapi.safe(id),)
921
922         return self.dbpool.runQuery(sql).addCallback(
923             lambda result: bool(result[0][0])
924         )
925
926
927     def articleRequest(self, group, index, id = None):
928         if id is not None:
929             sql = """
930                 SELECT postings.article_index, articles.message_id, articles.header, articles.body
931                 FROM groups,postings LEFT OUTER JOIN articles
932                 ON articles.message_id = '%s'
933                 WHERE groups.name = '%s'
934                 AND groups.group_id = postings.group_id
935             """ % (adbapi.safe(id), adbapi.safe(group))
936         else:
937             sql = """
938                 SELECT postings.article_index, articles.message_id, articles.header, articles.body
939                 FROM groups,articles LEFT OUTER JOIN postings
940                 ON postings.article_id = articles.article_id
941                 WHERE postings.article_index = %d
942                 AND postings.group_id = groups.group_id
943                 AND groups.name = '%s'
944             """ % (index, adbapi.safe(group))
945
946         return self.dbpool.runQuery(sql).addCallback(
947             lambda result: (
948                 result[0][0],
949                 result[0][1],
950                 StringIO.StringIO(result[0][2] + '\r\n' + result[0][3])
951             )
952         )
953
954
955     def headRequest(self, group, index):
956         sql = """
957             SELECT postings.article_index, articles.message_id, articles.header
958             FROM groups,articles LEFT OUTER JOIN postings
959             ON postings.article_id = articles.article_id
960             WHERE postings.article_index = %d
961             AND postings.group_id = groups.group_id
962             AND groups.name = '%s'
963         """ % (index, adbapi.safe(group))
964
965         return self.dbpool.runQuery(sql).addCallback(lambda result: result[0])
966
967
968     def bodyRequest(self, group, index):
969         sql = """
970             SELECT postings.article_index, articles.message_id, articles.body
971             FROM groups,articles LEFT OUTER JOIN postings
972             ON postings.article_id = articles.article_id
973             WHERE postings.article_index = %d
974             AND postings.group_id = groups.group_id
975             AND groups.name = '%s'
976         """ % (index, adbapi.safe(group))
977
978         return self.dbpool.runQuery(sql).addCallback(
979             lambda result: result[0]
980         ).addCallback(
981             lambda (index, id, body): (index, id, StringIO.StringIO(body))
982         )
983
984 ####
985 #### XXX - make these static methods some day
986 ####
987 def makeGroupSQL(groups):
988     res = ''
989     for g in groups:
990         res = res + """\n    INSERT INTO groups (name) VALUES ('%s');\n""" % (adbapi.safe(g),)
991     return res
992
993
994 def makeOverviewSQL():
995     res = ''
996     for o in OVERVIEW_FMT:
997         res = res + """\n    INSERT INTO overview (header) VALUES ('%s');\n""" % (adbapi.safe(o),)
998     return res
Note: See TracBrowser for help on using the browser.