Changeset 12

Show
Ignore:
Timestamp:
09/27/08 21:44:46 (2 years ago)
Author:
Jonathan Jacobs <korpse@…>
Message:

#1, #2: Overhaul Sirius.

Files:
1 added
3 modified

Legend:

Unmodified
Added
Removed
  • sirius/cache.py

    r8 r12  
    1 from axiom.attributes import bytes 
     1import sha 
     2from datetime import timedelta 
     3from zope.interface import implements 
     4 
     5from epsilon.extime import Time 
     6 
     7from axiom.attributes import text 
     8from axiom.dependency import dependsOn 
    29from axiom.item import Item 
     10from axiom.scheduler import Scheduler 
    311 
    4 # XXX: cache expiry would be nice 
     12from twisted.python import log 
     13from twisted.internet.defer import succeed 
    514 
    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) 
     15from sirius.isirius import ICacheService 
     16from sirius.util import PerseverantDownloader 
    1517 
    1618 
    17 def getCachedMetadata(store, segments): 
    18     """ 
    19     Retrieve cached metadata. 
     19class LocalCacheService(Item): 
     20    implements(ICacheService) 
    2021 
    21     @type store: C{axiom.store.Store} 
    22     @param store: Site store 
     22    typeName = 'sirius_cache_localcacheservice' 
     23    schemaVersion = 1 
     24    powerupInterfaces = [ICacheService] 
    2325 
    24     @type segments: C{sequence} 
    25     @param segments: Path segments, used to identify the cached item 
     26    scheduler = dependsOn(Scheduler) 
    2627 
    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 
    34112 
    35113 
    36 def cacheMetadata(store, segments, **kw): 
    37     """ 
    38     Store metadata in cache. 
     114class Cached(Item): 
     115    schemaVersion = 1 
     116    typeName = 'sirius_cache_cached' 
    39117 
    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) 
    41121 
    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) 
    44125 
    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  
    11from zope.interface import implements 
    2  
    3 from twisted.python import log 
    4 from twisted.internet.defer import maybeDeferred, succeed 
    52 
    63from axiom import attributes, item 
    74 
    8 from nevow import rend, static 
    9 from nevow.inevow import IResource 
     5from nevow import rend 
    106 
    117from xmantissa.ixmantissa import IPublicPage 
    128 
    13 from procyon.api import tvdb 
    14 from procyon.util import PerseverantDownloader, getAPIKey 
    159 
    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. 
    8313 
    8414class SiriusPublicPage(item.Item): 
     
    9626 
    9727class 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 
    12030 
    12131    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  
    33from xmantissa import website, offering 
    44 
    5 from sirius import publicpage 
     5from sirius import publicpage, cache 
    66 
    77 
    88plugin = offering.Offering( 
    99    name = u'Sirius', 
    10     description = u'Media cache for Procyon', 
     10    description = u'Local cache for Procyon', 
    1111 
    1212    siteRequirements = [ 
     
    1616    appPowerups = [ 
    1717        publicpage.SiriusPublicPage, 
     18        cache.LocalCacheService, 
    1819        ], 
    1920