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