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