--- /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=" » ".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=" » ".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]
+