1
0
mirror of https://github.com/sjlongland/tornado-gallery.git synced 2025-09-13 16:43:16 +10:00
tornado-gallery/tornado_gallery/gallery.py

231 lines
7.1 KiB
Python

#!/usr/bin/env python
import logging
import os.path
from cachefs import CacheFs
from weakref import ref
from collections import OrderedDict, Mapping
from time import time
from .metadata import MetadataCache
from .cache import Cache
from .photo import Photo
from .resizer import ResizerPool
from tornado.gen import coroutine, Return
GALLERY_META_FILE = 'info.txt'
CACHE_DIR_NAME = 'cache'
class GalleryCollection(Cache):
"""
Represents the collection of photo galleries.
"""
def __init__(self, root_dir, cache_subdir=CACHE_DIR_NAME,
num_proc=None, cache_expiry=300.0,
cache_stat_expiry=1.0, log=None):
if log is None:
log = logging.getLogger(self.__class__.__module__)
super(GalleryCollection, self).__init__(
cache_duration=cache_expiry, log=log)
self._fs_cache = CacheFs(cache_expiry, cache_stat_expiry)
self._meta_cache = MetadataCache(self._fs_cache, cache_expiry,
log=log.getChild('meta'))
self._root_node = self._fs_cache[root_dir]
self._resizer_pool = ResizerPool(
self._root_node, cache_subdir=cache_subdir,
num_proc=num_proc, log=log.getChild('resizer'))
self._cache_subdir = cache_subdir
self._content = None
self._content_mtime = None
def __iter__(self):
content_mtime = self._root_node.stat.st_mtime
if (self._content_mtime is None) or \
(content_mtime > self._content_mtime):
content = []
for entry in self._root_node:
if entry == self._cache_subdir:
continue
node = self._root_node[entry]
if not node.is_dir:
continue
try:
if not node[GALLERY_META_FILE].is_file:
continue
content.append(entry)
except KeyError:
continue
content.sort()
self._content = content
self._content_mtime = content_mtime
return iter(self._content)
def _fetch(self, name):
return Gallery(collection=self,
gallery_node=self._root_node.join_node(name))
class Gallery(Mapping):
"""
Representation of a photo gallery.
"""
def __init__(self, collection, gallery_node):
self._collection = ref(collection)
self._fs_node = gallery_node
self._content_mtime = None
self._content = None
self._links = None
self._content_prev_order = None
self._content_next_order = None
@property
def name(self):
return self._fs_node.base_name
@property
def title(self):
return self._meta['.title']
@property
def desc(self):
return self._meta['.desc']
def __iter__(self):
return iter(self._get_content().keys())
def __getitem__(self, item):
try:
return self._get_content()[item]
except KeyError:
# Maybe a link? Redirect!
(gallery_name, photo_name) = self._links[item]
return self._collection()[gallery_name][photo_name]
def __len__(self):
return len(self._get_content())
def _get_content(self):
content_mtime_now = self._fs_node.stat.st_mtime
if (self._content_mtime is None) or \
(self._content_mtime < content_mtime_now):
content = {}
links = {}
for name in self._fs_node:
# Grab the file extension and analyse
if '.' not in name:
continue
(_, ext) = name.rsplit('.',1)
if ext.lower() not in ('jpg', 'jpe', 'jpeg', 'gif',
'png', 'tif', 'tiff', 'bmp',):
continue
node = self._fs_node[name]
if node.is_link:
if not os.path.exists(node.abs_target):
# Broken link
continue
# Symbolic link, get the destination relative to the
# root directory.
(gallery_name, photo_name) = os.path.split(
os.path.relpath(node.abs_target,
self._fs_node.parent.abs_path))
# Check the gallery actually exists!
if not gallery_name:
# In the root itself, not valid!
continue
if os.path.sep in gallery_name:
# Deeply nested or above the root, not valid!
continue
if gallery_name == os.path.pardir:
# In directory above root, not valid!
continue
# All good
links[name] = (gallery_name, photo_name)
continue
# This is a photo.
content[name] = Photo(self, self._fs_node[name])
self._links = links
self._content = OrderedDict(sorted(content.items(),
key=lambda i : i[0]))
self._content_mtime = content_mtime_now
self._content_prev_order = None
self._content_next_order = None
return self._content
def _get_prev(self, name):
if self._content_prev_order is None:
content = list(self._get_content().keys())
self._content_prev_order = dict(zip(
content[1:], content[:-1]))
return self._content_prev_order[name]
def _get_next(self, name):
if self._content_next_order is None:
content = list(self._get_content().keys())
self._content_next_order = dict(zip(
content[:-1], content[1:]))
return self._content_next_order[name]
@property
def _meta(self):
return self._meta_cache[self._fs_node.join(GALLERY_META_FILE)]
@coroutine
def get_resized(self, photo, width=None, height=None, quality=60,
rotation=0.0, img_format=None, orientation=0):
if isinstance(photo, Photo):
orientation = photo.orientation
photo = photo.name
result = yield self._resizer_pool.get_resized(
gallery=self.name, photo=photo, width=width, height=height,
quality=quality, rotation=rotation, img_format=img_format,
orientation=orientation)
raise Return(result)
@property
def first(self):
return list(self._get_content().keys())[0]
@property
def last(self):
return list(self._get_content().keys())[-1]
@property
def meta(self):
return {
'name': self.name,
'title': self.title,
'desc': self.desc,
'content': list(self.keys())
}
# Collection services
@property
def _meta_cache(self):
return self._collection()._meta_cache
@property
def _fs_cache(self):
return self._collection()._fs_cache
@property
def _resizer_pool(self):
return self._collection()._resizer_pool