comparison MoinMoin/action/cache.py @ 3882:c8ffd029ab1f

action cache (and tests), backported from 1.8
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 19 Jul 2008 16:11:19 +0200
parents
children 9e40b4ecf68f 85c7b3b9c48c
comparison
equal deleted inserted replaced
3880:85cd05b8af42 3882:c8ffd029ab1f
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 """
112 Put an object into the cache to send it with cache action later.
113
114 @param request: the request object
115 @param key: non-guessable key into cache (str)
116 @param data: content data (str or open file-like obj)
117 @param filename: filename for content-disposition header and for autodetecting
118 content_type (unicode, default: None)
119 @param content_type: content-type header value (str, default: autodetect from filename)
120 @param content_disposition: type for content-disposition header (str, default: None)
121 @param content_length: data length for content-length header (int, default: autodetect)
122 @param last_modified: last modified timestamp (int, default: autodetect)
123 """
124 import os.path
125 from MoinMoin.util import timefuncs
126
127 if filename:
128 # make sure we just have a simple filename (without path)
129 filename = os.path.basename(filename)
130
131 if content_type is None:
132 # try autodetect
133 mt, enc = mimetypes.guess_type(filename)
134 if mt:
135 content_type = mt
136
137 if content_type is None:
138 content_type = 'application/octet-stream'
139
140 data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
141 data_cache.update(data)
142 content_length = content_length or data_cache.size()
143 last_modified = last_modified or data_cache.mtime()
144
145 last_modified = timefuncs.formathttpdate(int(last_modified))
146 headers = ['Content-Type: %s' % content_type,
147 'Last-Modified: %s' % last_modified,
148 'Content-Length: %s' % content_length,
149 ]
150 if content_disposition and filename:
151 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
152 # There is no solution that is compatible to IE except stripping non-ascii chars
153 filename = filename.encode(config.charset)
154 headers.append('Content-Disposition: %s; filename="%s"' % (content_disposition, filename))
155
156 meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
157 meta_cache.update((last_modified, headers))
158
159
160 def exists(request, key, strict=False):
161 """
162 Check if a cached object for this key exists.
163
164 @param request: the request object
165 @param key: non-guessable key into cache (str)
166 @param strict: if True, also check the data cache, not only meta (bool, default: False)
167 @return: is object cached? (bool)
168 """
169 if strict:
170 data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
171 data_cached = data_cache.exists()
172 else:
173 data_cached = True # we assume data will be there if meta is there
174
175 meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
176 meta_cached = meta_cache.exists()
177
178 return meta_cached and data_cached
179
180
181 def remove(request, key):
182 """ delete headers/data cache for key """
183 meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
184 meta_cache.remove()
185 data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
186 data_cache.remove()
187
188
189 def url(request, key, do='get'):
190 """ return URL for the object cached for key """
191 return "%s/?%s" % (
192 request.getScriptname(),
193 wikiutil.makeQueryString(dict(action=action_name, do=do, key=key), want_unicode=False))
194
195
196 def _get_headers(request, key):
197 """ get last_modified and headers cached for key """
198 meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
199 last_modified, headers = meta_cache.content()
200 return last_modified, headers
201
202
203 def _get_datafile(request, key):
204 """ get an open data file for the data cached for key """
205 data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
206 data_cache.open(mode='r')
207 return data_cache
208
209
210 def _do_get(request, key):
211 """ send a complete http response with headers/data cached for key """
212 try:
213 last_modified, headers = _get_headers(request, key)
214 if request.if_modified_since == last_modified:
215 request.emit_http_headers(["Status: 304 Not modified"])
216 else:
217 data_file = _get_datafile(request, key)
218 request.emit_http_headers(headers)
219 request.send_file(data_file)
220 except caching.CacheError:
221 request.emit_http_headers(["Status: 404 Not found"])
222
223
224 def _do_remove(request, key):
225 """ delete headers/data cache for key """
226 remove(request, key)
227 request.emit_http_headers(["Status: 200 OK"])
228
229
230 def _do(request, do, key):
231 if do == 'get':
232 _do_get(request, key)
233 elif do == 'remove':
234 _do_remove(request, key)
235
236 def execute(pagename, request):
237 do = request.form.get('do', [None])[0]
238 key = request.form.get('key', [None])[0]
239 _do(request, do, key)
240