MoinMoin/action/cache.py
author Eugene Syromyatnikov <evgsyr@gmail.com>
Fri, 04 Jun 2010 21:13:01 +0400
changeset 3896 f8871116c6b3
parent 3772 78c452cf0257
permissions -rw-r--r--
Fixed missed wikiutil module import.
     1 # -*- coding: iso-8859-1 -*-
     2 """
     3     MoinMoin - Send a raw object from the caching system (and offer utility
     4     functions to put data into cache, calculate cache key, etc.).
     5 
     6     Sample usage
     7     ------------
     8     Assume we have a big picture (bigpic) and we want to efficiently show some
     9     thumbnail (thumbpic) for it:
    10 
    11     # first calculate a (hard to guess) cache key (this key will change if the
    12     # original data (bigpic) changes):
    13     key = cache.key(..., attachname=bigpic, ...)
    14 
    15     # check if we don't have it in cache yet
    16     if not cache.exists(..., key):
    17         # if we don't have it in cache, we need to render it - this is an
    18         # expensive operation that we want to avoid by caching:
    19         thumbpic = render_thumb(bigpic)
    20         # put expensive operation's results into cache:
    21         cache.put(..., key, thumbpic, ...)
    22 
    23     url = cache.url(..., key)
    24     html = '<img src="%s">' % url
    25 
    26     @copyright: 2008 MoinMoin:ThomasWaldmann
    27     @license: GNU GPL, see COPYING for details.
    28 """
    29 
    30 import hmac, sha
    31 
    32 from MoinMoin import log
    33 logging = log.getLogger(__name__)
    34 
    35 # keep both imports below as they are, order is important:
    36 from MoinMoin import wikiutil
    37 import mimetypes
    38 
    39 from MoinMoin import config, caching
    40 from MoinMoin.util import filesys
    41 from MoinMoin.action import AttachFile
    42 
    43 action_name = __name__.split('.')[-1]
    44 
    45 # Do NOT get this directly from request.form or user would be able to read any cache!
    46 cache_arena = 'sendcache'  # just using action_name is maybe rather confusing
    47 
    48 # We maybe could use page local caching (not 'wiki' global) to have less directory entries.
    49 # Local is easier to automatically cleanup if an item changes. Global is easier to manually cleanup.
    50 # Local makes data_dir much larger, harder to backup.
    51 cache_scope = 'wiki'
    52 
    53 do_locking = False
    54 
    55 def key(request, wikiname=None, itemname=None, attachname=None, content=None, secret=None):
    56     """
    57     Calculate a (hard-to-guess) cache key.
    58 
    59     Important key properties:
    60     * The key must be hard to guess (this is because do=get does no ACL checks,
    61       so whoever got the key [e.g. from html rendering of an ACL protected wiki
    62       page], will be able to see the cached content.
    63     * The key must change if the (original) content changes. This is because
    64       ACLs on some item may change and even if somebody was allowed to see some
    65       revision of some item, it does not implicate that he is allowed to see
    66       any other revision also. There will be no harm if he can see exactly the
    67       same content again, but there could be harm if he could access a revision
    68       with different content.
    69 
    70     If content is supplied, we will calculate and return a hMAC of the content.
    71 
    72     If wikiname, itemname, attachname is given, we don't touch the content (nor do
    73     we read it ourselves from the attachment file), but we just calculate a key
    74     from the given metadata values and some metadata we get from the filesystem.
    75 
    76     Hint: if you need multiple cache objects for the same source content (e.g.
    77           thumbnails of different sizes for the same image), calculate the key
    78           only once and then add some different prefixes to it to get the final
    79           cache keys.
    80 
    81     @param request: the request object
    82     @param wikiname: the name of the wiki (if not given, will be read from cfg)
    83     @param itemname: the name of the page
    84     @param attachname: the filename of the attachment
    85     @param content: content data as unicode object (e.g. for page content or
    86                     parser section content)
    87     @param secret: secret for hMAC calculation (default: use secret from cfg)
    88     """
    89     if secret is None:
    90         secret = request.cfg.secrets
    91     if content:
    92         hmac_data = content
    93     elif itemname is not None and attachname is not None:
    94         wikiname = wikiname or request.cfg.interwikiname or request.cfg.siteid
    95         fuid = filesys.fuid(AttachFile.getFilename(request, itemname, attachname))
    96         hmac_data = u''.join([wikiname, itemname, attachname, repr(fuid)])
    97     else:
    98         raise AssertionError('cache_key called with unsupported parameters')
    99 
   100     hmac_data = hmac_data.encode('utf-8')
   101     key = hmac.new(secret, hmac_data, sha).hexdigest()
   102     return key
   103 
   104 
   105 def put(request, key, data,
   106         filename=None,
   107         content_type=None,
   108         content_disposition=None,
   109         content_length=None,
   110         last_modified=None,
   111         original=None):
   112     """
   113     Put an object into the cache to send it with cache action later.
   114 
   115     @param request: the request object
   116     @param key: non-guessable key into cache (str)
   117     @param data: content data (str or open file-like obj)
   118     @param filename: filename for content-disposition header and for autodetecting
   119                      content_type (unicode, default: None)
   120     @param content_type: content-type header value (str, default: autodetect from filename)
   121     @param content_disposition: type for content-disposition header (str, default: None)
   122     @param content_length: data length for content-length header (int, default: autodetect)
   123     @param last_modified: last modified timestamp (int, default: autodetect)
   124     @param original: location of original object (default: None) - this is just written to
   125                      the metadata cache "as is" and could be used for cache cleanup,
   126                      use (wikiname, itemname, attachname or None))
   127     """
   128     import os.path
   129     from MoinMoin.util import timefuncs
   130 
   131     if filename:
   132         # make sure we just have a simple filename (without path)
   133         filename = os.path.basename(filename)
   134 
   135         if content_type is None:
   136             # try autodetect
   137             mt, enc = mimetypes.guess_type(filename)
   138             if mt:
   139                 content_type = mt
   140 
   141     if content_type is None:
   142         content_type = 'application/octet-stream'
   143 
   144     data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
   145     data_cache.update(data)
   146     content_length = content_length or data_cache.size()
   147     last_modified = last_modified or data_cache.mtime()
   148 
   149     httpdate_last_modified = timefuncs.formathttpdate(int(last_modified))
   150     headers = ['Content-Type: %s' % content_type,
   151                'Last-Modified: %s' % httpdate_last_modified,
   152                'Content-Length: %s' % content_length,
   153               ]
   154     if content_disposition and filename:
   155         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
   156         # There is no solution that is compatible to IE except stripping non-ascii chars
   157         filename = filename.encode(config.charset)
   158         headers.append('Content-Disposition: %s; filename="%s"' % (content_disposition, filename))
   159 
   160     meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
   161     meta_cache.update({
   162         'httpdate_last_modified': httpdate_last_modified,
   163         'last_modified': last_modified,
   164         'headers': headers,
   165         'original': original,
   166     })
   167 
   168 
   169 def exists(request, key, strict=False):
   170     """
   171     Check if a cached object for this key exists.
   172 
   173     @param request: the request object
   174     @param key: non-guessable key into cache (str)
   175     @param strict: if True, also check the data cache, not only meta (bool, default: False)
   176     @return: is object cached? (bool)
   177     """
   178     if strict:
   179         data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
   180         data_cached = data_cache.exists()
   181     else:
   182         data_cached = True  # we assume data will be there if meta is there
   183 
   184     meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
   185     meta_cached = meta_cache.exists()
   186 
   187     return meta_cached and data_cached
   188 
   189 
   190 def remove(request, key):
   191     """ delete headers/data cache for key """
   192     meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
   193     meta_cache.remove()
   194     data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
   195     data_cache.remove()
   196 
   197 
   198 def url(request, key, do='get'):
   199     """ return URL for the object cached for key """
   200     return "%s/?%s" % (
   201         request.getScriptname(),
   202         wikiutil.makeQueryString(dict(action=action_name, do=do, key=key), want_unicode=False))
   203 
   204 
   205 def _get_headers(request, key):
   206     """ get last_modified and headers cached for key """
   207     meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
   208     meta = meta_cache.content()
   209     return meta['httpdate_last_modified'], meta['headers']
   210 
   211 
   212 def _get_datafile(request, key):
   213     """ get an open data file for the data cached for key """
   214     data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
   215     data_cache.open(mode='r')
   216     return data_cache
   217 
   218 
   219 def _do_get(request, key):
   220     """ send a complete http response with headers/data cached for key """
   221     try:
   222         last_modified, headers = _get_headers(request, key)
   223         if request.if_modified_since == last_modified:
   224             request.emit_http_headers(["Status: 304 Not modified"])
   225         else:
   226             data_file = _get_datafile(request, key)
   227             request.emit_http_headers(headers)
   228             request.send_file(data_file)
   229     except caching.CacheError:
   230         request.emit_http_headers(["Status: 404 Not found"])
   231 
   232 
   233 def _do_remove(request, key):
   234     """ delete headers/data cache for key """
   235     remove(request, key)
   236     request.emit_http_headers(["Status: 200 OK"])
   237 
   238 
   239 def _do(request, do, key):
   240     if do == 'get':
   241         _do_get(request, key)
   242     elif do == 'remove':
   243         _do_remove(request, key)
   244 
   245 def execute(pagename, request):
   246     do = request.form.get('do', [None])[0]
   247     key = request.form.get('key', [None])[0]
   248     _do(request, do, key)
   249