Changeset 12
- Timestamp:
- 09/27/08 21:44:46 (2 years ago)
- Files:
-
- 1 added
- 3 modified
-
sirius/cache.py (modified) (1 diff)
-
sirius/publicpage.py (modified) (2 diffs)
-
sirius/util.py (added)
-
xmantissa/plugins/siriusoff.py (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sirius/cache.py
r8 r12 1 from axiom.attributes import bytes 1 import sha 2 from datetime import timedelta 3 from zope.interface import implements 4 5 from epsilon.extime import Time 6 7 from axiom.attributes import text 8 from axiom.dependency import dependsOn 2 9 from axiom.item import Item 10 from axiom.scheduler import Scheduler 3 11 4 # XXX: cache expiry would be nice 12 from twisted.python import log 13 from twisted.internet.defer import succeed 5 14 6 class CachedMetadata(Item): 7 """ 8 Persistent copy of a cached item's metadata. 9 """ 10 schemaVersion = 1 11 typeName = 'sirius_cache_cachedmetada' 12 13 path = bytes(allowNone=False) 14 contentType = bytes(allowNone=False) 15 from sirius.isirius import ICacheService 16 from sirius.util import PerseverantDownloader 15 17 16 18 17 def getCachedMetadata(store, segments): 18 """ 19 Retrieve cached metadata. 19 class LocalCacheService(Item): 20 implements(ICacheService) 20 21 21 @type store: C{axiom.store.Store} 22 @param store: Site store 22 typeName = 'sirius_cache_localcacheservice' 23 schemaVersion = 1 24 powerupInterfaces = [ICacheService] 23 25 24 @type segments: C{sequence} 25 @param segments: Path segments, used to identify the cached item 26 scheduler = dependsOn(Scheduler) 26 27 27 @rtype: L{CachedMetadata} or C{None} 28 @return: The cached item's metadata or C{None} if it is not cached 29 """ 30 path = '/'.join(segments) 31 return store.findUnique(CachedMetadata, 32 CachedMetadata.path == path, 33 default=None) 28 def _getCachePath(self, cached): 29 """ 30 Get a C{twisted.python.filepath.FilePath} for the data of C{cached}. 31 """ 32 return self.store.newFilePath(u'cache', sha.sha(cached.location).hexdigest()) 33 34 def _cacheData(self, location, contentType, data, expiry=None): 35 """ 36 Cache C{location} and its data. 37 38 @type location: C{unicode} 39 @param location: The location of the original item 40 41 @type contentType: C{unicode} 42 @param contentType: The content type of the original item, expressed as 43 a MIME type 44 45 @type data: C{str} 46 @param data: The data to cache 47 48 @type expiry: C{datetime.timedelta} or C{None} 49 @param expiry: The amount of time that should be allowed to pass before 50 the cached item expires, this defaults to 30 days 51 52 @rtype: L{Cached} 53 @return: The newly cached item 54 """ 55 cached = Cached(store=self.store, 56 location=location, 57 contentType=contentType) 58 59 if expiry is None: 60 expiry = timedelta(days=30) 61 62 self.setExpiriation(cached, expiry) 63 64 fp = self._getCachePath(cached) 65 if fp.exists(): 66 fp.remove() 67 68 parent = fp.parent() 69 if not parent.exists(): 70 fp.parent().makedirs() 71 72 fd = fp.create() 73 fd.write(data) 74 fd.close() 75 76 return cached 77 78 def setExpiriation(self, cached, delta): 79 self.scheduler.unscheduleAll(cached) 80 81 print type(delta), delta 82 83 when = Time() + delta 84 self.scheduler.schedule(cached, when) 85 86 log.msg('Expiring %r at %s.' % (cached.location, when.asHumanly())) 87 88 def getCached(self, location): 89 # XXX: For now, `location` really has to be a URL. 90 location = unicode(location) 91 cached = self.store.findFirst(Cached, 92 Cached.location == location) 93 94 def _cacheData((data, headers)): 95 contentType = unicode(headers.get('content-type', ['application/octet-stream'])[0]) 96 return self._cacheData(location, contentType, data) 97 98 if cached is not None: 99 d = succeed(cached) 100 else: 101 d = PerseverantDownloader(location).go( 102 ).addCallback(_cacheData 103 ).addErrback(log.err) 104 105 return d 106 107 def getData(self, cached): 108 fd = self._getCachePath(cached).open() 109 data = fd.read() 110 fd.close() 111 return data 34 112 35 113 36 def cacheMetadata(store, segments, **kw):37 """38 Store metadata in cache.114 class Cached(Item): 115 schemaVersion = 1 116 typeName = 'sirius_cache_cached' 39 117 40 Additional keyword arguments are passed to L{CachedMetadata}. 118 location = text(doc=""" 119 The original location of the item that has been cached. 120 """, allowNone=False) 41 121 42 @type store: C{axiom.store.Store} 43 @param store: Site store 122 contentType = text(doc=""" 123 The original data's content type, expressed as a MIME type. 124 """, allowNone=False) 44 125 45 @type segments: C{sequence} 46 @param segments: Path segments, used to identify the cached item 47 48 @rtype: L{CachedMetadata} 49 @return: The newly created item 50 """ 51 path = '/'.join(segments) 52 store.query(CachedMetadata, CachedMetadata.path == path).deleteFromStore() 53 return CachedMetadata(store=store, 54 path=path, 55 **kw) 126 def run(self): 127 # By returning `None` the scheduler will delete the runnable 128 # (this item) from the store and effectively invalidate the cache 129 # for us. 130 print 'BLAM!' 131 return None -
sirius/publicpage.py
r8 r12 1 1 from zope.interface import implements 2 3 from twisted.python import log4 from twisted.internet.defer import maybeDeferred, succeed5 2 6 3 from axiom import attributes, item 7 4 8 from nevow import rend, static 9 from nevow.inevow import IResource 5 from nevow import rend 10 6 11 7 from xmantissa.ixmantissa import IPublicPage 12 8 13 from procyon.api import tvdb14 from procyon.util import PerseverantDownloader, getAPIKey15 9 16 from sirius.cache import cacheMetadata, getCachedMetadata 17 18 19 class Cache(object): 20 """ 21 Base class for cache handlers. 22 23 @type apiKeyName: C{unicode} or C{None} 24 @cvar apiKeyName: Name of the key to use when retrieving an API key, 25 or C{None} 26 27 @type store: C{axiom.store.Store} 28 """ 29 apiKeyName = None 30 31 def __init__(self, store): 32 self.store = store 33 34 @property 35 def apiKey(self): 36 """ 37 Retrieve the API key for L{self.apiKeyName} if specified. 38 """ 39 if self.apiKeyName is not None: 40 return getAPIKey(self.store, self.apiKeyName) 41 return None 42 43 def getResourceData(self, segments): 44 """ 45 Retrieve data and metadata to cache. 46 47 @type segments: C{sequence} 48 @param segments: Cache path segments 49 50 @rtype: C{twisted.internet.defer.Deferred} firing with C{(str, dict)} 51 @return: A deferred that fires with C{(resourceData, headers)} 52 """ 53 raise NotImplementedError() 54 55 56 # XXX: caching XML data (like languages) would be nice 57 58 class TVDBCache(Cache): 59 """ 60 Cache handler for "The TVDB" resources. 61 """ 62 apiKeyName = u'tvdb' 63 64 def getResourceData(self, segments): 65 if len(segments) != 3 or segments[1] not in ('banners', 'episodes'): 66 return rend.NotFound 67 68 resourceType, resourceID = segments[1], segments[2] 69 d = tvdb.locateMirror(self.apiKey) 70 71 if resourceType == 'banners': 72 d = d.addCallback(lambda conn: conn.getSeriesByID(resourceID) 73 ).addCallback(lambda series: series.bannerURL) 74 elif resourceType == 'episodes': 75 d = d.addCallback(lambda conn: conn.getEpisodeByID(resourceID) 76 ).addCallback(lambda ep: ep.imageURL) 77 else: 78 log.msg('Unknown TVDB resource type: %r' % (resourceType,)) 79 return rend.NotFound 80 81 return d.addCallback(lambda url: PerseverantDownloader(url).go()) 82 10 # XXX: What to do with this? Perhaps some kind of web-facing cache accessor 11 # could be useful? e.g. http://.../Sirius/http/www.somesite.com/foo/bar.jpg 12 # Although there is no use-case for this at the moment. 83 13 84 14 class SiriusPublicPage(item.Item): … … 96 26 97 27 class PublicIndexPage(rend.Page): 98 cacheHandlers = { 99 'tvdb': TVDBCache, 100 } 101 102 def __init__(self, store): 103 self.store = store 104 105 def handleError(self, f): 106 log.msg('Error retrieving cache media:') 107 log.err(f) 108 return rend.NotFound 109 110 def cacheMedia(self, (data, headers), segments): 111 contentType = headers.get('content-type', [None])[0] 112 if contentType is not None: 113 cm = cacheMetadata(self.store, segments, contentType=contentType) 114 else: 115 cm = None 116 117 fd = self.store.newFile(*segments) 118 fd.write(data) 119 return fd.close().addCallback(lambda fp: (fp, cm)) 28 def __init__(self, appStore): 29 self.appStore = appStore 120 30 121 31 def locateChild(self, ctx, segments): 122 if not segments or not segments[-1]: 123 return rend.NotFound 124 125 fp = self.store.newFilePath(*segments) 126 cm = getCachedMetadata(self.store, segments) 127 if fp.exists() and cm is not None: 128 log.msg('Using cached media: %r' % (segments,)) 129 d = succeed((fp, cm)) 130 else: 131 cacheType = segments[0] 132 handler = self.cacheHandlers.get(cacheType) 133 if handler is None: 134 log.msg('Unknown cache type: %r' % (cacheType,)) 135 return rend.NotFound 136 137 log.msg('Updating media cache: %r' % (segments,)) 138 139 d = maybeDeferred(handler(self.store).getResourceData, segments 140 ).addCallback(self.cacheMedia, segments) 141 142 return d.addCallback(lambda (fp, cm): (static.File(fp.path, defaultType=cm.contentType), []) 143 ).addErrback(self.handleError) 32 return rend.NotFound -
xmantissa/plugins/siriusoff.py
r2 r12 3 3 from xmantissa import website, offering 4 4 5 from sirius import publicpage 5 from sirius import publicpage, cache 6 6 7 7 8 8 plugin = offering.Offering( 9 9 name = u'Sirius', 10 description = u' Mediacache for Procyon',10 description = u'Local cache for Procyon', 11 11 12 12 siteRequirements = [ … … 16 16 appPowerups = [ 17 17 publicpage.SiriusPublicPage, 18 cache.LocalCacheService, 18 19 ], 19 20
