Add --port option to gallery.py.
[blog.git] / posts / gallery / gallery.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 """
20 CGI gallery server for a picture directory organized along::
21
22   pics
23   |-- some_directory
24   |   |-- a_picture.jpg
25   |   |-- another_picture.jpg
26   |   |-- ...
27   |-- another_directory
28   |   |-- a_picture.jpg
29   |   |-- captioned_picture.jpg
30   |   |-- captioned_picture.jpg.txt
31   |-- ...
32
33 With::
34
35   pics$ gallery.py some_directory another_directory
36
37 Note that you can store a caption for ``<PICTURE>`` as plain text in
38 ``<PICTURE>.txt``.
39
40 See RFC 3875 for more details on the the Common Gateway Interface.
41
42 This script can also be run as a Simple Common Gateway Interface
43 (SCGI) with the ``--scgi`` option.
44 """
45
46 import logging as _logging
47 import logging.handlers as _logging_handlers
48 import math as _math
49 import os as _os
50 import os.path as _os_path
51 import random as _random
52 import re as _re
53 import subprocess as _subprocess
54
55
56 __version__ = '0.5'
57
58
59 IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']
60 VIDEO_EXTENSIONS = ['.mov', '.mp4', '.ogv']
61 RESPONSES = {  # httplib takes half a second to load
62     200: 'OK',
63     404: 'Not Found',
64     }
65
66 LOG = _logging.getLogger('gallery.py')
67 LOG.addHandler(_logging.StreamHandler())
68 #LOG.addHandler(_logging_handlers.SysLogHandler())
69 LOG.handlers[0].setFormatter(
70     _logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
71 LOG.setLevel(_logging.DEBUG)
72 #LOG.setLevel(_logging.WARNING)
73
74
75 class CommandError(Exception):
76     def __init__(self, command, status, stdout=None, stderr=None):
77         strerror = ['Command failed (%d):\n  %s\n' % (status, stderr),
78                     'while executing\n  %s' % str(command)]
79         Exception.__init__(self, '\n'.join(strerror))
80         self.command = command
81         self.status = status
82         self.stdout = stdout
83         self.stderr = stderr
84
85
86 class ProcessingComplete(Exception):
87     pass
88
89
90 def invoke(args, stdin=None, stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
91            expect=(0,), cwd=None, encoding=None):
92     """
93     expect should be a tuple of allowed exit codes.  cwd should be
94     the directory from which the command will be executed.  When
95     unicode_output == True, convert stdout and stdin strings to
96     unicode before returing them.
97     """
98     if cwd == None:
99         cwd = '.'
100     LOG.debug('{}$ {}'.format(cwd, ' '.join(args)))
101     try :
102         q = _subprocess.Popen(args, stdin=_subprocess.PIPE, stdout=stdout,
103                               stderr=stderr, cwd=cwd)
104     except OSError, e:
105         raise CommandError(args, status=e.args[0], stderr=e)
106     stdout,stderr = q.communicate(input=stdin)
107     status = q.wait()
108     LOG.debug('{:d}\n{}{}'.format(status, stdout, stderr))
109     if status not in expect:
110         raise CommandError(args, status, stdout, stderr)
111     return status, stdout, stderr
112
113 def is_picture(filename):
114     for extension in IMAGE_EXTENSIONS:
115         if filename.lower().endswith(extension):
116             return True
117     return False
118
119 def image_base(filename):
120     parts = filename.rsplit('.', 1)
121     assert len(parts) == 2, parts
122     return parts[0]
123
124
125 class CGIGalleryServer (object):
126     def __init__(self, base_path='/var/www/localhost/htdocs/gallery/',
127                  base_url='/cgi-bin/gallery.py',
128                  cache_path='/tmp/gallery-cache/'):
129         self._base_path = _os_path.abspath(base_path)
130         self._base_url = base_url
131         self._cache_path = cache_path
132         self._url_regexp = _re.compile('^[a-z0-9._/-]*$')
133         self._rows = 3
134         self._columns = 3
135         self.header = []
136         self.footer = []
137
138     def _http_header(self, mime='text/html', status=200):
139         msg = RESPONSES[status]
140         header = ['Status: {:d} {}'.format(status, msg)]
141         if mime.startswith('text/'):
142             charset = '; charset=UTF-8'
143         else:
144             charset = ''
145         header.append('Content-type: {}{}'.format(mime, charset))
146         return '\n'.join(header)
147
148     def _response(self, header=None, content='<h1>It works!</h1>',
149                   stream=None):
150         if header is None:
151             header = self._http_header()
152         stream.write(header)
153         stream.write('\n\n')
154         stream.write(content)
155         raise ProcessingComplete()
156
157     def _response_stream(self, header=None, content=None, stream=None,
158                          chunk_size=1024):
159         LOG.debug('streaming response')
160         if header is None:
161             header = self._http_header()
162         stream.write(header)
163         stream.write('\n\n')
164         stream.flush()  # flush headers
165         while True:
166             chunk = content.read(chunk_size)
167             if not chunk:
168                 break
169             stream.write(chunk)
170         raise ProcessingComplete()
171
172     def _error(self, status=404, content=None, stream=None):
173         header = self._http_header(status=status)
174         if content is None:
175             content = RESPONSES[status]
176         self._response(header=header, content=content, stream=stream)
177
178     def validate_url(self, url, stream=None):
179         LOG.debug('validating {}'.format(repr(url)))
180         if url is None:
181             return
182         elif (not self._url_regexp.match(url) or
183             url.startswith('/') or
184             '..' in url
185             ):
186             LOG.error('invalid url')
187             self._error(404, stream=stream)
188         path = _os_path.join(self._base_path, url)
189         if _os_path.exists(path) and not _os_path.isdir(path):
190             LOG.error('nonexistent directory')
191             self._error(404, stream=stream)
192
193     def serve(self, url=None, page=0, stream=None):
194         LOG.info('serving url {} (page {})'.format(url, page))
195         try:
196             if url is None:
197                 self.index(stream=stream)
198             elif url.endswith('random'):
199                 self.random(
200                     url=url, stream=stream, max_width=500, max_height=500)
201             elif self.is_cached(url=url):
202                 self.cached(url=url, stream=stream)
203             elif url.endswith('.png'):
204                 self._thumb(url=url, stream=stream)
205             else:
206                 self.validate_url(url=url, stream=stream)
207                 self.page(url=url, page=page, stream=stream)
208             LOG.error('unexpected url type')
209             self._error(404, stream=stream)
210         except ProcessingComplete:
211             pass
212
213     def relative_url(self, url, stream=None):
214         if url is None:
215             return url
216         if not url.startswith(self._base_url):
217             LOG.error('cannot convert {} to a relative URL of {}'.format(
218                     url, self._base_url))
219             return self._error(404, stream=stream)
220         if url == self._base_url:
221             return None
222         return url[len(self._base_url):]
223
224     def _url(self, path):
225         relpath = _os_path.relpath(
226             _os_path.join(self._base_path, path), self._base_path)
227         if relpath == '.':
228             relpath = ''
229         elif path.endswith('/'):
230             relpath += '/'
231         return '{}{}'.format(self._base_url, relpath)
232
233     def _label(self, path):
234         dirname,base = _os_path.split(path)
235         if not base:  # directory path ending with '/'
236             dirname,base = _os_path.split(dirname)
237         return base.replace('_', ' ').title()
238
239     def _link(self, path, text=None):
240         if text is None:
241             text = self._label(path)
242         return '<a href="{}">{}</a>'.format(self._url(path), text)
243
244     def _subdirs(self, path):
245         try:
246             order = [d.strip() for d in
247                      open(_os_path.join(path, '_order')).readlines()]
248         except IOError:
249             order = []
250         dirs = sorted(_os.listdir(path))
251         start = []
252         for d in order:
253             if d in dirs:
254                 start.append(d)
255                 dirs.remove(d)
256         for d in start + dirs:
257             dirpath = _os_path.join(path, d)
258             if _os_path.isdir(dirpath):
259                 yield dirpath
260
261     def _images(self, path):
262         for p in sorted(_os.listdir(path)):
263             if p.startswith('.') or p.endswith('~'):
264                 continue
265             picture_path = _os_path.join(path, p)
266             if is_picture(picture_path):
267                 yield picture_path
268
269     def index(self, stream=None):
270         LOG.debug('index page')
271         return self._directory(self._base_path, stream=stream)
272
273     def _thumb(self, image, max_width=None, max_height=None, stream=None):
274         if not _os_path.exists(self._cache_path):
275             _os.makedirs(self._cache_path)
276         dirname,filename = _os_path.split(image)
277         reldir = _os_path.relpath(dirname, self._base_path)
278         cache_dir = _os_path.join(self._cache_path, reldir)
279         if not _os_path.isdir(cache_dir):
280             _os.makedirs(cache_dir)
281         extension = '-{:d}-{:d}.png'.format(max_width, max_height)
282         thumb_filename = image_base(filename)+extension
283         thumb_url = _os_path.join(dirname, thumb_filename)
284         thumb_path = _os_path.join(cache_dir, thumb_filename)
285         image_path = _os_path.join(self._base_path, image)
286         if not _os_path.isfile(image_path):
287             LOG.error('image path for thumbnail does not exist')
288             return self._error(404, stream=stream)
289         if (not _os_path.isfile(thumb_path)
290             or _os_path.getmtime(image_path) > _os_path.getmtime(thumb_path)):
291             invoke(['convert', '-format', 'png', '-strip', '-quality', '95',
292                     image_path,
293                     '-thumbnail', '{:d}x{:d}'.format(max_width, max_height),
294                     thumb_path])
295         return thumb_url
296
297     def _mp4(self, video, stream=None):
298         if not video.endswith('.mov'):
299             LOG.error("can't translate {} to MPEGv4".format(video))
300         dirname,filename = _os_path.split(video)
301         mp4_filename = image_base(filename) + '.mp4'
302         reldir = _os_path.relpath(dirname, self._base_path)
303         cache_dir = _os_path.join(self._cache_path, reldir)
304         if not _os_path.isdir(cache_dir):
305             _os.makedirs(cache_dir)
306         mp4_url = _os_path.join(dirname, mp4_filename)
307         mp4_path = _os_path.join(cache_dir, mp4_filename)
308         if not _os_path.isfile(video):
309             LOG.error('source video path does not exist')
310             return self._error(404, stream=stream)
311         if (not _os_path.isfile(mp4_path)
312             or _os_path.getmtime(video) > _os_path.getmtime(mp4_path)):
313             arg = ['ffmpeg', '-i', video, '-acodec', 'libfaac', '-aq', '200',
314                    '-ac', '1', '-s', '640x480', '-vcodec', 'libx264',
315                    '-preset', 'slower', '-vpre', 'ipod640', '-b', '800k',
316                    '-bt', '800k', '-aspect', '640:480', '-threads', '0']
317             arg.extend(args)
318             arg.append(mp4_path)
319             invoke(arg)
320         return self._url(mp4_url)
321
322     def _ogv(self, video, stream=None):
323         if not video.endswith('.mov'):
324             LOG.error("can't translate {} to Ogg Video".format(video))
325         dirname,filename = _os_path.split(video)
326         ogv_filename = image_base(filename) + '.ogv'
327         reldir = _os_path.relpath(dirname, self._base_path)
328         cache_dir = _os_path.join(self._cache_path, reldir)
329         if not _os_path.isdir(cache_dir):
330             _os.makedirs(cache_dir)
331         ogv_url = _os_path.join(dirname, ogv_filename)
332         ogv_path = _os_path.join(cache_dir, ogv_filename)
333         if not _os_path.isfile(video):
334             LOG.error('source video path does not exist')
335             return self._error(404, stream=stream)
336         if (not _os_path.isfile(ogv_path)
337             or _os_path.getmtime(video) > _os_path.getmtime(ogv_path)):
338             arg = ['ffmpeg2theora', '--optimize']
339             arg.extend(args)
340             arg.extend(['--output', ogv_path, video])
341             invoke(arg)
342         return self._url(ogv_url)
343
344     def _get_image_caption(self, path):
345         caption_path = path + '.txt'
346         try:
347             return open(caption_path, 'r').read()
348         except IOError:
349             return None
350
351     def _get_image_video(self, path, fallback=None, stream=None):
352         base_path = image_base(path)
353         for extension in VIDEO_EXTENSIONS:
354             video_path = base_path + extension
355             if _os_path.isfile(video_path):
356                 return self._video(
357                     video_path, fallback=fallback, stream=stream)
358         return None
359
360     def _captioned_video(self, path, href=None, stream=None):
361         img = self._image(path, max_width=640, max_height=480, stream=stream)
362         caption = self._get_image_caption(path)
363         video = self._get_image_video(path, fallback=[img], stream=stream)
364         content = []
365         if video:
366             content.extend(video)
367             if href:
368                 content.append('<p>{}</p>'.format(
369                         self._link(path=href, text='gallery page')))
370         elif href:
371             content.append(self._link(path=href, text=img))
372         else:
373             content.append(img)
374         if caption:
375             content.append('<p>{}</p>'.format(caption))
376         return content
377
378     def _video(self, video, fallback=None, stream=None, **kwargs):
379         if fallback is None:
380             fallback = [
381                 '<p>Your browser does not support the &lt;video&gt; tag, try',
382                 'downloading the video and playing it in an external player.',
383                 '</p>',
384                 ]
385         fallback = ['    '+line for line in fallback]
386         ogv = self._ogv(video, stream=stream)
387         mp4 = self._mp4(video, stream=stream)
388         return [
389             '<p>',
390             ('  <video preloads="none" controls="controls" '
391              'width="640" height="480">'),
392             '    <source src="{}"'.format(mp4),
393             ('''            type='video/mp4; '''
394              '''codecs="avc1.42E01E, mp4a.40.2"' />'''),
395             '    <source src="{}"'.format(ogv),
396             '''            type='video/ogg; codecs="theora,vorbis"' />''',
397             ] + fallback + [
398             '  </video>',
399             '</p>',
400             '<p>Download as',
401             '  <a href="{}">Ogg/Theora/Vorbis</a> or'.format(ogv),
402             ('  <a href="{}">Mpeg4/H.264(ConstrainedBaselineProfile)/AAC</a>.'
403              ).format(mp4),
404             '<p>',
405             ]
406
407     def _image(self, image, **kwargs):
408         if kwargs:
409             image = self._thumb(image, **kwargs)
410         return '<img src="{}" />'.format(self._url(image))
411
412     def _image_page(self, image):
413         return image_base(image) + '/'
414
415     def random(self, url=None, stream=None, **kwargs):
416         LOG.debug('random image')
417         if url.endswith('/random'):
418             base_dir = _os_path.join(
419                 self._base_path, url[:(-len('/random'))])
420         elif url == 'random':
421             base_dir = self._base_path
422         else:
423             self._error(404, stream=stream)
424         images = []
425         for dirpath,dirnames,filenames in _os.walk(base_dir):
426             for filename in filenames:
427                 if is_picture(filename):
428                     images.append(_os_path.join(dirpath, filename))
429         if not images:
430             self._response(content='<p>no images to choose from</p>',
431                            stream=stream)
432         image = _random.choice(images)
433         LOG.debug('selected random image {}'.format(image))
434         page = self._image_page(image)
435         content = self._captioned_video(path=image, href=page, stream=stream)
436         self._response(content='\n'.join(content), stream=stream)
437
438     def is_cached(self, url):
439         for extension in ['.png', '.mp4', '.ogv']:
440             if url.endswith(extension):
441                 return True
442         return False
443
444     def cached(self, url, stream=None):
445         LOG.debug('retrieving cached item')
446         if url.endswith('.png'):
447             mime = 'image/png'
448         elif url.endswith('.ogv'):
449             mime = 'video/ogg'
450         elif url.endswith('.mp4'):
451             mime = 'video/mp4'
452         else:
453             raise NotImplementedError()
454         header = self._http_header(mime=mime)
455         cache_path = _os_path.join(self._cache_path, url)
456         try:
457             content = open(cache_path, 'rb')
458         except IOError, e:
459             LOG.error('invalid url')
460             LOG.error(e)
461             self._error(404, stream=stream)
462         if mime in ['video/ogg']:
463             self._response_stream(
464                 header=header, content=content, stream=stream)
465         content = content.read()
466         self._response(header=header, content=content, stream=stream)
467
468     def page(self, url, page=0, stream=None):
469         LOG.debug('HTML page')
470         if not url.endswith('/'):
471             LOG.error('HTML page URLs must end with a slash')
472             self._error(404, stream=stream)
473         abspath = _os_path.join(self._base_path, url)
474         if _os_path.isdir(abspath):
475             self._directory(path=abspath, page=page, stream=stream)
476         for extension in IMAGE_EXTENSIONS:
477             file_path = abspath[:-1] + extension
478             if _os_path.isfile(file_path):
479                 self._page(path=file_path, stream=stream)
480         LOG.debug('unknown HTML page')
481         self._error(404, stream=stream)
482
483     def _directory_header(self, path):
484         relpath = _os_path.relpath(path, self._base_path)
485         crumbs = []
486         dirname = relpath
487         while dirname:
488             dirname,base = _os_path.split(dirname)
489             if base != '.':
490                 crumbs.insert(0, base)
491         crumbs.insert(0, '')
492         links = [None] * len(crumbs)
493         for i,c in enumerate(crumbs):
494             if i < len(crumbs)-1:
495                 if i == 0:
496                     links[i] = self._link(self._base_path, 'Gallery')
497                 else:
498                     relpath = '/'.join(crumbs[1:i+1]) + '/'
499                     fullpath = _os_path.join(self._base_path, relpath)
500                     links[i] = self._link(path=fullpath)
501             else:
502                 if i == 0:
503                     links[i] = 'Gallery'
504                 else:
505                     links[i] = self._label(crumbs[i])
506         content = ['<h1>{}</h1>'.format(' '.join(links))]
507         return content
508
509     def _directory_page_navigation(self, path, page, pages):
510         if pages <= 1:
511             return []
512         prev_page = path + '?pp={:d}'.format((page - 1) % pages + 1)
513         next_page = path + '?pp={:d}'.format((page + 1) % pages + 1)
514         return [
515             '<div style="text-align: center;">',
516             '<p>',
517             self._link(prev_page, 'previous'),
518             '({:d} of {:d})'.format(page+1, pages),
519             self._link(next_page, 'next'),
520             '</p>',
521             '</div>', 
522            ]
523
524     def _directory_subdirs(self, path):
525         content = []
526         dirs = list(self._subdirs(path))
527         if dirs:
528             content.append('<ul>')
529             for d in dirs:
530                 content.append('  <li>{}</li>'.format(self._link(d+'/')))
531             content.append('</ul>')
532         return content
533
534     def _directory_images(self, path, images, stream=None):
535         content = ['<table style="margin-left: auto; margin-right: auto;">']
536         column = 0
537         for image in images:
538             page = self._image_page(image)
539             img = self._image(
540                 image, max_width=300, max_height=300, stream=stream)
541             link = self._link(page, img)
542             if column == 0:
543                 content.append('  <tr>')
544             content.extend([
545                     '    <td style="text-align: center;">',
546                     '      {}'.format(link),
547                     '    </td>',
548                     ])
549             column += 1
550             if column == self._columns:
551                 content.append('  </tr>')
552                 column = 0
553         if column != 0:
554             #content.extend()
555             content.append('  </tr>')
556         content.append('</table>')
557         return content
558
559     def _directory(self, path, page=0, stream=None):
560         LOG.debug('directory page')
561         images = list(self._images(path))
562         images_per_page = self._rows * self._columns
563         pages = int(_math.ceil(float(len(images)) / images_per_page)) or 1
564         if page < 0 or page >= pages:
565             LOG.error(
566                 'page out of bounds for this gallery 0 <= {:d} < {:d}'.format(
567                     page, pages))
568             self._error(404, stream=stream)
569         first_image = images_per_page * page
570         images = images[first_image:first_image+images_per_page]
571         content = []
572         content.extend(self.header)
573         content.extend(self._directory_header(path))
574         nav = self._directory_page_navigation(path, page=page, pages=pages)
575         content.extend(nav)
576         content.extend(self._directory_subdirs(path))
577         content.extend(self._directory_images(
578                 path, images=images, stream=stream))
579         content.extend(nav)
580         content.extend(self.footer)
581         self._response(content='\n'.join(content), stream=stream)
582
583     def _page(self, path, stream=None):
584         LOG.debug('image page')
585         gallery = _os_path.dirname(path)
586         images = list(self._images(gallery))
587         images_per_page = self._rows * self._columns
588         i = images.index(path)
589         page = i / images_per_page
590         gallery_page = '{}/?pp={:d}'.format(gallery, page + 1)
591         prev_page = self._image_page(images[i - 1])
592         next_page = self._image_page(images[(i + 1) % len(images)])
593         content = []
594         content.extend(self.header)
595         content.extend([
596                 '<div style="text-align: center;">',
597                 '<p>',
598                 self._link(prev_page, 'previous'),
599                 self._link(gallery_page, 'all'),
600                 self._link(next_page, 'next'),
601                 '</p>',
602                 ])
603         content.extend(self._captioned_video(path, stream=stream))
604         content.append('</div>')
605         content.extend(self.footer)
606         self._response(content='\n'.join(content), stream=stream)
607
608
609 def serve_cgi(server):
610     import cgi
611     import cgitb
612     import sys
613
614     url=None
615     page=0
616     cgitb.enable()
617     #cgitb.enable(display=0, logdir="/tmp/")
618     data = cgi.FieldStorage()
619     if 'p' in data:
620         p = data['p']
621         if isinstance(p, list):
622             p = p[0]
623         url = p.value
624     if 'pp' in data:
625         try:
626             page = int(data['pp'].value) - 1
627         except ValueError:
628             pass
629     server.serve(url=url, page=page, stream=sys.stdout)
630
631 def serve_scgi(server, host='localhost', port=4000):
632     import scgi
633     import scgi.scgi_server
634     import urlparse
635
636     class GalleryHandler(scgi.scgi_server.SCGIHandler):
637         def produce(self, env, bodysize, input, output):
638             #LOG.info(HTTP_USER_AGENT REQUEST_METHOD REMOTE_ADDR REQUEST_URI
639             url = env.get('DOCUMENT_URI', None)
640             page = 0
641             data = urlparse.parse_qs(env.get('QUERY_STRING', ''))
642             if 'pp' in data:
643                 pp = data['pp']
644                 if isinstance(pp, list):
645                     pp = pp[0]
646                 try:
647                     page = int(pp) - 1
648                 except ValueError:
649                     pass
650             try:
651                 url = server.relative_url(url=url, stream=output)
652             except ProcessingComplete:
653                 pass
654             else:
655                 server.serve(url=url, page=page, stream=output)
656
657     s = scgi.scgi_server.SCGIServer(
658         handler_class=GalleryHandler, host=host, port=port)
659     LOG.info('serving SCGI on {}:{}'.format(host, port))
660     s.serve()
661
662
663 if __name__ == '__main__':
664     import argparse as _argparse
665
666     parser = _argparse.ArgumentParser(
667         description=__doc__, version=__version__,
668         formatter_class=_argparse.RawDescriptionHelpFormatter)
669     parser.add_argument(
670         '--scgi', default=False, action='store_const', const=True,
671         help='Run as a SCGI server (vs. serving a single CGI call)')
672     parser.add_argument(
673         '--port', default=4000, type=int,
674         help='Port to listen to (if runing as a SCGI server)')
675     parser.add_argument(
676         '--base-path', default='.',
677         help='Path to the root gallery source')
678     parser.add_argument(
679         '--base-url', default='/',
680         help='URL for the root gallery source')
681     parser.add_argument(
682         '--shared-path', default=None,
683         help=('Optional path to the shared directory containing '
684               '`header.shtml` and `footer.shtml`'))
685     parser.add_argument(
686         '--cache-path', default='/tmp/gallery-cache',
687         help='Path to the thumbnail and movie cache directory')
688
689     args = parser.parse_args()
690
691     s = CGIGalleryServer(
692         base_path=args.base_path, base_url=args.base_url,
693         cache_path=args.cache_path)
694     if args.shared_path:
695         shared = args.shared_path
696         s.header = [open(_os_path.join(shared, 'header.shtml'), 'r').read()]
697         s.footer = [open(_os_path.join(shared, 'footer.shtml'), 'r').read()]
698
699     if args.scgi:
700         serve_scgi(server=s, port=args.port)
701     else:
702         serve_cgi(server=s)