# HG changeset patch # User terom # Date 1198269363 0 # Node ID c2d8e9a754a15a6e00e8e32c13f9a6b807850a23 # Parent 27dac27d1a5811b8a33e04d319686d9ecc1ba045 Major code restructuring. Version is now 0.5, templates use Mako, and the code is split off into several files under lib/ diff -r 27dac27d1a58 -r c2d8e9a754a1 degal.py --- a/degal.py Thu Dec 20 17:42:04 2007 +0000 +++ b/degal.py Fri Dec 21 20:36:03 2007 +0000 @@ -21,647 +21,11 @@ # 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="%(message)s", -# format="%(name)8s %(levelname)8s %(lineno)3d %(message)s", - -) - -tpl = logging.getLogger('tpl') -index = logging.getLogger('index') -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('' % 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' - -# how many image/page -IMAGE_COUNT = 50 - -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 link tag with the given values - """ - - return "%s" % (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 .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) - if f.index(next_filter) : # recursion - # if a subdir is alive, we are alive as well - self.alive = True - - # 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) - - # sort and link the images - if self.images : - self.alive = True - - # sort the images - fnames = self.images.keys() - fnames.sort() - - prev = None - - # link - for fname in fnames : - img = self.images[fname] - - img.prev = prev - - if prev : - prev.next = img - - prev = img - - # add to the sorted images list - self.sorted_images.append(img) - - return self.alive - - def getObjInfo (self) : - """ - Metadata for shorturls2.db - """ - return 'dir', self.path, 'index' - - 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 _page_fname (self, page) : - assert page >= 0 - - if page > 0 : - return 'index_%d.html' % page - else : - return 'index.html' - - 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) +from optparse import OptionParser - else : - # create the thumb/preview dirs if needed - for dir in (THUMB_DIR, PREVIEW_DIR) : - path = self.pathFor(dir) - - if not os.path.isdir(path) : - render.info("Creating dir %s", path) - os.mkdir(path) - - # figure out our title - title_path = self.pathFor(TITLE_FILE) - - title, descr = readTitleDescr(title_path) - - if title : - self.title = title - self.descr = descr - - # default title for the root dir - elif self.name == '.' : - self.title = 'Index' - - else : - self.title = self.name - - # sort the subdirs - subdirs = self.subdirs.items() - subdirs.sort() - - # generate the '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 = "" % "\n\t
  • ".join(subdir_linkTags) - else : - directories = '' - - render.info("Rendering %s", self.path) - - # paginate! - images = self.sorted_images - image_count = len(images) - pages = [] - - while images : - pages.append(images[:IMAGE_COUNT]) - images = images[IMAGE_COUNT:] - - pagination_required = len(pages) > 1 - - if pagination_required : - render.info("Index split into %d pages of %d images each", len(pages), IMAGE_COUNT) - - for cur_page, page in enumerate(pages) : - if pagination_required : - pagination = "" - shorturl = "%s/%s" % (self.shorturl_code, cur_page+1) - else : - pagination = '' - shorturl = self.shorturl_code - - # render to index.html - gallery_tpl.renderTo(self.pathFor(self._page_fname(cur_page)), - STYLE_URL=self.inRoot('style.css'), - BREADCRUMB=" » ".join([link(u, t) for (u, t) in self.breadcrumb()]), - TITLE=self.title, - DIRECTORIES=directories, - PAGINATION=pagination, - CONTENT="".join([i.thumbImgTag() for i in page]), - DESCR=self.descr, - SHORTURL=self.inRoot('s', shorturl), - SHORTURL_CODE=shorturl, - ) - - # 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 getObjInfo (self) : - """ - Metadata for shorturl2.db - """ - return 'img', self.dir.path, self.name - - def thumbImgTag (self) : - """ - a of this image's thumbnail. Path relative to directory we are in - """ - return link(self.html_name, "%s" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title)) +from lib import folder, shorturl - def previewImgTag (self) : - """ - a of this image's preview. Path relative to directory we are in - """ - return link(self.name, "%s" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title)) - - def linkTag (self) : - """ - 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 - """ - - # 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) : - render.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') - - title, descr = readTitleDescr(title_path) - - if title : - self.title = title - self.descr = descr - - 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', writeback=True) - - id = db.get('_id', 1) - - dirqueue = [root] - - # dict of path -> obj - paths = {} - - index.info("Processing ShortURLs...") - - 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.path] = img - - for key in db.keys() : - if key.startswith('_') : - continue - - type, dirpath, fname = db[key] - - path = os.path.join(dirpath, fname).rstrip('/') - - 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=()) : +def main (dir='.', targets=()) : root_filter = {} for target in targets : @@ -670,24 +34,19 @@ 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 = folder.Folder(dir) root.index(root_filter) - updateShorturlDb(root) + shorturl.updateDB(root) root.render() -def fmtFilesize (size) : - return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False) - -def fmtTimestamp (ts) : - return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") - if __name__ == '__main__' : - from sys import argv - argv.pop(0) - - main(argv) - + parser = OptionParser(usage="usage: %prog [options] ... [target ...]") + + parser.add_option("-d", "--dir", dest="dir", help="look for images in DIR and write the HTML there", metavar="DIR", default=".") + + options, filter_targets = parser.parse_args() + + main(options.dir, filter_targets) diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/folder.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/folder.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,287 @@ +import os, os.path + +import settings, image, utils +from log import index, render +from template import gallery as gallery_tpl +from helpers import url_for_page + +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 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 settings.IMAGE_EXTS + +class Folder (object) : + def __init__ (self, name='.', parent=None) : + # the directory name, no trailing / + self.name = name.rstrip(os.sep) + + # 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(self.name) + else : + self.path = self.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 (settings.THUMB_DIR, settings.PREVIEW_DIR)) + and (self.parent or fname not in settings.ROOT_IGNORE) + ) : + index.debug("Found subdir %s", fpath) + f = self.subdirs[fname] = Folder(fname, self) + if f.index(next_filter) : # recursion + # if a subdir is alive, we are alive as well + self.alive = True + + # handle images + elif os.path.isfile(fpath) and isImage(fname) : + index.debug("Found image %s", fname) + self.images[fname] = image.Image(self, fname) + + # ignore everything else + else : + index.debug("Ignoring file %s", fname) + + # sort and link the images + if self.images : + self.alive = True + + # sort the images + fnames = self.images.keys() + fnames.sort() + + prev = None + + # link + for fname in fnames : + img = self.images[fname] + + img.prev = prev + + if prev : + prev.next = img + + prev = img + + # add to the sorted images list + self.sorted_images.append(img) + + # figure out our title/ descr. Must be done before our parent dir is rendered (self.title) + title_path = self.pathFor(settings.TITLE_FILE) + + self.title, self.descr = utils.readTitleDescr(title_path) + + # default title for the root dir + if self.title : + self.alive = True + pass # use what was in the title file + + elif not self.parent : + self.title = 'Index' + + else : + self.title = self.name + + if self.descr : + self.alive = True + + return self.alive + + def getObjInfo (self) : + """ + Metadata for shorturls2.db + """ + return 'dir', self.path, '' + + 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 countParents (self, acc=0) : + if self.parent : + return self.parent.countParents(acc+1) + else : + return acc + + def inRoot (self, *fnames) : + """ + Return a relative URL from this dir to the given path in the root dir + """ + + c = self.countParents() + + return utils.url_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 (no images)", 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 : + # create the thumb/preview dirs if needed + for dir in (settings.THUMB_DIR, settings.PREVIEW_DIR) : + path = self.pathFor(dir) + + if not os.path.isdir(path) : + render.info("Creating dir %s", path) + os.mkdir(path) + + # sort the subdirs + subdirs = self.subdirs.values() + subdirs.sort(key=lambda d: d.name) + + render.info("Rendering %s", self.path) + + # paginate! + images = self.sorted_images + image_count = len(images) + pages = [] + + while images : + pages.append(images[:settings.IMAGE_COUNT]) + images = images[settings.IMAGE_COUNT:] + + pagination_required = len(pages) > 1 + + if pagination_required : + render.info("Index split into %d pages of %d images each", len(pages), settings.IMAGE_COUNT) + + for cur_page, images in enumerate(pages) : + if pagination_required and cur_page > 0 : + shorturl = "%s/%s" % (self.shorturl_code, cur_page+1) + else : + shorturl = self.shorturl_code + + # render to index.html + gallery_tpl.render_to(self.pathFor(url_for_page(cur_page)), + stylesheet_url = self.inRoot('style.css'), + title = self.title, + breadcrumb = self.breadcrumb(), + + dirs = subdirs, + images = images, + + num_pages = len(pages), + cur_page = cur_page, + + description = self.descr, + + shorturl = self.inRoot('s', shorturl), + shorturl_code = shorturl, + ) + + # render images + for img in self.images.itervalues() : + img.render() + + # recurse into subdirs + for dir in self.subdirs.itervalues() : + dir.render() + \ No newline at end of file diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/formatbytes.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/formatbytes.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,116 @@ +############################################################### +# Functions taken from pathutils.py Version 0.2.5 (2005/12/06), http://www.voidspace.org.uk/python/recipebook.shtml#utils +# Copyright Michael Foord 2004 +# Released subject to the BSD License +# Please see http://www.voidspace.org.uk/python/license.shtml + +############################################################### +# formatbytes takes a filesize (as returned by os.getsize() ) +# and formats it for display in one of two ways !! + +# For information about bugfixes, updates and support, please join the Pythonutils mailing list. +# http://groups.google.com/group/pythonutils/ +# Comments, suggestions and bug reports welcome. +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# E-mail fuzzyman@voidspace.org.uk + +def formatbytes(sizeint, configdict=None, **configs): + """ + Given a file size as an integer, return a nicely formatted string that + represents the size. Has various options to control it's output. + + You can pass in a dictionary of arguments or keyword arguments. Keyword + arguments override the dictionary and there are sensible defaults for options + you don't set. + + Options and defaults are as follows : + + * ``forcekb = False`` - If set this forces the output to be in terms + of kilobytes and bytes only. + + * ``largestonly = True`` - If set, instead of outputting + ``1 Mbytes, 307 Kbytes, 478 bytes`` it outputs using only the largest + denominator - e.g. ``1.3 Mbytes`` or ``17.2 Kbytes`` + + * ``kiloname = 'Kbytes'`` - The string to use for kilobytes + + * ``meganame = 'Mbytes'`` - The string to use for Megabytes + + * ``bytename = 'bytes'`` - The string to use for bytes + + * ``nospace = True`` - If set it outputs ``1Mbytes, 307Kbytes``, + notice there is no space. + + Example outputs : :: + + 19Mbytes, 75Kbytes, 255bytes + 2Kbytes, 0bytes + 23.8Mbytes + + .. note:: + + It currently uses the plural form even for singular. + """ + defaultconfigs = { 'forcekb' : False, + 'largestonly' : True, + 'kiloname' : 'Kbytes', + 'meganame' : 'Mbytes', + 'bytename' : 'bytes', + 'nospace' : True} + if configdict is None: + configdict = {} + for entry in configs: + # keyword parameters override the dictionary passed in + configdict[entry] = configs[entry] + # + for keyword in defaultconfigs: + if not configdict.has_key(keyword): + configdict[keyword] = defaultconfigs[keyword] + # + if configdict['nospace']: + space = '' + else: + space = ' ' + # + mb, kb, rb = bytedivider(sizeint) + if configdict['largestonly']: + if mb and not configdict['forcekb']: + return stringround(mb, kb)+ space + configdict['meganame'] + elif kb or configdict['forcekb']: + if mb and configdict['forcekb']: + kb += 1024*mb + return stringround(kb, rb) + space+ configdict['kiloname'] + else: + return str(rb) + space + configdict['bytename'] + else: + outstr = '' + if mb and not configdict['forcekb']: + outstr = str(mb) + space + configdict['meganame'] +', ' + if kb or configdict['forcekb'] or mb: + if configdict['forcekb']: + kb += 1024*mb + outstr += str(kb) + space + configdict['kiloname'] +', ' + return outstr + str(rb) + space + configdict['bytename'] + +def stringround(main, rest): + """ + Given a file size in either (mb, kb) or (kb, bytes) - round it + appropriately. + """ + # divide an int by a float... get a float + value = main + rest/1024.0 + return str(round(value, 1)) + +def bytedivider(nbytes): + """ + Given an integer (probably a long integer returned by os.getsize() ) + it returns a tuple of (megabytes, kilobytes, bytes). + + This can be more easily converted into a formatted string to display the + size of the file. + """ + mb, remainder = divmod(nbytes, 1048576) + kb, rb = divmod(remainder, 1024) + return (mb, kb, rb) + + diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/helpers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/helpers.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,31 @@ +# template helper functions +import urllib +from formatbytes import formatbytes +from datetime import datetime + +def iter_is_first (seq) : + flag = True + + for item in seq : + yield item, flag + flag = False + +def url_for_page (page) : + assert page >= 0 + + if page > 0 : + return 'index_%d.html' % page + else : + return 'index.html' + +def tag_for_img (page, img) : + return """""" % (page, img) + +def format_filesize (size) : + return formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False) + +def format_timestamp (ts) : + return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") + +def format_imgsize (size) : + return "%dx%d" % size diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/image.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/image.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,129 @@ +import os, os.path + +import PIL.Image + +import settings, utils +from log import index, render +from template import image as image_tpl + +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) + + # 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 image-relative names for the html page, thumb and preview images + self.html_name = self.name + ".html" + self.thumb_name = utils.url_join(settings.THUMB_DIR, self.name) + self.preview_name = utils.url_join(settings.PREVIEW_DIR, self.name) + + # the root-relative paths to the html page, thumb and preview images + self.html_path = self.dir.pathFor(self.html_name) + self.thumb_path = self.dir.pathFor(settings.THUMB_DIR, self.name) + self.preview_path = self.dir.pathFor(settings.PREVIEW_DIR, self.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 getObjInfo (self) : + """ + Metadata for shorturl2.db + """ + return 'img', self.dir.path, self.name + + def breadcrumb (self) : + """ + Returns a [(fname, title)] list of this image's parents + """ + + return self.dir.breadcrumb() + [(self.html_name, self.title)] + + def render (self) : + """ + Write out the .html file + """ + + # 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, settings.THUMB_GEOM), (self.preview_path, settings.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) : + render.info("Create thumbnailed image at %s with geom %s", out_path, geom) + + # XXX: is this the most efficient way to do this? It seems slow + 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') + + self.title, self.descr = utils.readTitleDescr(title_path) + + if not self.title : + self.title = self.name + + render.info("Rendering image %s", self.path) + + image_tpl.render_to(self.html_path, + stylesheet_url = self.dir.inRoot('style.css'), + title = self.title, + breadcrumb = self.breadcrumb(), + + prev = self.prev, + next = self.next, + img = self, + + description = self.descr, + + filename = self.name, + img_size = self.img_size, + file_size = self.filesize, + timestamp = 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) diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/log.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/log.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,14 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(message)s", +# format="%(name)8s %(levelname)8s %(lineno)3d %(message)s", + +) + +index = logging.getLogger('index') +render = logging.getLogger('render') +template = logging.getLogger('template') + +template.setLevel(logging.ERROR) diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/settings.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,19 @@ +TEMPLATE_DIR = './templates' +TEMPLATE_EXT = 'html' + +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' + +# how many image/page +IMAGE_COUNT = 50 + +VERSION = "0.5" +ROOT_IGNORE = ('lib', 'templates') diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/shorturl.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/shorturl.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,78 @@ +import struct +import base64 +import shelve +import os.path + +from log import index + +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 updateDB (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', writeback=True) + + id = db.get('_id', 1) + + dirqueue = [root] + + # dict of path -> obj + paths = {} + + index.info("Processing ShortURLs...") + + 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.path] = img + + for key in db.keys() : + if key.startswith('_') : + continue + + type, dirpath, fname = db[key] + + path = os.path.join(dirpath, fname).rstrip(os.sep) + + 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() diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/template.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,45 @@ +from mako import exceptions +from mako.lookup import TemplateLookup + +import settings, helpers + +import log + +_lookup = TemplateLookup( + directories=[settings.TEMPLATE_DIR], + module_directory='%s/cache' % settings.TEMPLATE_DIR, + output_encoding='utf-8', + filesystem_checks=False, # this may need to be changed if used in a long-term process +) + +TEMPLATE_GLOBALS = dict( + h = helpers, + version = settings.VERSION, +) + +class Template (object) : + def __init__ (self, name) : + self.name = name + self.tpl = _lookup.get_template("%s.%s" % (name, settings.TEMPLATE_EXT)) + + def render (self, **data) : + data.update(TEMPLATE_GLOBALS) + + try : + log.template.debug("render %s with %s", self.name, data) + return self.tpl.render(**data) + except : + data = exceptions.text_error_template().render() + log.template.error(data) + + raise + + def render_to (self, file, **data) : + fh = open(file, "w") + fh.write(self.render(**data)) + fh.close() + +# templates +gallery = Template("gallery") +image = Template("image") + diff -r 27dac27d1a58 -r c2d8e9a754a1 lib/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/utils.py Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,38 @@ +import os.path + +def readFile (path) : + fo = open(path, 'r') + data = fo.read() + fo.close() + + return data + +def readTitleDescr (path) : + """ + Read a title.txt or .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 "", "" + +def url_join (*parts) : + return '/'.join(parts) + +from optparse import OptionParser + +def optparse (options) : + parser = OptionParser() + + + parser.add_option + + \ No newline at end of file diff -r 27dac27d1a58 -r c2d8e9a754a1 series.cgi --- a/series.cgi Thu Dec 20 17:42:04 2007 +0000 +++ b/series.cgi Fri Dec 21 20:36:03 2007 +0000 @@ -1,4 +1,24 @@ #!/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 shelve import cgi diff -r 27dac27d1a58 -r c2d8e9a754a1 shorturl.cgi --- a/shorturl.cgi Thu Dec 20 17:42:04 2007 +0000 +++ b/shorturl.cgi Fri Dec 21 20:36:03 2007 +0000 @@ -1,4 +1,25 @@ -#!/usr/bin/env python2.5 +#!/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 shelve import cgi diff -r 27dac27d1a58 -r c2d8e9a754a1 taggr.cgi --- a/taggr.cgi Thu Dec 20 17:42:04 2007 +0000 +++ b/taggr.cgi Fri Dec 21 20:36:03 2007 +0000 @@ -1,4 +1,25 @@ #!/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 cgi import shelve diff -r 27dac27d1a58 -r c2d8e9a754a1 templates/gallery.html --- a/templates/gallery.html Thu Dec 20 17:42:04 2007 +0000 +++ b/templates/gallery.html Fri Dec 21 20:36:03 2007 +0000 @@ -1,25 +1,56 @@ - - +<%inherit file="master.html" /> - - - <!-- TITLE --> - - - - -

    -
    -
    +<%def name="pagination(num_pages, cur_page)"> +% if num_pages > 1 : +
      + +% if cur_page > 0 : +
    • « Prev
    • +% else : +
    • « Prev
    • +% endif + +% for page in xrange(0, num_pages) : +% if page == cur_page : +
    • ${page + 1}
    • +% else : +
    • ${page + 1}
    • +% endif +% endfor + +% if cur_page < num_pages - 1 : +
    • Next »
    • +% else : +
    • Next »
    • +% endif +
    +% endif + + +

    ${title}

    +
    +% if dirs : + +% endif +
    +
    +${pagination(num_pages, cur_page)} +
    - +% for img in images : + ${h.tag_for_img(img.html_name, img.thumb_name)} +% endfor
    -
    -

    +
    +${pagination(num_pages, cur_page)} +
    +

    +${description} +

    -

    ShortURL:

    +

    ShortURL: ${shorturl_code}

    -

    DeGAL

    - - diff -r 27dac27d1a58 -r c2d8e9a754a1 templates/image.html --- a/templates/image.html Thu Dec 20 17:42:04 2007 +0000 +++ b/templates/image.html Fri Dec 21 20:36:03 2007 +0000 @@ -1,30 +1,27 @@ - - +<%inherit file="master.html" /> - - - <!-- TITLE --> - - - - - - -
    -

    -

    -

    +

    ${title}

    +

    +% if prev : + ${h.tag_for_img(prev.html_name, prev.thumb_name)} +% endif + + ${h.tag_for_img(img.html_name, img.preview_name)} + +% if next : + ${h.tag_for_img(next.html_name, next.thumb_name)} +% endif +

    +

    + ${description} +

    -

    -

    -

    -

    -

    ShortURL:

    -

    series

    +

    ${filename}

    +

    ${h.format_imgsize(img_size)}

    +

    ${h.format_filesize(file_size)}

    +

    ${h.format_timestamp(timestamp)}

    +

    ShortURL: ${shorturl_code}

    +

    ${series_verb} series

    -

    DeGAL

    - - diff -r 27dac27d1a58 -r c2d8e9a754a1 templates/master.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/master.html Fri Dec 21 20:36:03 2007 +0000 @@ -0,0 +1,22 @@ + + + + + + ${title} + + + + + ${next.body()} +

    DeGAL ${version}

    + +