-#!/usr/bin/python
+#!/usr/bin/env python
#
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2013 W. Trevor King <wking@tremily.us>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-Generate HTML gallery pages for a picture directory organized along::
+CGI gallery server for a picture directory organized along::
pics
|-- some_directory
With::
- pics$ gallery.py some_directory another_directory
+ pics$ gallery.py
Note that you can store a caption for ``<PICTURE>`` as plain text in
``<PICTURE>.txt``.
-The resulting gallery pages will be::
+See RFC 3875 for more details on the the Common Gateway Interface.
- pics
- |-- some_directory
- | |-- a_picture.jpg
- | |-- another_picture.jpg
- | |-- ...
- | |-- <INDEX> -> <GALLERY-1>
- | |-- <GALLERY-1>
- | |-- <GALLERY-2>
- | |-- <GALLERY-...>
- | |-- <THUMBDIR>
- | | |-- a_picture.png
- | | |-- another_picture.png
- | | |-- ...
- | |-- <PAGEDIR>
- | | |-- a_picture.html
- | | |-- another_picture.html
- | | |-- ...
- |-- ...
-
-So you'll probably want to symlink index.html to <GALLERY-1>.
+This script can also be run as a Simple Common Gateway Interface
+(SCGI) with the ``--scgi`` option.
"""
-import logging
-import os
-import os.path
-from subprocess import Popen, PIPE
-import sys
+import logging as _logging
+import logging.handlers as _logging_handlers
+import math as _math
+import mimetypes as _mimetypes
+import os as _os
+import os.path as _os_path
+import random as _random
+import re as _re
+import subprocess as _subprocess
+import urlparse as _urlparse
+import xml.sax.saxutils as _xml_sax_saxutils
+
+
+__version__ = '0.5'
-__version__ = '0.3'
-LOG = logging
+IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']
+VIDEO_EXTENSIONS = ['.mov', '.mp4', '.ogv']
+STREAMING_TYPES = ['video/ogg']
+RESPONSES = { # httplib takes half a second to load
+ 200: 'OK',
+ 404: 'Not Found',
+ }
+
+LOG = _logging.getLogger('gallery.py')
+LOG.addHandler(_logging.StreamHandler())
+#LOG.addHandler(_logging_handlers.SysLogHandler())
+LOG.handlers[0].setFormatter(
+ _logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
+LOG.setLevel(_logging.DEBUG)
+#LOG.setLevel(_logging.WARNING)
class CommandError(Exception):
def __init__(self, command, status, stdout=None, stderr=None):
strerror = ['Command failed (%d):\n %s\n' % (status, stderr),
'while executing\n %s' % str(command)]
- Exception.__init__(self, '\n'.join(strerror))
+ super(CommandError, self).__init__('\n'.join(strerror))
self.command = command
self.status = status
self.stdout = stdout
self.stderr = stderr
-def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
- cwd=None, encoding=None):
+
+class HTTPError(Exception):
+ def __init__(self, status, message=None, content=None):
+ if message is None:
+ message = RESPONSES[status]
+ super(HTTPError, self).__init__('{} {}'.format(status, message))
+ self.status = status
+ self.message = message
+ self.content = content
+
+
+class ProcessingComplete(Exception):
+ pass
+
+
+def invoke(args, stdin=None, stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
+ expect=(0,), cwd=None, encoding=None):
"""
expect should be a tuple of allowed exit codes. cwd should be
the directory from which the command will be executed. When
"""
if cwd == None:
cwd = '.'
- LOG.debug('%s$ %s' % (cwd, ' '.join(args)))
+ LOG.debug('{}$ {}'.format(cwd, ' '.join(args)))
try :
- q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
- except OSError, e:
+ q = _subprocess.Popen(args, stdin=_subprocess.PIPE, stdout=stdout,
+ stderr=stderr, cwd=cwd)
+ except OSError as e:
raise CommandError(args, status=e.args[0], stderr=e)
stdout,stderr = q.communicate(input=stdin)
status = q.wait()
- LOG.debug('%d\n%s%s' % (status, stdout, stderr))
+ LOG.debug('{:d}\n{}{}'.format(status, stdout, stderr))
if status not in expect:
raise CommandError(args, status, stdout, stderr)
return status, stdout, stderr
-def is_picture(filename):
- for extension in ['.jpg', '.jpeg', '.tif', '.tiff', '.png', '.gif']:
+def is_image(filename):
+ for extension in IMAGE_EXTENSIONS:
if filename.lower().endswith(extension):
return True
return False
-def picture_base(filename):
+def is_video(filename):
+ for extension in IMAGE_EXTENSIONS:
+ if filename.lower().endswith(extension):
+ return True
+ return False
+
+def image_base(filename):
parts = filename.rsplit('.', 1)
assert len(parts) == 2, parts
return parts[0]
-def pictures(picdir):
- return sorted([p for p in os.listdir(picdir) if is_picture(p)])
-
-def make_thumbs(picdir, pictures, thumbdir, max_height, max_width,
- force=False):
- fullthumbdir = os.path.join(picdir, thumbdir)
- if not os.path.exists(fullthumbdir):
- os.mkdir(fullthumbdir)
- if force == False:
- new_pictures = []
- for p in pictures:
- thumb_p = os.path.join(picdir, thumbdir, picture_base(p)+'.png')
- if (not os.path.exists(thumb_p)
- or os.path.getmtime(p) > os.path.getmtime(thumb_p)):
- new_pictures.append(p)
- if len(new_pictures) > 0:
- log.info(' making %d thumbnails for %s' % (len(new_pictures), picdir))
- invoke(['mogrify', '-format', 'png', '-strip', '-quality', '95',
- '-path', thumbdir,
- '-thumbnail', '%dx%d' % (max_width, max_height),
- ]+new_pictures, cwd=picdir)
- return [os.path.join(thumbdir, picture_base(p)+'.png')
- for p in pictures]
-
-def page_header():
- return '\n'.join([
- '<html>',
- '<body>', ''])
-
-def page_footer():
- return '\n'.join([
- '</body>',
- '</html>', ''])
-
-def pagename(pic):
- return picture_base(pic) + '.html'
-
-def make_page(picdir, gallery, pagedir, pic,
- previous_pic=None, next_pic=None, force=False):
- fullpagedir = os.path.join(picdir, pagedir)
- name = pagename(pic)
- if not os.path.exists(fullpagedir):
- os.mkdir(fullpagedir)
- page_p = os.path.join(fullpagedir, name)
- captionfile = os.path.join(picdir, pic+'.txt')
- if (not force
- and os.path.exists(page_p)
- and ((not os.path.exists(captionfile))
- or (os.path.exists(captionfile)
- and os.path.getmtime(captionfile) < os.path.getmtime(page_p)))):
- LOG.info(' skip page for %s' % page_p)
- return os.path.join(pagedir, name)
- LOG.info(' make page for %s' % page_p)
- p = open(page_p, 'w')
- p.write(page_header())
- if os.path.exists(captionfile):
- caption = open(captionfile, 'r').read()
- LOG.debug(' found caption %s' % captionfile)
- else:
- caption = None
- p.write('<div style="align: center; text-align: center;">\n')
- if previous_pic != None:
- p.write('<a href="./%s">previous</a>\n' % pagename(previous_pic))
- p.write('<a href="../%s">all</a>\n' % gallery)
- if next_pic != None:
- p.write('<a href="./%s">next</a>\n' % pagename(next_pic))
- p.write('<br />\n')
- p.write('<img width="600" src="../%s" />\n' % pic)
- if caption != None:
- p.write('<p>%s</p>\n' % caption)
- p.write('</div>\n')
- p.write(page_footer())
- return os.path.join(pagedir, name)
-
-def gallery_header(gallery_page_index=None):
- return '\n'.join([
- '<html>',
- '<body>',''])
-
-def gallery_footer(gallery_page_index=None):
- return '\n'.join([
- '</body>',
- '</html>',''])
-
-def make_gallery(picdir, index, gallery, pagedir, thumbdir,
- cols, rows, height, width,
- up_link=None, force=False):
- LOG.info('make gallery for %s' % picdir)
- pics = pictures(picdir)
- thumbs = make_thumbs(picdir, pics, thumbdir, height, width, force=force)
- pages = []
- if os.path.exists(os.path.join(picdir, gallery)) and force == False:
- return
- pic_thumbs = zip(pics, thumbs)
- i = 0
- gallery_i = 1 # one-indexed
- g = None
- while i < len(pic_thumbs):
- if g == None:
- gallery_page = os.path.join(picdir, gallery % gallery_i))
- LOG.info(' write gallery page %s' % gallery_page)
- g = open(gallery_page, 'w')
- g.write(gallery_header(gallery_i))
- if up_link != None:
- up_html = '<a href="../">%s</a>\n' % up_link
+
+class CGIGalleryServer (object):
+ def __init__(self, base_path='.',
+ base_url='/',
+ cache_path='/tmp/gallery-cache/',
+ serve_originals=True):
+ self._base_path = _os_path.abspath(base_path)
+ self._base_url = base_url
+ self._cache_path = cache_path
+ self._serve_originals = serve_originals
+ self._url_regexp = _re.compile('^[a-zA-Z0-9._/-]*$')
+ self._rows = 3
+ self._columns = 3
+ self.header = []
+ self.footer = []
+
+ def _http_header(self, mime='text/html', status=200):
+ msg = RESPONSES[status]
+ header = ['Status: {:d} {}'.format(status, msg)]
+ if mime.startswith('text/'):
+ charset = '; charset=UTF-8'
+ else:
+ charset = ''
+ header.append('Content-type: {}{}'.format(mime, charset))
+ return '\n'.join(header)
+
+ def _response(self, header=None, content='<h1>It works!</h1>',
+ stream=None):
+ if header is None:
+ header = self._http_header()
+ stream.write(header)
+ stream.write('\n\n')
+ stream.write(content)
+ raise ProcessingComplete()
+
+ def _response_stream(self, header=None, content=None, stream=None,
+ chunk_size=1024):
+ LOG.debug('streaming response')
+ if header is None:
+ header = self._http_header()
+ stream.write(header)
+ stream.write('\n\n')
+ stream.flush() # flush headers
+ while True:
+ chunk = content.read(chunk_size)
+ if not chunk:
+ break
+ stream.write(chunk)
+ raise ProcessingComplete()
+
+ def _error(self, status=404, content=None, stream=None):
+ header = self._http_header(status=status)
+ if content is None:
+ content = RESPONSES[status]
+ self._response(header=header, content=content, stream=stream)
+
+ def validate_url(self, url, exists=True, directory=False):
+ LOG.debug('validating {} (exists={}, directory={})'.format(
+ repr(url), exists, directory))
+ if url is None:
+ return
+ elif (not self._url_regexp.match(url) or
+ url.startswith('/') or
+ '..' in url
+ ):
+ LOG.error('invalid url')
+ raise HTTPError(404)
+ if exists:
+ path = _os_path.join(self._base_path, url)
+ if directory:
+ if not _os_path.isdir(path):
+ LOG.error('nonexistent directory')
+ raise HTTPError(404)
else:
- up_html = ''
- if gallery_i > 1:
- prev_url = gallery % (gallery_i-1)
- prev_html = '<a href="./%s">previous</a>\n' % prev_url
+ if not _os_path.isfile(path):
+ raise HTTPError(404, 'nonexistent file')
+
+ def serve(self, url=None, page=0, stream=None):
+ LOG.info('serving url {} (page {})'.format(url, page))
+ try:
+ try:
+ if url is None:
+ self.index(stream=stream)
+ elif url.endswith('random'):
+ self.random(
+ url=url, stream=stream, max_width=500, max_height=500)
+ elif self.is_cacheable(url=url):
+ self.validate_url(url=url, exists=False)
+ self.cached(url=url, stream=stream)
+ else:
+ self.validate_url(url=url, exists=False, directory=True)
+ self.page(url=url, page=page, stream=stream)
+ raise HTTPError(404, 'unexpected URL type')
+ except HTTPError as e:
+ LOG.error(e.message)
+ self._error(e.status, content=e.content, stream=stream)
+ except ProcessingComplete:
+ pass
+
+ def page_from_query(self, query=None, query_string=None):
+ """Extract the requested page from a query string
+
+ This is a helper method for CGIGalleryServer consumers.
+ Specify either query or query_string, but not both.
+ """
+ if query is None:
+ query = _urlparse.parse_qs(query_string)
+ page = 0
+ if 'pp' in query:
+ pp = query['pp']
+ if isinstance(pp, list):
+ pp = pp[0]
+ try:
+ page = int(pp) - 1
+ except ValueError:
+ pass
+ return page
+
+ def relative_url(self, url):
+ if url is None:
+ return url
+ if not url.startswith(self._base_url):
+ message = 'cannot convert {} to a relative URL of {}'.format(
+ url, self._base_url)
+ raise HTTPError(404, message)
+ if url == self._base_url:
+ return None
+ return url[len(self._base_url):]
+
+ def _url(self, path):
+ relpath = _os_path.relpath(
+ _os_path.join(self._base_path, path), self._base_path)
+ if relpath == '.':
+ relpath = ''
+ elif path.endswith('/'):
+ relpath += '/'
+ return '{}{}'.format(self._base_url, relpath)
+
+ def _label(self, path):
+ dirname,base = _os_path.split(path)
+ if not base: # directory path ending with '/'
+ dirname,base = _os_path.split(dirname)
+ return base.replace('_', ' ').title()
+
+ def _link(self, path, text=None):
+ if text is None:
+ text = self._label(path)
+ return '<a href="{}">{}</a>'.format(self._url(path), text)
+
+ def _subdirs(self, path):
+ try:
+ order = [d.strip() for d in
+ open(_os_path.join(path, '_order')).readlines()]
+ except IOError:
+ order = []
+ dirs = sorted(_os.listdir(path))
+ start = []
+ for d in order:
+ if d in dirs:
+ start.append(d)
+ dirs.remove(d)
+ for d in start + dirs:
+ dirpath = _os_path.join(path, d)
+ if _os_path.isdir(dirpath):
+ yield dirpath
+
+ def _images(self, path):
+ for p in sorted(_os.listdir(path)):
+ if p.startswith('.') or p.endswith('~'):
+ continue
+ picture_path = _os_path.join(path, p)
+ if is_image(picture_path):
+ yield picture_path
+
+ def index(self, stream=None):
+ LOG.debug('index page')
+ return self._directory(self._base_path, stream=stream)
+
+ def _original_url(self, url):
+ """Reverse thumbnail URL mapping
+
+ Returns (original_url, generating_callback, callback_kwargs).
+ """
+ base,extension = _os_path.splitext(url)
+ if extension in ['.png']:
+ try:
+ root,width,height = base.rsplit('-', 2)
+ except ValueError:
+ raise HTTPError(404, 'missing width/height in {}'.format(base))
+ try:
+ width = int(width)
+ height = int(height)
+ except ValueError as e:
+ raise HTTPError(404, 'invalid width/height: {}'.format(e))
+ return (
+ root + '.jpg',
+ self._thumb,
+ {'max_width': width,
+ 'max_height': height},
+ )
+ elif extension in VIDEO_EXTENSIONS:
+ return (
+ base + '.mov',
+ getattr(self, '_{}'.format(extension), None),
+ {},
+ )
+ raise HTTPError(404, 'no original URL for {}'.format(url))
+
+ def _thumb(self, image, max_width=None, max_height=None):
+ if not _os_path.exists(self._cache_path):
+ _os.makedirs(self._cache_path)
+ dirname,filename = _os_path.split(image)
+ reldir = _os_path.relpath(dirname, self._base_path)
+ cache_dir = _os_path.join(self._cache_path, reldir)
+ if not _os_path.isdir(cache_dir):
+ _os.makedirs(cache_dir)
+ extension = '-{:d}-{:d}.png'.format(max_width, max_height)
+ thumb_filename = image_base(filename)+extension
+ thumb_url = _os_path.join(dirname, thumb_filename)
+ thumb_path = _os_path.join(cache_dir, thumb_filename)
+ image_path = _os_path.join(self._base_path, image)
+ if not _os_path.isfile(image_path):
+ raise HTTPError(404, 'image path for thumbnail does not exist')
+ if (not _os_path.isfile(thumb_path)
+ or _os_path.getmtime(image_path) > _os_path.getmtime(thumb_path)):
+ invoke(['convert', '-format', 'png', '-strip', '-quality', '95',
+ image_path,
+ '-thumbnail', '{:d}x{:d}'.format(max_width, max_height),
+ thumb_path])
+ return (thumb_path, self._url(thumb_url))
+
+ def _mp4(self, video, *args):
+ if not video.endswith('.mov'):
+ raise HTTPError(404, "can't translate {} to MPEGv4".format(video))
+ dirname,filename = _os_path.split(video)
+ mp4_filename = image_base(filename) + '.mp4'
+ reldir = _os_path.relpath(dirname, self._base_path)
+ cache_dir = _os_path.join(self._cache_path, reldir)
+ if not _os_path.isdir(cache_dir):
+ _os.makedirs(cache_dir)
+ mp4_url = _os_path.join(dirname, mp4_filename)
+ mp4_path = _os_path.join(cache_dir, mp4_filename)
+ if not _os_path.isfile(video):
+ raise HTTPError(404, 'source video path does not exist')
+ if (not _os_path.isfile(mp4_path)
+ or _os_path.getmtime(video) > _os_path.getmtime(mp4_path)):
+ arg = ['ffmpeg', '-i', video, '-acodec', 'libfaac', '-aq', '200',
+ '-ac', '1', '-s', '640x480', '-vcodec', 'libx264',
+ '-preset', 'slower', '-vpre', 'ipod640', '-b', '800k',
+ '-bt', '800k', '-aspect', '640:480', '-threads', '0']
+ arg.extend(args)
+ arg.append(mp4_path)
+ invoke(arg)
+ return (mp4_path, self._url(mp4_url))
+
+ def _ogv(self, video, *args):
+ if not video.endswith('.mov'):
+ LOG.error("can't translate {} to Ogg Video".format(video))
+ dirname,filename = _os_path.split(video)
+ ogv_filename = image_base(filename) + '.ogv'
+ reldir = _os_path.relpath(dirname, self._base_path)
+ cache_dir = _os_path.join(self._cache_path, reldir)
+ if not _os_path.isdir(cache_dir):
+ _os.makedirs(cache_dir)
+ ogv_url = _os_path.join(dirname, ogv_filename)
+ ogv_path = _os_path.join(cache_dir, ogv_filename)
+ if not _os_path.isfile(video):
+ LOG.error('source video path does not exist')
+ if (not _os_path.isfile(ogv_path)
+ or _os_path.getmtime(video) > _os_path.getmtime(ogv_path)):
+ arg = ['ffmpeg2theora', '--optimize']
+ arg.extend(args)
+ arg.extend(['--output', ogv_path, video])
+ invoke(arg)
+ return (ogv_path, self._url(ogv_url))
+
+ def _get_image_caption(self, path):
+ caption_path = path + '.txt'
+ try:
+ return open(caption_path, 'r').read()
+ except IOError:
+ return None
+
+ def _get_image_video(self, path, fallback=None):
+ base_path = image_base(path)
+ for extension in VIDEO_EXTENSIONS:
+ video_path = base_path + extension
+ if _os_path.isfile(video_path):
+ return self._video(video_path, fallback=fallback)
+ return None
+
+ def _captioned_video(self, path, href=None):
+ img = self._image(path, max_width=640, max_height=480)
+ caption = self._get_image_caption(path)
+ video = self._get_image_video(path, fallback=[img])
+ content = []
+ if video:
+ content.extend(video)
+ if href:
+ content.append('<p>{}</p>'.format(
+ self._link(path=href, text='gallery page')))
+ elif href:
+ content.append(self._link(path=href, text=img))
+ else:
+ content.append(img)
+ if caption:
+ caption = _xml_sax_saxutils.escape(caption)
+ content.append('<p>{}</p>'.format(caption))
+ return content
+
+ def _video(self, video, fallback=None, **kwargs):
+ if fallback is None:
+ fallback = [
+ '<p>Your browser does not support the <video> tag, try',
+ 'downloading the video and playing it in an external player.',
+ '</p>',
+ ]
+ fallback = [' '+line for line in fallback]
+ ogv_path,ogv_url = self._ogv(video)
+ mp4_path,mp4_url = self._mp4(video)
+ return [
+ '<p>',
+ (' <video preloads="none" controls="controls" '
+ 'width="640" height="480">'),
+ ' <source src="{}"'.format(mp4_url),
+ (''' type='video/mp4; '''
+ '''codecs="avc1.42E01E, mp4a.40.2"' />'''),
+ ' <source src="{}"'.format(ogv_url),
+ ''' type='video/ogg; codecs="theora,vorbis"' />''',
+ ] + fallback + [
+ ' </video>',
+ '</p>',
+ '<p>Download as',
+ ' <a href="{}">Ogg/Theora/Vorbis</a> or'.format(ogv_url),
+ (' <a href="{}">Mpeg4/H.264(ConstrainedBaselineProfile)/AAC</a>.'
+ ).format(mp4_url),
+ '<p>',
+ ]
+
+ def _image(self, image, **kwargs):
+ if kwargs:
+ image_path,image_url = self._thumb(image, **kwargs)
+ else:
+ image_url = image
+ sections = ['<img src="{}"'.format(image_url)]
+ caption = self._get_image_caption(path=image)
+ if caption:
+ caption = _xml_sax_saxutils.quoteattr(
+ caption.replace('\n', ' ').strip())
+ sections.extend([
+ 'title={}'.format(caption),
+ 'alt={}'.format(caption),
+ ])
+ sections.append('/>')
+ return ' '.join(sections)
+
+ def _image_page(self, image):
+ return image_base(image) + '/'
+
+ def random(self, url=None, stream=None, **kwargs):
+ LOG.debug('random image')
+ if url.endswith('/random'):
+ url = url[:(-len('/random'))]
+ self.validate_url(url=url, directory=True, stream=stream)
+ base_dir = _os_path.join(self._base_path, url)
+ elif url == 'random':
+ base_dir = self._base_path
+ else:
+ raise HTTPError(404)
+ images = []
+ for dirpath,dirnames,filenames in _os.walk(base_dir):
+ for filename in filenames:
+ if is_image(filename):
+ images.append(_os_path.join(dirpath, filename))
+ if not images:
+ self._response(content='<p>no images to choose from</p>',
+ stream=stream)
+ image = _random.choice(images)
+ LOG.debug('selected random image {}'.format(image))
+ page = self._image_page(image)
+ content = self._captioned_video(path=image, href=page)
+ self._response(content='\n'.join(content), stream=stream)
+
+ def is_cacheable(self, url):
+ return is_image(url) or is_video(url)
+
+ def cached(self, url, stream=None):
+ LOG.debug('retrieving possibly cached item')
+ mime = _mimetypes.guess_type(url)[0]
+ if mime is None:
+ raise HTTPError(404, 'unknown mime type for {}'.format(url))
+ cache_path = _os_path.join(self._cache_path, url)
+ original_path = _os_path.join(self._base_path, url)
+ path = None
+ if _os_path.isfile(cache_path):
+ LOG.debug('return cached item {}'.format(cache_path))
+ path = cache_path
+ elif self._serve_originals and _os_path.isfile(original_path):
+ LOG.debug('return original item {}'.format(original_path))
+ path = original_path
+ else:
+ LOG.debug('possibly create cached item {}'.format(cache_path))
+ original_url,callback,kwargs = self._original_url(url)
+ original_path = _os_path.join(self._base_path, original_url)
+ if callback and _os_path.isfile(original_path):
+ path,cache_url = callback(original_path, **kwargs)
+ if not path:
+ raise HTTPError(404)
+ try:
+ content = open(path, 'rb')
+ except IOError as e:
+ LOG.error(e)
+ raise HTTPError(404, 'item not found {}'.format(url))
+ header = self._http_header(mime=mime)
+ if mime in STREAMING_TYPES:
+ self._response_stream(
+ header=header, content=content, stream=stream)
+ content = content.read()
+ self._response(header=header, content=content, stream=stream)
+
+ def page(self, url, page=0, stream=None):
+ LOG.debug('HTML page {} {}'.format(url, page))
+ if not url.endswith('/'):
+ raise HTTPError(404, 'HTML page URLs must end with a slash')
+ abspath = _os_path.join(self._base_path, url)
+ if _os_path.isdir(abspath):
+ self._directory(path=abspath, page=page, stream=stream)
+ for extension in IMAGE_EXTENSIONS:
+ file_path = abspath[:-1] + extension
+ if _os_path.isfile(file_path):
+ self._page(path=file_path, stream=stream)
+ raise HTTPError(404, 'unknown HTML page {}'.format(url))
+
+ def _directory_header(self, path):
+ relpath = _os_path.relpath(path, self._base_path)
+ crumbs = []
+ dirname = relpath
+ while dirname:
+ dirname,base = _os_path.split(dirname)
+ if base != '.':
+ crumbs.insert(0, base)
+ crumbs.insert(0, '')
+ links = [None] * len(crumbs)
+ for i,c in enumerate(crumbs):
+ if i < len(crumbs)-1:
+ if i == 0:
+ links[i] = self._link(self._base_path, 'Gallery')
+ else:
+ relpath = '/'.join(crumbs[1:i+1]) + '/'
+ fullpath = _os_path.join(self._base_path, relpath)
+ links[i] = self._link(path=fullpath)
else:
- prev_html = ''
- if i + rows*cols < len(pic_thumbs):
- next_url = gallery % (gallery_i+1)
- next_html = '<a href="./%s">next</a>\n' % next_url
+ if i == 0:
+ links[i] = 'Gallery'
+ else:
+ links[i] = self._label(crumbs[i])
+ content = ['<h1>{}</h1>'.format(' '.join(links))]
+ return content
+
+ def _directory_page_navigation(self, path, page, pages):
+ if pages <= 1:
+ return []
+ prev_page = path + '?pp={:d}'.format((page - 1) % pages + 1)
+ next_page = path + '?pp={:d}'.format((page + 1) % pages + 1)
+ return [
+ '<div style="text-align: center;">',
+ '<p>',
+ self._link(prev_page, 'previous'),
+ '({:d} of {:d})'.format(page+1, pages),
+ self._link(next_page, 'next'),
+ '</p>',
+ '</div>',
+ ]
+
+ def _directory_subdirs(self, path):
+ content = []
+ dirs = list(self._subdirs(path))
+ if dirs:
+ content.append('<ul>')
+ for d in dirs:
+ content.append(' <li>{}</li>'.format(self._link(d+'/')))
+ content.append('</ul>')
+ return content
+
+ def _directory_images(self, path, images):
+ content = ['<table style="margin-left: auto; margin-right: auto;">']
+ column = 0
+ for image in images:
+ page = self._image_page(image)
+ img = self._image(image, max_width=300, max_height=300)
+ link = self._link(page, img)
+ if column == 0:
+ content.append(' <tr>')
+ content.extend([
+ ' <td style="text-align: center;">',
+ ' {}'.format(link),
+ ' </td>',
+ ])
+ column += 1
+ if column == self._columns:
+ content.append(' </tr>')
+ column = 0
+ if column != 0:
+ #content.extend()
+ content.append(' </tr>')
+ content.append('</table>')
+ return content
+
+ def _directory(self, path, page=0, stream=None):
+ LOG.debug('directory page {} {}'.format(path, page))
+ images = list(self._images(path))
+ images_per_page = self._rows * self._columns
+ pages = int(_math.ceil(float(len(images)) / images_per_page)) or 1
+ if page < 0 or page >= pages:
+ raise HTTPError(
+ 404,
+ 'page out of bounds for this gallery 0 <= {:d} < {:d}'.format(
+ page, pages))
+ first_image = images_per_page * page
+ images = images[first_image:first_image+images_per_page]
+ content = []
+ content.extend(self.header)
+ content.extend(self._directory_header(path))
+ nav = self._directory_page_navigation(path, page=page, pages=pages)
+ content.extend(nav)
+ content.extend(self._directory_subdirs(path))
+ content.extend(self._directory_images(path, images=images))
+ content.extend(nav)
+ content.extend(self.footer)
+ self._response(content='\n'.join(content), stream=stream)
+
+ def _page(self, path, stream=None):
+ LOG.debug('image page {}'.format(path))
+ gallery = _os_path.dirname(path)
+ images = list(self._images(gallery))
+ images_per_page = self._rows * self._columns
+ i = images.index(path)
+ page = i / images_per_page
+ gallery_page = '{}/?pp={:d}'.format(gallery, page + 1)
+ prev_page = self._image_page(images[i - 1])
+ next_page = self._image_page(images[(i + 1) % len(images)])
+ content = []
+ content.extend(self.header)
+ content.extend([
+ '<div style="text-align: center;">',
+ '<p>',
+ self._link(prev_page, 'previous'),
+ self._link(gallery_page, 'all'),
+ self._link(next_page, 'next'),
+ '</p>',
+ ])
+ content.extend(self._captioned_video(path))
+ content.append('</div>')
+ content.extend(self.footer)
+ self._response(content='\n'.join(content), stream=stream)
+
+
+def serve_cgi(server):
+ import cgi
+ import cgitb
+ import sys
+
+ url=None
+ cgitb.enable()
+ #cgitb.enable(display=0, logdir="/tmp/")
+ data = cgi.FieldStorage()
+ page = server.page_from_query(
+ query={key: data[key].getlist() for key in data.keys()})
+ if 'p' in data:
+ p = data['p']
+ if isinstance(p, list):
+ p = p[0]
+ url = p.value
+ server.serve(url=url, page=page, stream=sys.stdout)
+
+def serve_scgi(server, host='localhost', port=4000):
+ import scgi
+ import scgi.scgi_server
+
+ class GalleryHandler(scgi.scgi_server.SCGIHandler):
+ def produce(self, env, bodysize, input, output):
+ #LOG.info(HTTP_USER_AGENT REQUEST_METHOD REMOTE_ADDR REQUEST_URI
+ url = env.get('DOCUMENT_URI', None)
+ page = server.page_from_query(
+ query_string=env.get('QUERY_STRING', ''))
+ try:
+ try:
+ url = server.relative_url(url=url)
+ except HTTPError as e:
+ LOG.error(e.message)
+ server._error(e.status, content=e.content, stream=output)
+ except ProcessingComplete:
+ pass
else:
- next_html = ''
- g.write('<div style="align: center; text-align: center;">\n')
- g.write('<p>%s%s%s</p>\n' % (prev_html, up_html, next_html))
- g.write('<table>\n')
- column = 0
- row = 0
- LOG.info('placing picture %d of %d' % (i+1, len(pic_thumbs)))
- pic,thumb = pic_thumbs[i]
- prev = next = None
- if i > 0:
- prev = pics[i-1]
- if i+1 < len(pics):
- next = pics[i+1]
- page = make_page(picdir, gallery % gallery_i, pagedir, pic, prev, next,
- force=force)
- if column == 0:
- g.write(' <tr>\n')
- g.write(' <td style="text-align: center">\n')
- g.write(' <a href="%s" style="border: 0">\n' % page)
- g.write(' <img alt="%s" src="%s"\n' % (pic, thumb))
- g.write(' </a>\n')
- g.write(' </td>\n')
- column += 1
- if column == cols:
- g.write(' </tr>\n')
- column = 0
- row += 1
- if row == rows or i+1 == len(pic_thumbs):
- g.write('</table>\n')
- g.write('</div>\n')
- g.write(gallery_footer(gallery_i))
- g.close()
- g = None
- gallery_i += 1
- i += 1
- if i > 0 and not os.path.exists(index):
- os.symlink(gallery % 1, index)
+ server.serve(url=url, page=page, stream=output)
+
+ s = scgi.scgi_server.SCGIServer(
+ handler_class=GalleryHandler, host=host, port=port)
+ LOG.info('serving SCGI on {}:{}'.format(host, port))
+ s.serve()
if __name__ == '__main__':
- import optparse
- parser = optparse.OptionParser(usage='%prog [options] PICTURE-DIR ...',
- epilog=__doc__)
- parser.format_epilog = lambda formatter : __doc__
- parser.add_option('--index', default='index.html', dest='index',
- help='Name of the index page (symlinked to <GALLERY-1> (%default)')
- parser.add_option('--gallery', default='gallery-%d.html', dest='gallery',
- help='Name of the gallery page, must include a %%d. (%default)')
- parser.add_option('--up-link', default=None, dest='up_link',
- help='Text for link to gallery parent')
- parser.add_option('--pagedir', default='./pages', dest='pagedir',
- help='Relative path from gallery page to page directory (%default)')
- parser.add_option('--thumbdir', default='./thumbs', dest='thumbdir',
- help='Relative path from gallery page to thumbnail directory (%default)')
- parser.add_option('-r', '--rows', default=4, type='int', dest='cols',
- help='Rows of thumbnails per page (%default)')
- parser.add_option('-c', '--cols', default=10, type='int', dest='rows',
- help='Columns of thumbnails per page (%default)')
- parser.add_option('-H', '--height', default=100, type='int', dest='height',
- help='Maximum thumbnail height in pixels (%default)')
- parser.add_option('-W', '--width', default=300, type='int', dest='width',
- help='Maximum thumbnail width in pixels (%default)')
- parser.add_option('--force', default=False, dest='force',
- help='Regenerate existing gallery files', action='store_true')
- parser.add_option('-v', '--verbose', default=0, dest='verbose',
- help='Increment verbosity', action='count')
- options,args = parser.parse_args()
-
- logging.basicConfig(level=level)
- levels =
- level = [
- logging.WARNING,
- logging.INFO,
- logging.DEBUG][options.verbose]
-
- for picdir in args:
- kwargs = {}
- for attr in ['index', 'gallery', 'up_link', 'pagedir', 'thumbdir',
- 'cols', 'rows', 'height', 'width', 'force']:
- kwargs[attr] = getattr(options, attr)
- make_gallery(picdir, **kwargs)
+ import argparse as _argparse
+
+ parser = _argparse.ArgumentParser(
+ description=__doc__, version=__version__,
+ formatter_class=_argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ '--scgi', default=False, action='store_const', const=True,
+ help='Run as a SCGI server (vs. serving a single CGI call)')
+ parser.add_argument(
+ '--port', default=4000, type=int,
+ help='Port to listen to (if runing as a SCGI server)')
+ parser.add_argument(
+ '--base-path', default='.',
+ help='Path to the root gallery source')
+ parser.add_argument(
+ '--base-url', default='/',
+ help='URL for the root gallery source')
+ parser.add_argument(
+ '--shared-path', default=None,
+ help=('Optional path to the shared directory containing '
+ '`header.shtml` and `footer.shtml`'))
+ parser.add_argument(
+ '--cache-path', default='/tmp/gallery-cache',
+ help='Path to the thumbnail and movie cache directory')
+
+ args = parser.parse_args()
+
+ s = CGIGalleryServer(
+ base_path=args.base_path, base_url=args.base_url,
+ cache_path=args.cache_path)
+ if args.shared_path:
+ shared = args.shared_path
+ s.header = [open(_os_path.join(shared, 'header.shtml'), 'r').read()]
+ s.footer = [open(_os_path.join(shared, 'footer.shtml'), 'r').read()]
+
+ if args.scgi:
+ serve_scgi(server=s, port=args.port)
+ else:
+ serve_cgi(server=s)