]> git.llucax.com Git - software/blitiri.git/blob - blitiri.cgi
11a7df8f4628018a37e15a12ad6732bfe439efb9
[software/blitiri.git] / blitiri.cgi
1 #!/usr/bin/env python
2 #coding: utf8
3
4 # blitiri - A single-file blog engine.
5 # Alberto Bertogli (albertito@gmail.com)
6
7 #
8 # Configuration section
9 #
10 # You can edit these values, or create a file named "config.py" and put them
11 # there to make updating easier. The ones in config.py take precedence.
12 #
13
14 # Directory where entries are stored
15 data_path = "/tmp/blog/data"
16
17 # Directory where comments are stored (must be writeable by the web server)
18 comments_path = "/tmp/blog/comments"
19
20 # Path where templates are stored. Use an empty string for the built-in
21 # default templates. If they're not found, the built-in ones will be used.
22 templates_path = "/tmp/blog/templates"
23
24 # URL to the blog, including the name. Can be a full URL or just the path.
25 blog_url = "/blog/blitiri.cgi"
26
27 # Style sheet (CSS) URL. Can be relative or absolute. To use the built-in
28 # default, set it to blog_url + "/style".
29 css_url = blog_url + "/style"
30
31 # Blog title
32 title = "I don't like blogs"
33
34 # Default author
35 author = "Hartmut Kegan"
36
37 # Article encoding
38 encoding = "utf8"
39
40 #
41 # End of configuration
42 # DO *NOT* EDIT ANYTHING PAST HERE
43 #
44
45
46 import sys
47 import os
48 import time
49 import datetime
50 import calendar
51 import zlib
52 import urllib
53 import cgi
54 from docutils.core import publish_parts
55
56 # Before importing the config, add our cwd to the Python path
57 sys.path.append(os.getcwd())
58
59 # Load the config file, if there is one
60 try:
61         from config import *
62 except:
63         pass
64
65
66 # Pimp *_path config variables to support relative paths
67 data_path = os.path.realpath(data_path)
68 templates_path = os.path.realpath(templates_path)
69
70 # Default template
71
72 default_main_header = """\
73 <?xml version="1.0" encoding="utf-8"?>
74 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
75           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
76
77 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
78 <head>
79 <link rel="alternate" title="%(title)s" href="%(fullurl)s/atom"
80         type="application/atom+xml" />
81 <link href="%(css_url)s" rel="stylesheet" type="text/css" />
82 <title>%(title)s</title>
83 </head>
84
85 <body>
86
87 <h1><a href="%(url)s">%(title)s</a></h1>
88
89 <div class="content">
90 """
91
92 default_main_footer = """
93 </div>
94 <div class="footer">
95   %(showyear)s: %(monthlinks)s<br/>
96   years: %(yearlinks)s<br/>
97   subscribe: <a href="%(url)s/atom">atom</a><br/>
98   views: <a href="%(url)s/">blog</a> <a href="%(url)s/list">list</a><br/>
99 </div>
100
101 </body>
102 </html>
103 """
104
105 default_article_header = """
106 <div class="article">
107 <h2><a href="%(url)s/post/%(uuid)s">%(arttitle)s</a></h2>
108 <span class="artinfo">
109   by %(author)s on <span class="date">
110
111 <a class="date" href="%(url)s/%(cyear)d/">%(cyear)04d</a>-\
112 <a class="date" href="%(url)s/%(cyear)d/%(cmonth)d/">%(cmonth)02d</a>-\
113 <a class="date" href="%(url)s/%(cyear)d/%(cmonth)d/%(cday)d/">%(cday)02d</a>\
114     %(chour)02d:%(cminute)02d</span>
115   (updated on <span class="date">
116 <a class="date" href="%(url)s/%(uyear)d/">%(uyear)04d</a>-\
117 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/">%(umonth)02d</a>-\
118 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/%(uday)d/">%(uday)02d</a>\
119     %(uhour)02d:%(uminute)02d)</span><br/>
120   <span class="tags">tagged %(tags)s</span> -
121   <span class="comments">with %(comments)s comment(s)</span>
122 </span><br/>
123 <p/>
124 <div class="artbody">
125 """
126
127 default_article_footer = """
128 <p/>
129 </div>
130 </div>
131 """
132
133 default_comment_header = """
134 <div class="comment">
135 <a name="comment-%(number)d" />
136 <h3><a href="#comment-%(number)d">Comment #%(number)d</a></h3>
137 <span class="cominfo">by <a href="%(link)s">%(author)s</a>
138   on %(year)04d-%(month)02d-%(day)02d %(hour)02d:%(minute)02d</span>
139 <p/>
140 <div class="combody">
141 """
142
143 default_comment_footer = """
144 <p/>
145 </div>
146 </div>
147 """
148
149
150 # Default CSS
151 default_css = """
152 body {
153         font-family: sans-serif;
154         font-size: small;
155         width: 52em;
156 }
157
158 div.content {
159         width: 96%;
160 }
161
162 h1 {
163         font-size: large;
164         border-bottom: 2px solid #99F;
165         width: 100%;
166         margin-bottom: 1em;
167 }
168
169 h2 {
170         font-size: medium;
171         font-weigth: none;
172         margin-bottom: 1pt;
173         border-bottom: 1px solid #99C;
174 }
175
176 h3 {
177         font-size: small;
178         font-weigth: none;
179         margin-bottom: 1pt;
180         border-bottom: 1px solid #99C;
181 }
182
183 h1 a, h2 a, h3 a {
184         text-decoration: none;
185         color: black;
186 }
187
188 span.artinfo {
189         font-size: xx-small;
190 }
191
192 span.artinfo a {
193         text-decoration: none;
194         color: #339;
195 }
196
197 span.artinfo a:hover {
198         text-decoration: none;
199         color: blue;
200 }
201
202 div.artbody {
203         margin-left: 1em;
204 }
205
206 div.article {
207         margin-bottom: 2em;
208 }
209
210 span.cominfo {
211         font-size: xx-small;
212 }
213
214 span.cominfo a {
215         text-decoration: none;
216         color: #339;
217 }
218
219 span.cominfo a:hover {
220         text-decoration: none;
221         color: blue;
222 }
223
224 div.combody {
225         margin-left: 2em;
226 }
227
228 div.comment {
229         margin-left: 1em;
230         margin-bottom: 1em;
231 }
232
233 hr {
234         float: left;
235         height: 2px;
236         border: 0;
237         background-color: #99F;
238         width: 60%;
239 }
240
241 div.footer {
242         margin-top: 1em;
243         padding-top: 0.4em;
244         width: 100%;
245         border-top: 2px solid #99F;
246         font-size: x-small;
247 }
248
249 div.footer a {
250         text-decoration: none;
251 }
252
253 /* Articles are enclosed in <div class="section"> */
254 div.section h1 {
255         font-size: small;
256         font-weigth: none;
257         width: 100%;
258         margin-bottom: 1pt;
259         border-bottom: 1px dotted #99C;
260 }
261
262 """
263
264 # helper functions
265 def rst_to_html(rst):
266         settings = {
267                 'input_encoding': encoding,
268                 'output_encoding': 'utf8',
269         }
270         parts = publish_parts(rst, settings_overrides = settings,
271                                 writer_name = "html")
272         return parts['body'].encode('utf8')
273
274 def sanitize(obj):
275         if isinstance(obj, basestring):
276                 return cgi.escape(obj, True)
277         return obj
278
279
280 # find out our URL, needed for syndication
281 try:
282         n = os.environ['SERVER_NAME']
283         p = os.environ['SERVER_PORT']
284         s = os.environ['SCRIPT_NAME']
285         if p == '80': p = ''
286         else: p = ':' + p
287         full_url = 'http://%s%s%s' % (n, p, s)
288 except KeyError:
289         full_url = 'Not needed'
290
291
292 class Templates (object):
293         def __init__(self, tpath, db, showyear = None):
294                 self.tpath = tpath
295                 self.db = db
296                 now = datetime.datetime.now()
297                 if not showyear:
298                         showyear = now.year
299
300                 self.vars = {
301                         'css_url': css_url,
302                         'title': title,
303                         'url': blog_url,
304                         'fullurl': full_url,
305                         'year': now.year,
306                         'month': now.month,
307                         'day': now.day,
308                         'showyear': showyear,
309                         'monthlinks': ' '.join(db.get_month_links(showyear)),
310                         'yearlinks': ' '.join(db.get_year_links()),
311                 }
312
313         def get_template(self, page_name, default_template, extra_vars = None):
314                 if extra_vars is None:
315                         vars = self.vars
316                 else:
317                         vars = self.vars.copy()
318                         vars.update(extra_vars)
319
320                 p = '%s/%s.html' % (self.tpath, page_name)
321                 if os.path.isfile(p):
322                         return open(p).read() % vars
323                 return default_template % vars
324
325         def get_main_header(self):
326                 return self.get_template('header', default_main_header)
327
328         def get_main_footer(self):
329                 return self.get_template('footer', default_main_footer)
330
331         def get_article_header(self, article):
332                 return self.get_template(
333                         'art_header', default_article_header, article.to_vars())
334
335         def get_article_footer(self, article):
336                 return self.get_template(
337                         'art_footer', default_article_footer, article.to_vars())
338
339         def get_comment_header(self, comment):
340                 return self.get_template(
341                         'com_header', default_comment_header, comment.to_vars())
342
343         def get_comment_footer(self, comment):
344                 return self.get_template(
345                         'com_footer', default_comment_footer, comment.to_vars())
346
347
348 class Comment (object):
349         def __init__(self, article, number, created = None):
350                 self.article = article
351                 self.number = number
352                 if created is None:
353                         self.created = datetime.datetime.now()
354                 else:
355                         self.created = created
356
357                 self.loaded = False
358
359                 # loaded on demand
360                 self._author = author
361                 self._link = ''
362                 self._raw_content = 'Removed comment'
363
364
365         def get_author(self):
366                 if not self.loaded:
367                         self.load()
368                 return self._author
369         author = property(fget = get_author)
370
371         def get_link(self):
372                 if not self.loaded:
373                         self.load()
374                 return self._link
375         link = property(fget = get_link)
376
377         def get_raw_content(self):
378                 if not self.loaded:
379                         self.load()
380                 return self._raw_content
381         raw_content = property(fget = get_raw_content)
382
383
384         def load(self):
385                 filename = os.path.join(comments_path, self.article.uuid,
386                                         str(self.number))
387                 try:
388                         raw = open(filename).readlines()
389                 except:
390                         return
391
392                 count = 0
393                 for l in raw:
394                         if ':' in l:
395                                 name, value = l.split(':', 1)
396                                 if name.lower() == 'author':
397                                         self._author = value.strip()
398                                 elif name.lower() == 'link':
399                                         self._link = value.strip()
400                         elif l == '\n':
401                                 # end of header
402                                 break
403                         count += 1
404                 self._raw_content = ''.join(raw[count + 1:])
405                 self.loaded = True
406
407         def to_html(self):
408                 return rst_to_html(self.raw_content)
409
410         def to_vars(self):
411                 return {
412                         'number': self.number,
413                         'author': sanitize(self.author),
414                         'link': sanitize(self.link),
415                         'date': self.created.isoformat(' '),
416                         'created': self.created.isoformat(' '),
417
418                         'year': self.created.year,
419                         'month': self.created.month,
420                         'day': self.created.day,
421                         'hour': self.created.hour,
422                         'minute': self.created.minute,
423                         'second': self.created.second,
424                 }
425
426 class CommentDB (object):
427         def __init__(self, article):
428                 self.path = os.path.join(comments_path, article.uuid)
429                 self.comments = []
430                 self.load(article)
431
432         def load(self, article):
433                 try:
434                         f = open(os.path.join(self.path, 'db'))
435                 except:
436                         return
437
438                 for l in f:
439                         # Each line has the following comma separated format:
440                         # number, created (epoch)
441                         # Empty lines are meaningful and represent removed
442                         # comments (so we can preserve the comment number)
443                         l = l.split(',')
444                         try:
445                                 n = int(l[0])
446                                 d = datetime.datetime.fromtimestamp(float(l[1]))
447                         except:
448                                 # Removed/invalid comment
449                                 self.comments.append(None)
450                                 continue
451                         self.comments.append(Comment(article, n, d))
452
453         def save(self):
454                 old_db = os.path.join(self.path, 'db')
455                 new_db = os.path.join(self.path, 'db.tmp')
456                 f = open(new_db, 'w')
457                 for c in self.comments:
458                         s = ''
459                         if c is not None:
460                                 s = ''
461                                 s += str(c.number) + ', '
462                                 s += str(time.mktime(c.created.timetuple()))
463                         s += '\n'
464                         f.write(s)
465                 f.close()
466                 os.rename(new_db, old_db)
467
468
469 class Article (object):
470         def __init__(self, path, created = None, updated = None):
471                 self.path = path
472                 self.created = created
473                 self.updated = updated
474                 self.uuid = "%08x" % zlib.crc32(self.path)
475
476                 self.loaded = False
477
478                 # loaded on demand
479                 self._title = 'Removed post'
480                 self._author = author
481                 self._tags = []
482                 self._raw_content = ''
483                 self._comments = []
484
485
486         def get_title(self):
487                 if not self.loaded:
488                         self.load()
489                 return self._title
490         title = property(fget = get_title)
491
492         def get_author(self):
493                 if not self.loaded:
494                         self.load()
495                 return self._author
496         author = property(fget = get_author)
497
498         def get_tags(self):
499                 if not self.loaded:
500                         self.load()
501                 return self._tags
502         tags = property(fget = get_tags)
503
504         def get_raw_content(self):
505                 if not self.loaded:
506                         self.load()
507                 return self._raw_content
508         raw_content = property(fget = get_raw_content)
509
510         def get_comments(self):
511                 if not self.loaded:
512                         self.load()
513                 return self._comments
514         comments = property(fget = get_comments)
515
516
517         def __cmp__(self, other):
518                 if self.path == other.path:
519                         return 0
520                 if not self.created:
521                         return 1
522                 if not other.created:
523                         return -1
524                 if self.created < other.created:
525                         return -1
526                 return 1
527
528         def title_cmp(self, other):
529                 return cmp(self.title, other.title)
530
531
532         def load(self):
533                 # XXX this tweak is only needed for old DB format, where
534                 # article's paths started with a slash
535                 path = self.path
536                 if path.startswith('/'):
537                         path = path[1:]
538                 filename = os.path.join(data_path, path)
539                 try:
540                         raw = open(filename).readlines()
541                 except:
542                         return
543
544                 count = 0
545                 for l in raw:
546                         if ':' in l:
547                                 name, value = l.split(':', 1)
548                                 if name.lower() == 'title':
549                                         self._title = value.strip()
550                                 elif name.lower() == 'author':
551                                         self._author = value.strip()
552                                 elif name.lower() == 'tags':
553                                         ts = value.split(',')
554                                         ts = [t.strip() for t in ts]
555                                         self._tags = set(ts)
556                         elif l == '\n':
557                                 # end of header
558                                 break
559                         count += 1
560                 self._raw_content = ''.join(raw[count + 1:])
561                 db = CommentDB(self)
562                 self._comments = db.comments
563                 self.loaded = True
564
565         def to_html(self):
566                 return rst_to_html(self.raw_content)
567
568         def to_vars(self):
569                 return {
570                         'arttitle': sanitize(self.title),
571                         'author': sanitize(self.author),
572                         'date': self.created.isoformat(' '),
573                         'uuid': self.uuid,
574                         'tags': self.get_tags_links(),
575                         'comments': len(self.comments),
576
577                         'created': self.created.isoformat(' '),
578                         'ciso': self.created.isoformat(),
579                         'cyear': self.created.year,
580                         'cmonth': self.created.month,
581                         'cday': self.created.day,
582                         'chour': self.created.hour,
583                         'cminute': self.created.minute,
584                         'csecond': self.created.second,
585
586                         'updated': self.updated.isoformat(' '),
587                         'uiso': self.updated.isoformat(),
588                         'uyear': self.updated.year,
589                         'umonth': self.updated.month,
590                         'uday': self.updated.day,
591                         'uhour': self.updated.hour,
592                         'uminute': self.updated.minute,
593                         'usecond': self.updated.second,
594                 }
595
596         def get_tags_links(self):
597                 l = []
598                 tags = list(self.tags)
599                 tags.sort()
600                 for t in tags:
601                         l.append('<a class="tag" href="%s/tag/%s">%s</a>' % \
602                                 (blog_url, urllib.quote(t), sanitize(t) ))
603                 return ', '.join(l)
604
605
606 class ArticleDB (object):
607         def __init__(self, dbpath):
608                 self.dbpath = dbpath
609                 self.articles = []
610                 self.uuids = {}
611                 self.actyears = set()
612                 self.actmonths = set()
613                 self.load()
614
615         def get_articles(self, year = 0, month = 0, day = 0, tags = None):
616                 l = []
617                 for a in self.articles:
618                         if year and a.created.year != year: continue
619                         if month and a.created.month != month: continue
620                         if day and a.created.day != day: continue
621                         if tags and not tags.issubset(a.tags): continue
622
623                         l.append(a)
624
625                 return l
626
627         def get_article(self, uuid):
628                 return self.uuids[uuid]
629
630         def load(self):
631                 try:
632                         f = open(self.dbpath)
633                 except:
634                         return
635
636                 for l in f:
637                         # Each line has the following comma separated format:
638                         # path (relative to data_path), \
639                         #       created (epoch), \
640                         #       updated (epoch)
641                         try:
642                                 l = l.split(',')
643                         except:
644                                 continue
645
646                         a = Article(l[0],
647                                 datetime.datetime.fromtimestamp(float(l[1])),
648                                 datetime.datetime.fromtimestamp(float(l[2])))
649                         self.uuids[a.uuid] = a
650                         self.actyears.add(a.created.year)
651                         self.actmonths.add((a.created.year, a.created.month))
652                         self.articles.append(a)
653
654         def save(self):
655                 f = open(self.dbpath + '.tmp', 'w')
656                 for a in self.articles:
657                         s = ''
658                         s += a.path + ', '
659                         s += str(time.mktime(a.created.timetuple())) + ', '
660                         s += str(time.mktime(a.updated.timetuple())) + '\n'
661                         f.write(s)
662                 f.close()
663                 os.rename(self.dbpath + '.tmp', self.dbpath)
664
665         def get_year_links(self):
666                 yl = list(self.actyears)
667                 yl.sort(reverse = True)
668                 return [ '<a href="%s/%d/">%d</a>' % (blog_url, y, y)
669                                 for y in yl ]
670
671         def get_month_links(self, year):
672                 am = [ i[1] for i in self.actmonths if i[0] == year ]
673                 ml = []
674                 for i in range(1, 13):
675                         name = calendar.month_name[i][:3]
676                         if i in am:
677                                 s = '<a href="%s/%d/%d/">%s</a>' % \
678                                         ( blog_url, year, i, name )
679                         else:
680                                 s = name
681                         ml.append(s)
682                 return ml
683
684 #
685 # Main
686 #
687
688
689 def render_html(articles, db, actyear = None, show_comments = False):
690         template = Templates(templates_path, db, actyear)
691         print 'Content-type: text/html; charset=utf-8\n'
692         print template.get_main_header()
693         for a in articles:
694                 print template.get_article_header(a)
695                 print a.to_html()
696                 print template.get_article_footer(a)
697                 if show_comments:
698                         print '<a name="comments" />'
699                         for c in a.comments:
700                                 if c is None:
701                                         continue
702                                 print template.get_comment_header(c)
703                                 print c.to_html()
704                                 print template.get_comment_footer(c)
705         print template.get_main_footer()
706
707 def render_artlist(articles, db, actyear = None):
708         template = Templates(templates_path, db, actyear)
709         print 'Content-type: text/html; charset=utf-8\n'
710         print template.get_main_header()
711         print '<h2>Articles</h2>'
712         for a in articles:
713                 print '<li><a href="%(url)s/uuid/%(uuid)s">%(title)s</a></li>' \
714                         % {     'url': blog_url,
715                                 'uuid': a.uuid,
716                                 'title': a.title,
717                                 'author': a.author,
718                         }
719         print template.get_main_footer()
720
721 def render_atom(articles):
722         if len(articles) > 0:
723                 updated = articles[0].updated.isoformat()
724         else:
725                 updated = datetime.datetime.now().isoformat()
726
727         print 'Content-type: application/atom+xml; charset=utf-8\n'
728         print """<?xml version="1.0" encoding="utf-8"?>
729
730 <feed xmlns="http://www.w3.org/2005/Atom">
731  <title>%(title)s</title>
732  <link rel="alternate" type="text/html" href="%(url)s"/>
733  <link rel="self" type="application/atom+xml" href="%(url)s/atom"/>
734  <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
735  <updated>%(updated)sZ</updated>
736
737         """ % {
738                 'title': title,
739                 'url': full_url,
740                 'updated': updated,
741         }
742
743         for a in articles:
744                 vars = a.to_vars()
745                 vars.update( {
746                         'url': full_url,
747                         'contents': a.to_html(),
748                 } )
749                 print """
750   <entry>
751     <title>%(arttitle)s</title>
752     <author><name>%(author)s</name></author>
753     <link href="%(url)s/post/%(uuid)s" />
754     <id>%(url)s/post/%(uuid)s</id>
755     <summary>%(arttitle)s</summary>
756     <published>%(ciso)sZ</published>
757     <updated>%(uiso)sZ</updated>
758     <content type="xhtml">
759       <div xmlns="http://www.w3.org/1999/xhtml"><p>
760 %(contents)s
761       </p></div>
762     </content>
763   </entry>
764                 """ % vars
765         print "</feed>"
766
767
768 def render_style():
769         print 'Content-type: text/css\r\n\r\n',
770         print default_css
771
772 def handle_cgi():
773         import cgitb; cgitb.enable()
774
775         form = cgi.FieldStorage()
776         year = int(form.getfirst("year", 0))
777         month = int(form.getfirst("month", 0))
778         day = int(form.getfirst("day", 0))
779         tags = set(form.getlist("tag"))
780         uuid = None
781         atom = False
782         style = False
783         post = False
784         artlist = False
785
786         if os.environ.has_key('PATH_INFO'):
787                 path_info = os.environ['PATH_INFO']
788                 style = path_info == '/style'
789                 atom = path_info == '/atom'
790                 tag = path_info.startswith('/tag/')
791                 post = path_info.startswith('/post/')
792                 artlist = path_info.startswith('/list')
793                 if not style and not atom and not post and not tag \
794                                 and not artlist:
795                         date = path_info.split('/')[1:]
796                         try:
797                                 if len(date) > 1 and date[0]:
798                                         year = int(date[0])
799                                 if len(date) > 2 and date[1]:
800                                         month = int(date[1])
801                                 if len(date) > 3 and date[2]:
802                                         day = int(date[2])
803                         except ValueError:
804                                 pass
805                 elif post:
806                         uuid = path_info.replace('/post/', '')
807                         uuid = uuid.replace('/', '')
808                 elif tag:
809                         t = path_info.replace('/tag/', '')
810                         t = t.replace('/', '')
811                         t = urllib.unquote_plus(t)
812                         tags = set((t,))
813
814         db = ArticleDB(os.path.join(data_path, 'db'))
815         if atom:
816                 articles = db.get_articles(tags = tags)
817                 articles.sort(reverse = True)
818                 render_atom(articles[:10])
819         elif style:
820                 render_style()
821         elif post:
822                 render_html( [db.get_article(uuid)], db, year, True )
823         elif artlist:
824                 articles = db.get_articles()
825                 articles.sort(cmp = Article.title_cmp)
826                 render_artlist(articles, db)
827         else:
828                 articles = db.get_articles(year, month, day, tags)
829                 articles.sort(reverse = True)
830                 if not year and not month and not day and not tags:
831                         articles = articles[:10]
832                 render_html(articles, db, year)
833
834
835 def usage():
836         print 'Usage: %s {add|rm|update} article_path' % sys.argv[0]
837
838 def handle_cmd():
839         if len(sys.argv) != 3:
840                 usage()
841                 return 1
842
843         cmd = sys.argv[1]
844         art_path = os.path.realpath(sys.argv[2])
845
846         if os.path.commonprefix([data_path, art_path]) != data_path:
847                 print "Error: article (%s) must be inside data_path (%s)" % \
848                                 (art_path, data_path)
849                 return 1
850         art_path = art_path[len(data_path)+1:]
851
852         db_filename = os.path.join(data_path, 'db')
853         if not os.path.isfile(db_filename):
854                 open(db_filename, 'w').write('')
855         db = ArticleDB(db_filename)
856
857         if cmd == 'add':
858                 article = Article(art_path, datetime.datetime.now(),
859                                         datetime.datetime.now())
860                 for a in db.articles:
861                         if a == article:
862                                 print 'Error: article already exists'
863                                 return 1
864                 db.articles.append(article)
865                 db.save()
866         elif cmd == 'rm':
867                 article = Article(art_path)
868                 for a in db.articles:
869                         if a == article:
870                                 break
871                 else:
872                         print "Error: no such article"
873                         return 1
874                 db.articles.remove(a)
875                 db.save()
876         elif cmd == 'update':
877                 article = Article(art_path)
878                 for a in db.articles:
879                         if a == article:
880                                 break
881                 else:
882                         print "Error: no such article"
883                         return 1
884                 a.updated = datetime.datetime.now()
885                 db.save()
886         else:
887                 usage()
888                 return 1
889
890         return 0
891
892
893 if os.environ.has_key('GATEWAY_INTERFACE'):
894         handle_cgi()
895 else:
896         sys.exit(handle_cmd())
897
898