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