terom@1: #!/usr/bin/env python2.4 terom@1: # terom@1: # DeGAL - A pretty simple web image gallery terom@1: # Copyright (C) 2007 Tero Marttila terom@1: # http://marttila.de/~terom/degal/ terom@1: # terom@1: # This program is free software; you can redistribute it and/or modify terom@1: # it under the terms of the GNU General Public License as published by terom@1: # the Free Software Foundation; either version 2 of the License, or terom@1: # (at your option) any later version. terom@1: # terom@1: # This program is distributed in the hope that it will be useful, terom@1: # but WITHOUT ANY WARRANTY; without even the implied warranty of terom@1: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terom@1: # GNU General Public License for more details. terom@1: # terom@1: # You should have received a copy of the GNU General Public License terom@1: # along with this program; if not, write to the terom@1: # Free Software Foundation, Inc., terom@1: # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. terom@1: # terom@1: terom@1: import os.path, os terom@1: import urllib terom@1: import logging terom@1: import string terom@1: from datetime import datetime terom@1: import struct terom@1: import base64 terom@1: import shelve terom@1: terom@1: import PIL terom@1: import PIL.Image terom@1: terom@1: import utils terom@1: terom@1: __version__ = '0.2' terom@1: terom@1: TEMPLATE_DIR='templates' terom@1: TEMPLATE_EXT='html' terom@1: terom@1: logging.basicConfig( terom@1: level=logging.INFO, terom@1: format="%(name)8s %(levelname)8s %(lineno)3d %(message)s", terom@1: terom@1: ) terom@1: terom@1: tpl = logging.getLogger('tpl') terom@1: index = logging.getLogger('index') terom@1: prepare = logging.getLogger('prepare') terom@1: render = logging.getLogger('render') terom@1: terom@1: tpl.setLevel(logging.WARNING) terom@1: terom@1: #for l in (tpl, index, prepare, render) : terom@1: # l.setLevel(logging.DEBUG) terom@1: terom@1: def readFile (path) : terom@1: fo = open(path, 'r') terom@1: data = fo.read() terom@1: fo.close() terom@1: terom@1: return data terom@1: terom@1: class Template (object) : terom@1: GLOBALS = dict( terom@1: VERSION=__version__, terom@1: ) terom@1: terom@1: def __init__ (self, name) : terom@1: self.name = name terom@1: self.path = os.path.join(TEMPLATE_DIR, "%s.%s" % (name, TEMPLATE_EXT)) terom@1: terom@1: tpl.debug("Template %s at %s", name, self.path) terom@1: terom@1: self.content = readFile(self.path) terom@1: terom@1: def render (self, **vars) : terom@1: content = self.content terom@1: terom@1: vars.update(self.GLOBALS) terom@1: terom@1: for name, value in vars.iteritems() : terom@1: content = content.replace('' % name, str(value)) terom@1: terom@1: return content terom@1: terom@1: def renderTo (self, path, **vars) : terom@1: tpl.info("Render %s to %s", self.name, path) terom@1: terom@1: fo = open(path, 'w') terom@1: fo.write(self.render(**vars)) terom@1: fo.close() terom@1: terom@1: gallery_tpl = Template('gallery') terom@1: image_tpl = Template('image') terom@1: terom@1: IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp') terom@1: terom@1: THUMB_DIR = 'thumbs' terom@1: PREVIEW_DIR = 'previews' terom@1: TITLE_FILE = 'title.txt' terom@1: terom@1: THUMB_GEOM = (160, 120) terom@1: PREVIEW_GEOM = (640, 480) terom@1: terom@1: DEFAULT_TITLE = 'Image gallery' terom@1: terom@5: # how many image/page terom@5: IMAGE_COUNT = 50 terom@5: terom@1: def isImage (fname) : terom@1: """ terom@1: Is the given filename likely to be an image file? terom@1: """ terom@1: terom@1: fname = fname.lower() terom@1: base, ext = os.path.splitext(fname) terom@1: ext = ext.lstrip('.') terom@1: terom@1: return ext in IMAGE_EXTS terom@1: terom@1: def link (url, title) : terom@1: """ terom@1: Returns an link tag with the given values terom@1: """ terom@1: terom@1: return "%s" % (urllib.quote(url), title) terom@1: terom@1: def dirUp (count=1) : terom@1: """ terom@1: Returns a relative path to the directly count levels above the current one terom@1: """ terom@1: terom@1: if not count : terom@1: return '.' terom@1: terom@1: return os.path.join(*(['..']*count)) terom@1: terom@1: def readTitleDescr (path) : terom@1: """ terom@1: Read a title.txt or .txt file terom@1: """ terom@1: terom@1: if os.path.exists(path) : terom@1: content = readFile(path) terom@1: terom@1: if '---' in content : terom@1: title, descr = content.split('---', 1) terom@1: else : terom@1: title, descr = content, '' terom@1: terom@1: return title.strip(), descr.strip() terom@1: terom@1: return None, None terom@1: terom@1: class Folder (object) : terom@1: def __init__ (self, name='.', parent=None) : terom@1: # the directory name terom@1: self.name = name terom@1: terom@1: # our parent Folder, or None terom@1: self.parent = parent terom@1: terom@1: # the path to this dir, as a relative path to the root of the image gallery, always starts with . terom@1: if parent and name : terom@1: self.path = parent.pathFor(name) terom@1: else : terom@1: self.path = name terom@1: terom@1: # the url-path to the index.html file terom@1: self.html_path = self.path terom@1: terom@1: # dict of fname -> Folder terom@1: self.subdirs = {} terom@1: terom@1: # dict of fname -> Image terom@1: self.images = {} terom@1: terom@1: # our human-friendly title terom@1: self.title = None terom@1: terom@1: # our long-winded description terom@1: self.descr = '' terom@1: terom@1: # is this folder non-empty? terom@1: self.alive = None terom@1: terom@1: # self.images.values(), but sorted by filename terom@1: self.sorted_images = [] terom@1: terom@1: # the ShortURL key to this dir terom@1: self.shorturl_code = None terom@1: terom@1: # were we filtered out? terom@1: self.filtered = False terom@1: terom@1: def pathFor (self, *fnames) : terom@1: """ terom@1: Return a root-relative path to the given path inside this dir terom@1: """ terom@1: return os.path.join(self.path, *fnames) terom@1: terom@1: def index (self, filters=None) : terom@1: """ terom@1: Look for other dirs and images inside this dir. Filters must be either None, terom@1: whereupon all files will be included, or a dict of {filename -> next_filter}. terom@1: If given, only filenames that are present in the dict will be indexed, and in terom@1: the case of dirs, the next_filter will be passed on to that Folder's index terom@1: method. terom@1: """ terom@1: terom@1: index.info("Indexing %s", self.path) terom@1: terom@1: if filters : terom@1: self.filtered = True terom@1: terom@1: # iterate through listdir terom@1: for fname in os.listdir(self.path) : terom@1: # the full filesystem path to it terom@1: fpath = self.pathFor(fname) terom@1: terom@1: # ignore dotfiles terom@1: if fname.startswith('.') : terom@1: index.debug("Skipping dotfile %s", fname) terom@1: continue terom@1: terom@1: # apply filters terom@1: if filters : terom@1: if fname in filters : terom@1: next_filter = filters[fname] terom@1: else : terom@1: index.debug("Skip `%s' as we have a filter", fname) terom@1: continue terom@1: else : terom@1: next_filter = None terom@1: terom@1: # recurse into subdirs, but not thumbs/previews terom@1: if os.path.isdir(fpath) and fname not in (THUMB_DIR, PREVIEW_DIR) : terom@1: index.debug("Found subdir %s", fpath) terom@1: f = self.subdirs[fname] = Folder(fname, self) terom@1: f.index(next_filter) # recursion terom@1: terom@1: # handle images terom@1: elif os.path.isfile(fpath) and isImage(fname) : terom@1: index.debug("Found image %s", fname) terom@1: self.images[fname] = Image(self, fname) terom@1: terom@1: # ignore everything else terom@1: else : terom@1: index.debug("Ignoring file %s", fname) terom@1: terom@1: def prepare (self) : terom@1: """ terom@1: Prepare the dir, i.e. sort+prepare the images, as well as recurse terom@1: into subdirs terom@1: """ terom@1: terom@1: prepare.info("Preparing dir %s", self.path) terom@1: terom@1: # is this folder non-empty? terom@1: alive = False terom@1: terom@1: # only create thumbs/previews dirs if we have images in here terom@1: if self.images : terom@1: # folder is non-empty terom@1: alive = True terom@1: prepare.info("Have %d images", len(self.images)) terom@1: terom@1: # create the thumb/preview dirs if needed terom@1: for dir in (THUMB_DIR, PREVIEW_DIR) : terom@1: prepare.debug("Checking for existance of %s dir", dir) terom@1: path = self.pathFor(dir) terom@1: terom@1: if not os.path.isdir(path) : terom@1: prepare.info("Creating dir %s", path) terom@1: os.mkdir(path) terom@1: terom@1: # sort the images terom@1: fnames = self.images.keys() terom@1: fnames.sort() terom@1: terom@1: prev = None terom@1: terom@1: # link them together and prepare them terom@1: for fname in fnames : terom@1: img = self.images[fname] terom@1: terom@1: prepare.debug("Linking %s <-> %s", prev, img) terom@1: terom@1: img.prev = prev terom@1: terom@1: if prev : terom@1: prev.next = img terom@1: terom@1: prev = img terom@1: terom@1: img.prepare() terom@1: terom@1: # add to the sorted images list terom@1: self.sorted_images.append(img) terom@1: terom@1: # prepare subdirs terom@1: if self.subdirs : terom@1: prepare.info("Have %d subdirs", len(self.subdirs)) terom@1: terom@1: # just recurse into them, we're alive if one of them is terom@1: for dir in self.subdirs.itervalues() : terom@1: if dir.prepare() : terom@1: alive = True terom@1: terom@1: # figure out our title terom@1: title_path = self.pathFor(TITLE_FILE) terom@1: terom@1: title, descr = readTitleDescr(title_path) terom@1: terom@1: if title : terom@1: prepare.info("Found title/descr") terom@1: self.title = title terom@1: self.descr = descr terom@1: alive = True terom@1: terom@1: # default title for the root dir terom@1: elif self.name == '.' : terom@1: self.title = 'Index' terom@1: terom@1: else : terom@1: self.title = self.name terom@1: terom@1: prepare.debug("Our title is '%s'", self.title) terom@1: terom@1: # lol ded terom@1: if not alive : terom@1: prepare.info("Dir %s is not alive", self.path) terom@1: terom@1: self.alive = alive terom@1: terom@1: return alive terom@1: terom@1: def getObjInfo (self) : terom@1: """ terom@1: Metadata for shorturls2.db terom@1: """ terom@5: return 'dir', self.path, 'index' terom@1: terom@1: def linkTag (self) : terom@1: """ terom@1: A text-link to this dir terom@1: """ terom@1: terom@1: return link(self.path, self.title) terom@1: terom@1: def breadcrumb (self) : terom@1: """ terom@1: Returns a [(fname, title)] list of this dir's parent dirs terom@1: """ terom@1: terom@1: f = self terom@1: b = [] terom@1: d = 0 terom@1: terom@1: while f : terom@1: b.insert(0, (dirUp(d), f.title)) terom@1: terom@1: d += 1 terom@1: f = f.parent terom@1: terom@1: return b terom@1: terom@1: def inRoot (self, *fnames) : terom@1: """ terom@1: Return a relative URL from this dir to the given path in the root dir terom@1: """ terom@1: terom@1: c = len(self.path.split('/')) - 1 terom@1: terom@1: return os.path.join(*((['..']*c) + list(fnames))) terom@5: terom@5: def _page_fname (self, page) : terom@5: assert page >= 0 terom@5: terom@5: if page > 0 : terom@5: return 'index_%d.html' % page terom@5: else : terom@5: return 'index.html' terom@1: terom@1: def render (self) : terom@1: """ terom@1: Render the index.html, Images, and recurse into subdirs terom@1: """ terom@1: terom@1: # ded folders are skipped terom@1: if not self.alive : terom@1: render.info("Skipping dir %s", self.path) terom@1: return terom@1: terom@1: # 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 terom@1: if self.filtered : terom@1: render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path) terom@1: terom@1: else : terom@1: # sort the subdirs terom@1: subdirs = self.subdirs.items() terom@1: subdirs.sort() terom@1: terom@1: # generate the 's for the subdirs terom@1: subdir_linkTags = [link(f.name, f.title) for fname, f in subdirs if f.alive] terom@1: terom@1: # stick them into a list terom@1: if subdir_linkTags : terom@1: directories = "" % "\n\t
  • ".join(subdir_linkTags) terom@1: else : terom@1: directories = '' terom@1: terom@1: render.info("Rendering %s", self.path) terom@5: terom@5: # paginate! terom@5: images = self.sorted_images terom@5: image_count = len(images) terom@5: pages = [] terom@1: terom@5: while images : terom@5: pages.append(images[:IMAGE_COUNT]) terom@5: images = images[IMAGE_COUNT:] terom@5: terom@5: pagination_required = len(pages) > 1 terom@5: terom@5: if pagination_required : terom@5: render.info("Index split into %d pages of %d images each", len(pages), IMAGE_COUNT) terom@5: terom@5: for cur_page, page in enumerate(pages) : terom@5: if pagination_required : terom@5: pagination = "" terom@6: shorturl = "%s/%s" % (self.shorturl_code, cur_page+1) terom@5: else : terom@5: pagination = '' terom@6: shorturl = self.shorturl_code terom@5: terom@5: # render to index.html terom@5: gallery_tpl.renderTo(self.pathFor(self._page_fname(cur_page)), terom@5: STYLE_URL=self.inRoot('style.css'), terom@5: BREADCRUMB=" » ".join([link(u, t) for (u, t) in self.breadcrumb()]), terom@5: TITLE=self.title, terom@5: DIRECTORIES=directories, terom@5: PAGINATION=pagination, terom@5: CONTENT="".join([i.thumbImgTag() for i in page]), terom@5: DESCR=self.descr, terom@6: SHORTURL=self.inRoot('s', shorturl), terom@6: SHORTURL_CODE=shorturl, terom@5: ) terom@1: terom@1: # render images terom@1: for img in self.images.itervalues() : terom@1: img.render() terom@1: terom@1: # recurse into subdirs terom@1: for dir in self.subdirs.itervalues() : terom@1: dir.render() terom@1: terom@1: class Image (object) : terom@1: def __init__ (self, dir, name) : terom@1: # the image filename, e.g. DSC3948.JPG terom@1: self.name = name terom@1: terom@1: # the Folder object that we are in terom@1: self.dir = dir terom@1: terom@1: # the relative path from the root to us terom@1: self.path = dir.pathFor(name) terom@1: terom@1: # the basename+ext, e.g. DSCR3948, .JPG terom@1: self.base_name, self.ext = os.path.splitext(name) terom@1: terom@1: # the root-relative paths to the thumb and preview images terom@1: self.thumb_path = self.dir.pathFor(THUMB_DIR, self.name) terom@1: self.preview_path = self.dir.pathFor(PREVIEW_DIR, self.name) terom@1: terom@1: # our user-friendly title terom@1: self.title = name terom@1: terom@1: # our long-winded description terom@1: self.descr = '' terom@1: terom@1: # the image before and after us, both may be None terom@1: self.prev = self.next = None terom@1: terom@1: # the name of the .html gallery view thing for this image, *always* self.name + ".html" terom@1: self.html_name = self.name + ".html" terom@1: terom@1: # the root-relative path to the gallery view terom@1: self.html_path = self.dir.pathFor(self.html_name) terom@1: terom@1: # terom@1: # Figured out after prepare terom@1: # terom@1: terom@1: # (w, h) tuple terom@1: self.img_size = None terom@1: terom@1: # the ShortURL code for this image terom@1: self.shorturl_code = None terom@1: terom@1: # what to use in the rendered templates, intended to be overridden by subclasses terom@1: self.series_act = "add" terom@1: self.series_verb = "Add to" terom@1: terom@1: def prepare (self) : terom@1: """ terom@1: Generate the thumbnail/preview views if needed, get the image info, and look for the title terom@1: """ terom@1: terom@1: prepare.info("Preparing image %s", self.path) terom@7: terom@1: # stat the image file to get the filesize and mtime terom@1: st = os.stat(self.path) terom@1: terom@1: self.filesize = st.st_size terom@1: self.timestamp = st.st_mtime terom@1: terom@1: # open the image in PIL to get image attributes + generate thumbnails terom@1: img = PIL.Image.open(self.path) terom@1: terom@1: self.img_size = img.size terom@1: terom@1: for out_path, geom in ((self.thumb_path, THUMB_GEOM), (self.preview_path, PREVIEW_GEOM)) : terom@1: # if it doesn't exist, or it's older than the image itself, generate terom@1: if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) : terom@1: prepare.info("Create thumbnailed image at %s with geom %s", out_path, geom) terom@1: terom@1: # XXX: is this the most efficient way to do this? terom@1: out_img = img.copy() terom@1: out_img.thumbnail(geom, resample=True) terom@1: out_img.save(out_path) terom@1: terom@1: # look for the metadata file terom@1: title_path = self.dir.pathFor(self.base_name + '.txt') terom@1: prepare.debug("Looking for title at %s", title_path) terom@1: terom@1: title, descr = readTitleDescr(title_path) terom@1: terom@1: if title : terom@1: self.title = title terom@1: self.descr = descr terom@1: terom@1: prepare.info("Found title `%s'", self.title) terom@1: terom@1: def getObjInfo (self) : terom@1: """ terom@1: Metadata for shorturl2.db terom@1: """ terom@1: return 'img', self.dir.path, self.name terom@1: terom@1: def thumbImgTag (self) : terom@1: """ terom@1: a of this image's thumbnail. Path relative to directory we are in terom@1: """ terom@5: return link(self.html_name, "%s" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title)) terom@1: terom@1: def previewImgTag (self) : terom@1: """ terom@1: a of this image's preview. Path relative to directory we are in terom@1: """ terom@5: return link(self.name, "%s" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title)) terom@1: terom@1: def linkTag (self) : terom@1: """ terom@1: a text-link to this image terom@1: """ terom@1: return link(self.html_name, self.title) terom@1: terom@1: def breadcrumb (self) : terom@1: """ terom@1: Returns a [(fname, title)] list of this image's parents terom@1: """ terom@1: terom@1: f = self.dir terom@1: b = [(self.html_name, self.title)] terom@1: d = 0 terom@1: terom@1: while f : terom@1: b.insert(0, (dirUp(d), f.title)) terom@1: terom@1: d += 1 terom@1: f = f.parent terom@1: terom@1: return b terom@1: terom@1: def render (self) : terom@1: """ terom@1: Write out the .html file terom@1: """ terom@1: terom@1: render.info("Rendering image %s", self.path) terom@1: terom@1: image_tpl.renderTo(self.html_path, terom@1: STYLE_URL=self.dir.inRoot('style.css'), terom@1: UP_URL=('.'), terom@1: PREV_URL=(self.prev and self.prev.html_name or ''), terom@1: NEXT_URL=(self.next and self.next.html_name or ''), terom@1: FILE=self.name, terom@1: BREADCRUMB=" » ".join([link(u, t) for u, t in self.breadcrumb()]), terom@1: TITLE=self.title, terom@1: PREVIOUS_THUMB=(self.prev and self.prev.thumbImgTag() or ''), terom@1: IMAGE=self.previewImgTag(), terom@1: NEXT_THUMB=(self.next and self.next.thumbImgTag() or ''), terom@1: DESCRIPTION=self.descr, terom@1: IMGSIZE="%dx%d" % self.img_size, terom@1: FILESIZE=fmtFilesize(self.filesize), terom@1: TIMESTAMP=fmtTimestamp(self.timestamp), terom@1: SHORTURL=self.dir.inRoot('s', self.shorturl_code), terom@1: SHORTURL_CODE=self.shorturl_code, terom@1: SERIES_URL=self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)), terom@1: SERIES_VERB=self.series_verb, terom@1: ) terom@1: terom@1: def __str__ (self) : terom@1: return "Image `%s' in `%s'" % (self.name, self.dir.path) terom@1: terom@1: def int2key (id) : terom@1: """ terom@1: Turn an integer into a short-as-possible url-safe string terom@1: """ terom@1: for type in ('B', 'H', 'I') : terom@1: try : terom@1: return base64.b64encode(struct.pack(type, id), '-_').rstrip('=') terom@1: except struct.error : terom@1: continue terom@1: terom@1: raise Exception("ID overflow: %s" % id) terom@1: terom@1: def updateShorturlDb (root) : terom@1: """ terom@1: DeGAL <= 0.2 used a simple key => path mapping, but now we use terom@1: something more structured, key => (type, dirpath, fname), where terom@1: terom@1: type - one of 'img', 'dir' terom@1: dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux' terom@1: fname - the filename, one of '', 'DSC9839.JPG', 'this.png', etc. terom@1: """ terom@1: terom@5: db = shelve.open('shorturls2', 'c', writeback=True) terom@1: terom@1: id = db.get('_id', 1) terom@1: terom@1: dirqueue = [root] terom@1: terom@1: # dict of path -> obj terom@1: paths = {} terom@1: terom@5: index.info("Processing ShortURLs...") terom@5: terom@1: while dirqueue : terom@1: dir = dirqueue.pop(0) terom@1: terom@1: dirqueue.extend(dir.subdirs.itervalues()) terom@1: terom@1: if dir.alive : terom@1: paths[dir.path] = dir terom@1: terom@1: for img in dir.images.itervalues() : terom@5: paths[img.path] = img terom@1: terom@1: for key in db.keys() : terom@1: if key.startswith('_') : terom@1: continue terom@1: terom@1: type, dirpath, fname = db[key] terom@1: terom@5: path = os.path.join(dirpath, fname).rstrip('/') terom@1: terom@1: try : terom@1: paths.pop(path).shorturl_code = key terom@1: index.debug("Code for `%s' is %s", path, key) terom@1: terom@1: except KeyError : terom@1: index.debug("Path `%s' in DB does not exist?", path) terom@1: terom@1: for obj in paths.itervalues() : terom@1: key = int2key(id) terom@1: id += 1 terom@1: terom@1: index.info("Alloc code `%s' for `%s'", key, obj.html_path) terom@1: terom@1: obj.shorturl_code = key terom@1: terom@1: db[key] = obj.getObjInfo() terom@1: terom@1: db['_id'] = id terom@1: db.close() terom@1: terom@1: def main (targets=()) : terom@1: root_filter = {} terom@1: terom@1: for target in targets : terom@1: f = root_filter terom@1: for path_part in target.split('/') : terom@1: if path_part : terom@1: if path_part not in f : terom@1: f[path_part] = {} terom@1: f = f[path_part] terom@1: terom@1: index.debug('Filter: %s', root_filter) terom@1: terom@1: root = Folder() terom@1: root.index(root_filter) terom@1: root.prepare() terom@1: updateShorturlDb(root) terom@1: root.render() terom@1: terom@1: def fmtFilesize (size) : terom@10: return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False) terom@1: terom@1: def fmtTimestamp (ts) : terom@10: return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") terom@1: terom@1: if __name__ == '__main__' : terom@1: from sys import argv terom@1: argv.pop(0) terom@1: terom@1: main(argv) terom@1: