fix alignment of teh paginator, and make shorturl dirs better (no index.html + pagination)
#!/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'
# 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 <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, '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)
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)
# 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 = "<ul>" # <li>Showing Images %d - %d of %d</li>" % (cur_page*IMAGE_COUNT, cur_page*IMAGE_COUNT+len(page), image_count)
if cur_page > 0 :
pagination += '<li><a href="%s">« Prev</a></li>' % (self._page_fname(cur_page - 1))
else :
pagination += '<li><span>« Prev</span></li>'
for x in xrange(0, len(pages)) :
if x == cur_page :
pagination += '<li><strong>%s</strong></li>' % (x + 1)
else :
pagination += '<li><a href="%s">%s</a></li>' % (self._page_fname(x), x+1)
if cur_page < len(pages) - 1 :
pagination += '<li><a href="%s">Next »</a></li>' % (self._page_fname(cur_page + 1))
else :
pagination += '<li><span>Next »</span></li>'
pagination += "</ul>"
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 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', 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=()) :
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)