1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - Send a raw object from the caching system (and offer utility
4
functions to put data into cache, calculate cache key, etc.).
8
Assume we have a big picture (bigpic) and we want to efficiently show some
9
thumbnail (thumbpic) for it:
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, ...)
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, ...)
23
url = cache.url(..., key)
24
html = '<img src="%s">' % url
26
@copyright: 2008 MoinMoin:ThomasWaldmann
27
@license: GNU GPL, see COPYING for details.
32
from MoinMoin import log
33
logging = log.getLogger(__name__)
35
# keep both imports below as they are, order is important:
36
from MoinMoin import wikiutil
39
from MoinMoin import config, caching
40
from MoinMoin.util import filesys
41
from MoinMoin.action import AttachFile
43
action_name = __name__.split('.')[-1]
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
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.
55
def key(request, wikiname=None, itemname=None, attachname=None, content=None, secret=None):
57
Calculate a (hard-to-guess) cache key.
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.
70
If content is supplied, we will calculate and return a hMAC of the content.
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.
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
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)
90
secret = request.cfg.secrets
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)])
98
raise AssertionError('cache_key called with unsupported parameters')
100
hmac_data = hmac_data.encode('utf-8')
101
key = hmac.new(secret, hmac_data, sha).hexdigest()
105
def put(request, key, data,
108
content_disposition=None,
113
Put an object into the cache to send it with cache action later.
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))
129
from MoinMoin.util import timefuncs
132
# make sure we just have a simple filename (without path)
133
filename = os.path.basename(filename)
135
if content_type is None:
137
mt, enc = mimetypes.guess_type(filename)
141
if content_type is None:
142
content_type = 'application/octet-stream'
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()
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,
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))
160
meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
162
'httpdate_last_modified': httpdate_last_modified,
163
'last_modified': last_modified,
165
'original': original,
169
def exists(request, key, strict=False):
171
Check if a cached object for this key exists.
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)
179
data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
180
data_cached = data_cache.exists()
182
data_cached = True # we assume data will be there if meta is there
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()
187
return meta_cached and data_cached
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)
194
data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
198
def url(request, key, do='get'):
199
""" return URL for the object cached for key """
201
request.getScriptname(),
202
wikiutil.makeQueryString(dict(action=action_name, do=do, key=key), want_unicode=False))
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']
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')
219
def _do_get(request, key):
220
""" send a complete http response with headers/data cached for key """
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"])
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"])
233
def _do_remove(request, key):
234
""" delete headers/data cache for key """
236
request.emit_http_headers(["Status: 200 OK"])
239
def _do(request, do, key):
241
_do_get(request, key)
243
_do_remove(request, key)
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)