initial code, somewhere between 0.2 and 0.5
authorterom
Thu, 08 Nov 2007 17:06:03 +0000
changeset 1 740133ab6353
parent 0 5dbdcb79024b
child 2 8b2b40a51098
initial code, somewhere between 0.2 and 0.5
degal.py
series.cgi
shorturl.cgi
style.css
templates/gallery.html
templates/gallery.html
templates/image.html
templates/image.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal.py	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,685 @@
+#!/usr/bin/env python2.4
+#
+# DeGAL - A pretty simple web image gallery
+# Copyright (C) 2007 Tero Marttila
+# http://marttila.de/~terom/degal/
+#
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# 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.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+
+import os.path, os
+import urllib
+import logging
+import string
+from datetime import datetime
+import struct
+import base64
+import shelve
+
+import PIL
+import PIL.Image
+
+import utils
+
+__version__ = '0.2'
+
+TEMPLATE_DIR='templates'
+TEMPLATE_EXT='html'
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(name)8s %(levelname)8s   %(lineno)3d %(message)s",
+
+)
+
+tpl = logging.getLogger('tpl')
+index = logging.getLogger('index')
+prepare = logging.getLogger('prepare')
+render = logging.getLogger('render')
+
+tpl.setLevel(logging.WARNING)
+
+#for l in (tpl, index, prepare, render) :
+#    l.setLevel(logging.DEBUG)
+
+def readFile (path) :
+    fo = open(path, 'r')
+    data = fo.read()
+    fo.close()
+
+    return data
+
+class Template (object) :
+    GLOBALS = dict(
+        VERSION=__version__,
+    )
+
+    def __init__ (self, name) :
+        self.name = name
+        self.path = os.path.join(TEMPLATE_DIR, "%s.%s" % (name, TEMPLATE_EXT))
+
+        tpl.debug("Template %s at %s", name, self.path)
+
+        self.content = readFile(self.path)
+
+    def render (self, **vars) :
+        content = self.content
+        
+        vars.update(self.GLOBALS)
+
+        for name, value in vars.iteritems() :
+            content = content.replace('<!-- %s -->' % name, str(value))
+
+        return content
+
+    def renderTo (self, path, **vars) :
+        tpl.info("Render %s to %s", self.name, path)
+
+        fo = open(path, 'w')
+        fo.write(self.render(**vars))
+        fo.close()
+
+gallery_tpl = Template('gallery')
+image_tpl = Template('image')
+
+IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
+
+THUMB_DIR = 'thumbs'
+PREVIEW_DIR = 'previews'
+TITLE_FILE = 'title.txt'
+
+THUMB_GEOM = (160, 120)
+PREVIEW_GEOM = (640, 480)
+
+DEFAULT_TITLE = 'Image gallery'
+
+def isImage (fname) :
+    """
+        Is the given filename likely to be an image file?
+    """
+
+    fname = fname.lower()
+    base, ext = os.path.splitext(fname)
+    ext = ext.lstrip('.')
+
+    return ext in IMAGE_EXTS
+
+def link (url, title) :
+    """
+        Returns an <a href=""></a> link tag with the given values
+    """
+
+    return "<a href='%s'>%s</a>" % (urllib.quote(url), title)
+
+def dirUp (count=1) :
+    """
+        Returns a relative path to the directly count levels above the current one
+    """
+
+    if not count :
+        return '.'
+
+    return os.path.join(*(['..']*count))
+
+def readTitleDescr (path) :
+    """
+        Read a title.txt or <imgname>.txt file
+    """
+
+    if os.path.exists(path) :
+        content = readFile(path)
+
+        if '---' in content :
+            title, descr = content.split('---', 1)
+        else :
+            title, descr = content, ''
+
+        return title.strip(), descr.strip()
+
+    return None, None
+
+class Folder (object) :
+    def __init__ (self, name='.', parent=None) :
+        # the directory name
+        self.name = name
+
+        # our parent Folder, or None
+        self.parent = parent
+
+        # the path to this dir, as a relative path to the root of the image gallery, always starts with .
+        if parent and name :
+            self.path = parent.pathFor(name)
+        else :
+            self.path = name
+
+        # the url-path to the index.html file
+        self.html_path = self.path
+        
+        # dict of fname -> Folder
+        self.subdirs = {}
+
+        # dict of fname -> Image
+        self.images = {}
+        
+        # our human-friendly title
+        self.title = None
+
+        # our long-winded description
+        self.descr = ''
+
+        # is this folder non-empty?
+        self.alive = None
+        
+        # self.images.values(), but sorted by filename
+        self.sorted_images = []
+        
+        # the ShortURL key to this dir
+        self.shorturl_code = None
+
+        # were we filtered out?
+        self.filtered = False
+   
+    def pathFor (self, *fnames) :
+        """
+            Return a root-relative path to the given path inside this dir
+        """
+        return os.path.join(self.path, *fnames)
+
+    def index (self, filters=None) :
+        """
+            Look for other dirs and images inside this dir. Filters must be either None,
+            whereupon all files will be included, or a dict of {filename -> next_filter}.
+            If given, only filenames that are present in the dict will be indexed, and in
+            the case of dirs, the next_filter will be passed on to that Folder's index
+            method.
+        """
+
+        index.info("Indexing %s", self.path)
+
+        if filters :
+            self.filtered = True
+        
+        # iterate through listdir
+        for fname in os.listdir(self.path) :
+            # the full filesystem path to it
+            fpath = self.pathFor(fname)
+            
+            # ignore dotfiles
+            if fname.startswith('.') :
+                index.debug("Skipping dotfile %s", fname)
+                continue
+            
+            # apply filters
+            if filters :
+                if fname in filters :
+                    next_filter = filters[fname]
+                else :
+                    index.debug("Skip `%s' as we have a filter", fname)
+                    continue
+            else :
+                next_filter = None
+                
+            # recurse into subdirs, but not thumbs/previews
+            if os.path.isdir(fpath) and fname not in (THUMB_DIR, PREVIEW_DIR) :
+                index.debug("Found subdir %s", fpath)
+                f = self.subdirs[fname] = Folder(fname, self)
+                f.index(next_filter)   # recursion
+
+            # handle images
+            elif os.path.isfile(fpath) and isImage(fname) :
+                index.debug("Found image %s", fname)
+                self.images[fname] = Image(self, fname)
+
+            # ignore everything else
+            else :
+                index.debug("Ignoring file %s", fname)
+
+    def prepare (self) :
+        """
+            Prepare the dir, i.e. sort+prepare the images, as well as recurse
+            into subdirs
+        """
+
+        prepare.info("Preparing dir %s", self.path)
+        
+        # is this folder non-empty?
+        alive = False
+
+        # only create thumbs/previews dirs if we have images in here
+        if self.images :
+            # folder is non-empty
+            alive = True
+            prepare.info("Have %d images", len(self.images))
+            
+            # create the thumb/preview dirs if needed
+            for dir in (THUMB_DIR, PREVIEW_DIR) :
+                prepare.debug("Checking for existance of %s dir", dir)
+                path = self.pathFor(dir)
+
+                if not os.path.isdir(path) :
+                    prepare.info("Creating dir %s", path)
+                    os.mkdir(path)
+            
+            # sort the images
+            fnames = self.images.keys()
+            fnames.sort()
+            
+            prev = None
+            
+            # link them together and prepare them
+            for fname in fnames :
+                img = self.images[fname]
+
+                prepare.debug("Linking %s <-> %s", prev, img)
+
+                img.prev = prev
+
+                if prev :
+                    prev.next = img
+
+                prev = img
+                
+                img.prepare()
+                
+                # add to the sorted images list
+                self.sorted_images.append(img)
+        
+        # prepare subdirs
+        if self.subdirs :
+            prepare.info("Have %d subdirs", len(self.subdirs))
+            
+            # just recurse into them, we're alive if one of them is
+            for dir in self.subdirs.itervalues() :
+                if dir.prepare() :
+                    alive = True
+
+        # figure out our title
+        title_path = self.pathFor(TITLE_FILE)
+        
+        title, descr = readTitleDescr(title_path)
+
+        if title :
+            prepare.info("Found title/descr")
+            self.title = title
+            self.descr = descr
+            alive = True
+        
+        # default title for the root dir
+        elif self.name == '.' :
+            self.title = 'Index'
+
+        else :
+            self.title = self.name
+
+        prepare.debug("Our title is '%s'", self.title)
+        
+        # lol ded
+        if not alive :
+            prepare.info("Dir %s is not alive", self.path)
+
+        self.alive = alive
+
+        return alive
+
+    def getObjInfo (self) :
+        """
+            Metadata for shorturls2.db
+        """
+        return 'dir', self.path, ''
+
+    def linkTag (self) :
+        """
+            A text-link to this dir
+        """
+
+        return link(self.path, self.title)
+
+    def breadcrumb (self) :
+        """
+            Returns a [(fname, title)] list of this dir's parent dirs
+        """
+
+        f = self
+        b = []
+        d = 0
+        
+        while f :
+            b.insert(0, (dirUp(d), f.title))
+
+            d += 1
+            f = f.parent
+        
+        return b
+
+    def inRoot (self, *fnames) :
+        """
+            Return a relative URL from this dir to the given path in the root dir
+        """
+
+        c = len(self.path.split('/')) - 1
+
+        return os.path.join(*((['..']*c) + list(fnames)))
+
+    def render (self) :
+        """
+            Render the index.html, Images, and recurse into subdirs
+        """
+        
+        # ded folders are skipped
+        if not self.alive :
+            render.info("Skipping dir %s", self.path)
+            return
+        
+        # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here
+        if self.filtered :
+            render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path)
+
+        else :
+            # sort the subdirs
+            subdirs = self.subdirs.items()
+            subdirs.sort()
+            
+            # generate the <a href=""></a>'s for the subdirs
+            subdir_linkTags = [link(f.name, f.title) for fname, f in subdirs if f.alive]
+            
+            # stick them into a list
+            if subdir_linkTags :
+                directories = "<ul>\n\t<li>%s</li>\n</ul>" % "</li>\n\t<li>".join(subdir_linkTags)
+            else :
+                directories = ''
+
+            render.info("Rendering %s", self.path)
+            
+            # render to index.html
+            gallery_tpl.renderTo(self.pathFor('index.html'), 
+                STYLE_URL=self.inRoot('style.css'),
+                BREADCRUMB=" &raquo; ".join([link(u, t) for (u, t) in self.breadcrumb()]),
+                TITLE=self.title,
+                DIRECTORIES=directories,
+                CONTENT="".join([i.thumbImgTag() for i in self.sorted_images]),
+                DESCR=self.descr,
+                SHORTURL=self.inRoot('s', self.shorturl_code),
+                SHORTURL_CODE=self.shorturl_code,
+            )
+        
+        # render images
+        for img in self.images.itervalues() :
+            img.render()
+        
+        # recurse into subdirs
+        for dir in self.subdirs.itervalues() :
+            dir.render()
+                    
+class Image (object) :
+    def __init__ (self, dir, name) :
+        # the image filename, e.g. DSC3948.JPG
+        self.name = name
+
+        # the Folder object that we are in
+        self.dir = dir
+        
+        # the relative path from the root to us
+        self.path = dir.pathFor(name)
+
+        # the basename+ext, e.g. DSCR3948, .JPG
+        self.base_name, self.ext = os.path.splitext(name)
+        
+        # the root-relative paths to the thumb and preview images
+        self.thumb_path = self.dir.pathFor(THUMB_DIR, self.name)
+        self.preview_path = self.dir.pathFor(PREVIEW_DIR, self.name)
+        
+        # our user-friendly title
+        self.title = name
+
+        # our long-winded description
+        self.descr = ''
+
+        # the image before and after us, both may be None
+        self.prev = self.next = None
+        
+        # the name of the .html gallery view thing for this image, *always* self.name + ".html"
+        self.html_name = self.name + ".html"
+
+        # the root-relative path to the gallery view
+        self.html_path = self.dir.pathFor(self.html_name)
+        
+        #
+        # Figured out after prepare
+        #
+
+        # (w, h) tuple
+        self.img_size = None
+        
+        # the ShortURL code for this image
+        self.shorturl_code = None
+
+        # what to use in the rendered templates, intended to be overridden by subclasses
+        self.series_act = "add"
+        self.series_verb = "Add to"
+    
+    def prepare (self) :
+        """
+            Generate the thumbnail/preview views if needed, get the image info, and look for the title
+        """
+
+        prepare.info("Preparing image %s", self.path)
+
+        # stat the image file to get the filesize and mtime
+        st = os.stat(self.path)
+
+        self.filesize = st.st_size
+        self.timestamp = st.st_mtime
+        
+        # open the image in PIL to get image attributes + generate thumbnails
+        img = PIL.Image.open(self.path)
+
+        self.img_size = img.size
+
+        for out_path, geom in ((self.thumb_path, THUMB_GEOM), (self.preview_path, PREVIEW_GEOM)) :
+            # if it doesn't exist, or it's older than the image itself, generate
+            if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) :
+                prepare.info("Create thumbnailed image at %s with geom %s", out_path, geom)
+                
+                # XXX: is this the most efficient way to do this?
+                out_img = img.copy()
+                out_img.thumbnail(geom, resample=True)
+                out_img.save(out_path)
+        
+        # look for the metadata file
+        title_path = self.dir.pathFor(self.base_name + '.txt')
+        prepare.debug("Looking for title at %s", title_path)
+        
+        title, descr = readTitleDescr(title_path)
+
+        if title :
+            self.title = title
+            self.descr = descr
+
+            prepare.info("Found title `%s'", self.title)
+    
+    def getObjInfo (self) :
+        """
+            Metadata for shorturl2.db
+        """
+        return 'img', self.dir.path, self.name
+
+    def thumbImgTag (self) :
+        """
+            a <a><img /></a> of this image's thumbnail. Path relative to directory we are in
+        """
+        return link(self.html_name, "<img src='%s' alt='%s' title='%s'>" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title))
+
+    def previewImgTag (self) :
+        """
+            a <a><img /></a> of this image's preview. Path relative to directory we are in
+        """
+        return link(self.name, "<img src='%s' alt='%s' title='%s'>" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title))
+
+    def linkTag (self) :
+        """
+            a <a></a> text-link to this image
+        """
+        return link(self.html_name, self.title)
+
+    def breadcrumb (self) :
+        """
+            Returns a [(fname, title)] list of this image's parents
+        """
+
+        f = self.dir
+        b = [(self.html_name, self.title)]
+        d = 0
+        
+        while f :
+            b.insert(0, (dirUp(d), f.title))
+
+            d += 1
+            f = f.parent
+        
+        return b
+
+    def render (self) :
+        """
+            Write out the .html file
+        """
+
+        render.info("Rendering image %s", self.path)
+
+        image_tpl.renderTo(self.html_path,
+            STYLE_URL=self.dir.inRoot('style.css'),
+            UP_URL=('.'),
+            PREV_URL=(self.prev and self.prev.html_name or ''),
+            NEXT_URL=(self.next and self.next.html_name or ''),
+            FILE=self.name,
+            BREADCRUMB=" &raquo; ".join([link(u, t) for u, t in self.breadcrumb()]),
+            TITLE=self.title,
+            PREVIOUS_THUMB=(self.prev and self.prev.thumbImgTag() or ''),
+            IMAGE=self.previewImgTag(),
+            NEXT_THUMB=(self.next and self.next.thumbImgTag() or ''),
+            DESCRIPTION=self.descr,
+            IMGSIZE="%dx%d" % self.img_size,
+            FILESIZE=fmtFilesize(self.filesize),
+            TIMESTAMP=fmtTimestamp(self.timestamp),
+            SHORTURL=self.dir.inRoot('s', self.shorturl_code),
+            SHORTURL_CODE=self.shorturl_code,
+            SERIES_URL=self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)),
+            SERIES_VERB=self.series_verb,
+        )   
+    
+    def __str__ (self) :
+        return "Image `%s' in `%s'" % (self.name, self.dir.path)
+
+def int2key (id) :
+    """
+        Turn an integer into a short-as-possible url-safe string
+    """
+    for type in ('B', 'H', 'I') :
+        try :
+            return base64.b64encode(struct.pack(type, id), '-_').rstrip('=')
+        except struct.error :
+            continue
+
+    raise Exception("ID overflow: %s" % id)
+
+def updateShorturlDb (root) :
+    """
+        DeGAL <= 0.2 used a simple key => path mapping, but now we use
+        something more structured, key => (type, dirpath, fname), where
+
+        type    - one of 'img', 'dir'
+        dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux'
+        fname   - the filename, one of '', 'DSC9839.JPG', 'this.png', etc.
+    """
+
+    db = shelve.open('shorturls2', 'c')
+    
+    id = db.get('_id', 1)
+
+    dirqueue = [root]
+
+    # dict of path -> obj
+    paths = {}
+
+    while dirqueue :
+        dir = dirqueue.pop(0)
+
+        dirqueue.extend(dir.subdirs.itervalues())
+
+        if dir.alive :
+            paths[dir.path] = dir
+
+        for img in dir.images.itervalues() :
+            paths[img.html_path] = img
+
+    for key in db.keys() :
+        if key.startswith('_') :
+            continue
+
+        type, dirpath, fname = db[key]
+        
+        path = os.path.join(dirpath, fname)
+
+        try :
+            paths.pop(path).shorturl_code = key
+            index.debug("Code for `%s' is %s", path, key)
+
+        except KeyError :
+            index.debug("Path `%s' in DB does not exist?", path)
+
+    for obj in paths.itervalues() :
+        key = int2key(id)
+        id += 1
+        
+        index.info("Alloc code `%s' for `%s'", key, obj.html_path)
+
+        obj.shorturl_code = key
+
+        db[key] = obj.getObjInfo()
+
+    db['_id'] = id
+    db.close()
+
+def main (targets=()) :
+    root_filter = {}
+
+    for target in targets :
+        f = root_filter
+        for path_part in target.split('/') :
+            if path_part :
+                if path_part not in f :
+                    f[path_part] = {}
+                f = f[path_part]
+    
+    index.debug('Filter: %s', root_filter)
+
+    root = Folder()
+    root.index(root_filter)
+    root.prepare()
+    updateShorturlDb(root)
+    root.render()
+
+def fmtFilesize (size) :
+    return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='K', meganame='M', bytename='B', nospace=True)
+
+def fmtTimestamp (ts) :
+    return datetime.fromtimestamp(ts).strftime("%Y/%m/%d %H:%M")
+
+if __name__ == '__main__' :
+    from sys import argv
+    argv.pop(0)
+
+    main(argv)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/series.cgi	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,182 @@
+#!/usr/bin/env python2.4
+
+import dbm
+import cgi
+import Cookie
+import os, os.path
+#import pprint
+
+import degal
+
+#
+# load request params
+#
+vars = cgi.FieldStorage()
+
+# keys = what photos are in the series
+keys = vars["keys"].value.split()
+
+# index = what photo to show, or the index
+if 'index' in vars :
+    index = vars["index"].value
+else :
+    index = None
+
+if keys and keys[0] in ('add', 'del', 'clear', 'view') or index == 'load' :
+    cookie = Cookie.SimpleCookie(os.environ.get('HTTP_COOKIE', None))
+    act = keys[0]
+    _keys = keys
+
+    if 'series' in cookie :
+        keys = cookie["series"].value.split()
+    else :
+        keys = []
+    
+    if index == 'load' :
+        keys = _keys
+    elif act == 'add' and index not in keys :
+        keys.append(index)
+    elif act == 'del' :
+        keys.remove(index)
+    elif act == 'clear' :
+        keys = []
+    elif act == 'view' :
+        pass
+    
+    cookie['series'] = ' '.join(keys)
+    cookie['series']['path'] = '/'
+
+    if keys :
+        redirect_to = "../%s/" % ('+'.join(keys))
+    else :
+        redirect_to = "../.."
+
+    print "Status: 302"
+    print "Location: %s" % redirect_to
+    print cookie
+    print
+    print "Redirect..."
+else :
+    cookie = Cookie.SimpleCookie(os.environ.get('HTTP_COOKIE', None))
+
+    my_series = 'series' in cookie and cookie['series'].value.split() == keys 
+        
+    if index :
+        index = int(index)
+
+    #
+    # load DB
+    #
+    db = dbm.open('shorturls', 'r')
+
+    #
+    # get the Image objects
+    #
+    photos = []
+
+    INDEX_URL = "/series/%s/"
+    IMAGE_URL = "/series/%s/%d"
+
+    # monkey-patch the templates
+    rendered_templates = []
+
+    def _myRenderTo (self, path, **vars) :
+        rendered_templates.append(self.render(**vars))
+
+    degal.Template.renderTo = _myRenderTo
+
+    # our own version of Folder
+    class Series (degal.Folder) :
+        def __init__ (self) :
+            super(Series, self).__init__()
+            
+            self.alive = True
+            self.filtered = False
+            self.subdirs = {}
+            self.images = {}
+            self.sorted_images = []
+            self.title = "Series"
+
+            if my_series :
+                self.descr = '<a href="../clear/">Clear your series</a>'
+            else :
+                self.descr = '<a href="load">Load as your series</a>'
+
+            self.shorturl_code = ''
+        
+        def breadcrumb (self) :
+            return [('../..', 'Gallery'), ('.', 'Series')]
+
+        def inRoot (self, *fnames) :
+            return os.path.join('../..', *fnames)
+
+    #    def pathFor (self, *fnames) :
+    #        return os.path.join(fnames)
+
+    class Image (degal.Image) :
+        def __init__ (self, series, key, i) :
+            path = db[key]
+            path = path.rstrip(".html")
+            self.dir_name, self.image_name = os.path.split(path)
+
+            super(Image, self).__init__(series, self.image_name)
+            
+            self.path = path
+            self.shorturl_code = key
+            self.html_path = self.html_name = str(i + 1)
+            self.title = 'Image %d' % (i + 1, )
+            self.descr = '<span style="font-size: x-small">Standalone image: <a href="%s.html">%s</a></span>' % (self._path(), self._path().lstrip('./'))
+            self.img_size = (-1, -1)
+            self.filesize = 0
+            self.timestamp = 0
+
+            if my_series :
+                self.series_act = "del"
+                self.series_verb = "Remove from"
+            else :
+                self.series_act = self.series_verb = ""
+
+        def breadcrumb (self) :
+            return [('../..', 'Gallery'), ('.', 'Series'), (self.html_name, self.title)]
+
+        def _path (self, blaa='') :
+            return os.path.join('../..', self.dir_name, blaa, self.image_name)
+
+        def thumbImgTag (self) :
+            return degal.link(self.html_name, "<img src='%s' alt='%s' title='%s'>" % (os.path.join('../..', self.dir_name, degal.THUMB_DIR, self.image_name), '', self.title))
+
+        def previewImgTag (self) :
+            return degal.link(os.path.join('../..', self.dir_name, self.image_name), "<img src='%s' alt='%s' title='%s'>" % (os.path.join('../..', self.dir_name, degal.PREVIEW_DIR, self.image_name), '', self.title))
+
+           
+
+    series = Series()
+
+    try :
+        prev = None
+
+        for i, key in enumerate(keys) :
+            img = Image(series, key, i)
+
+            if prev :
+                prev.next = img
+
+            img.prev = prev
+            prev = img
+
+    #        series.images[str(i + 1)] = img
+            series.sorted_images.append(img)
+    finally :
+        db.close()
+
+    if index :
+        img = series.sorted_images[index - 1]
+
+        img.render()
+    else :
+        series.render()
+
+    print "Content-Type: text/html"
+    print
+    print rendered_templates[0]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/shorturl.cgi	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,22 @@
+#!/usr/bin/env python2.5
+
+import dbm
+import cgi
+import os, os.path
+#import pprint
+
+vars = cgi.FieldStorage()
+
+key = vars['key'].value
+db = dbm.open('shorturls', 'r')
+
+try :
+    path = db[key]
+finally :
+    db.close()
+
+print "Status: 302"
+print "Location: ../%s" % path
+print
+print "../%s" % path
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/style.css	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,57 @@
+body {
+	background-color: #333333;
+	color: #cccccc;
+	font-family: "Arial", sans-serif;
+	font-size: small;
+}
+
+a {
+	color: #ff8800;
+	text-decoration: none;
+}
+
+a:hover {
+	text-decoration: underline;
+}
+
+#thumbnails, #image, #description, h1 {
+	text-align: center;
+}
+
+#thumbnails img {
+	margin: 0.2em;
+}
+
+img {
+	border: 1px solid #666666;
+}
+
+a:focus img {
+	border: 1px solid #cccccc;
+}
+
+img:hover, a:focus img:hover {
+	border: 1px solid #ff8800;
+}
+
+div#breadcrumb {
+    
+}
+
+div#info {
+    font-size: x-small;
+    color: #666666;
+}
+
+div#info p {
+    padding: 0px;
+    margin: 0px;
+}
+
+p#about {
+    padding-top: 50px;
+    font-size: xx-small;
+    text-align: center;
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/gallery.html	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+  <head>
+    <title><!-- TITLE --></title>
+    <link rel="Stylesheet" type="text/css" href="<!-- STYLE_URL -->" />
+  </head>
+  <body>
+    <div id="breadcrumb"><!-- BREADCRUMB --></div>
+    <h1><!-- TITLE --></h1>
+    <div id="dirs"><!-- DIRECTORIES --></div>
+    <div id="thumbnails">
+      <!-- CONTENT -->
+    </div>
+    <p id="description"><!-- DESCR --></p>
+    <div id="info">
+        <p>ShortURL: <a href="<!-- SHORTURL -->"><!-- SHORTURL_CODE --></a></p>
+    </div>
+    <p id="about"><a href="http://marttila.de/~terom/degal/">DeGAL</a> <!-- VERSION --></p>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/image.html	Thu Nov 08 17:06:03 2007 +0000
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+  <head>
+    <title><!-- TITLE --></title>
+    <link rel="Stylesheet" type="text/css" href="<!-- STYLE_URL -->" />
+    <link rel="up" href="<!-- UP_URL -->" />
+    <link rel="prev" href="<!-- PREV_URL -->" />
+    <link rel="next" href="<!-- NEXT_URL -->" />
+  </head>
+  <body>
+    <div id="breadcrumb"><!-- BREADCRUMB --></div>
+    <div id="image">
+      <h1><!-- TITLE --></h1>
+      <p><!-- PREVIOUS_THUMB --> <!-- IMAGE --> <!-- NEXT_THUMB --></p>
+      <p><!-- DESCRIPTION --></p>
+    </div>
+    <div id="info">
+      <p><!-- FILE --></p>
+      <p><!-- IMGSIZE --></p>
+      <p><!-- FILESIZE --></p>
+      <p><!-- TIMESTAMP --></p>
+      <p>ShortURL: <a href="<!-- SHORTURL -->"><!-- SHORTURL_CODE --></a></p>
+      <p><a href="<!-- SERIES_URL -->"><!-- SERIES_VERB --></a> series</p>
+    </div>
+    <p id="about"><a href="http://marttila.de/~terom/degal/">DeGAL</a> <!-- VERSION --></p>
+  </body>
+</html>