1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Static Resource Delivery
19 ========================
20
21 This service provides static delivery helpers. It's based on the
22 `resource service`_, which must be loaded (read: configured) before this
23 one.
24
25 .. _resource service: `wtf.app.services.resource.ResourceService`
26 """
27 __author__ = u"Andr\xe9 Malo"
28 __docformat__ = "restructuredtext en"
29
30 import datetime as _datetime
31 import os as _os
32 import sys as _sys
33
34 from wtf.app.decorators import Method
35 from wtf import services as _services
36 from wtf import stream as _stream
37
38 from __svc__.wtf import resource as _resource
42 """
43 Static delivery controller implementation
44
45 :IVariables:
46 - `_svc`: The service object
47 - `_resource`: The resource list (``[Resource, ...]``)
48 - `_group`: The regex group
49
50 :Types:
51 - `_svc`: `StaticService`
52 - `_resource`: ``list``
53 - `_group`: ``unicode``
54 """
55 - def __init__(self, svc, resource, group):
56 """
57 Initialization
58
59 :Parameters:
60 - `svc`: Service
61 - `resource`: Resource name
62 - `group`: Regex group
63
64 :Types:
65 - `svc`: `StaticService`
66 - `resource`: ``unicode``
67 - `group`: ``unicode``
68 """
69 self._svc = svc
70 self._resource = _resource[resource]
71 self._group = group
72
73 @Method('GET')
95
98 """
99 Response hint factory collection
100
101 :IVariables:
102 `_env` : ``dict``
103 ENV update dict
104 """
105
107 """
108 Initialization
109
110 :Parameters:
111 `svc` : `StaticService`
112 StaticService instance
113
114 `x_sendfile` : ``str``
115 X-Sendfile header name or ``None``
116 """
117 x_sendfile = x_sendfile and str(x_sendfile.strip()) or None
118 def sendfile(response):
119 """ Response factory for ``sendfile`` """
120 return self._sendfile_factory(response, svc, x_sendfile)
121
122 self._env = {
123 'wtf.response.sendfile': sendfile,
124 }
125
127 """
128 Update request environment
129
130 :Parameters:
131 `env` : ``dict``
132 The environment to update
133
134 :Return: The env dict again (MAY be a copy)
135 :Rtype: ``dict``
136 """
137 env.update(self._env)
138 return env
139
141 """
142 Response factory for ``sendfile``
143
144 :Parameters:
145 `response` : `wtf.app.response.Response`
146 Response object
147
148 `svc` : `StaticService`
149 Static service instance
150
151 `x_sendfile` : ``str``
152 X-Sendfile header name or ``None``
153
154 :Return: The ``sendfile`` callable
155 :Rtype: ``callable``
156 """
157 def sendfile(name, content_type=None, charset=None, expiry=None,
158 audience=None, local=True):
159 """
160 Conditional sendfile mechanism
161
162 If configured with::
163
164 [static]
165 x_sendfile = 'X-Sendfile'
166
167 The file is not passed but submitted to the gateway with the
168 ``X-Sendfile`` header containing the filename, but no body.
169 Otherwise the stream is passed directly. In order to make the
170 filename passing work, the gateway must be configured to do
171 something with the header!
172
173 :Parameters:
174 `name` : ``str``
175 Filename to send
176
177 `content_type` : ``str``
178 Optional content type. If it's the empty string, the mime
179 types file is queried. If it's ``None``, it's not touched.
180
181 `charset` : ``str``
182 Optional charset
183
184 `expiry` : ``int``
185 Expire time in seconds from now
186
187 `audience` : ``str``
188 Caching audience (``private`` or ``public``)
189
190 `local` : ``bool``
191 Is this file local (vs. remote only on the gateway)? If true,
192 content length and last modified time are determined here.
193 If ``x_sendfile`` is not configured, this flag is ignored.
194
195 :Return: Iterable delivering the stream
196 :Rtype: ``iterable``
197
198 :Exceptions:
199 - `Exception` : Anything happened
200 """
201 if local or not x_sendfile:
202 stat = _os.stat(name)
203 response.last_modified(_datetime.datetime.utcfromtimestamp(
204 stat.st_mtime
205 ))
206 response.content_length(stat.st_size)
207
208 if expiry is not None:
209 response.cache(expiry, audience=audience)
210 if content_type == '':
211 content_type = svc.mime_type(name)
212 response.content_type(content_type, charset=charset)
213 if x_sendfile:
214 response.headers.set(x_sendfile, name)
215 return ()
216
217 stream = file(name, 'rb')
218 try:
219 return _stream.GenericStream(stream, blockiter=0)
220 except:
221 e = _sys.exc_info()
222 try:
223 stream.close()
224 finally:
225 try:
226 raise e[0], e[1], e[2]
227 finally:
228 del e
229
230 return sendfile
231
234 """
235 Static middleware - provides ``response.sendfile``
236
237 :IVariables:
238 `_func` : ``callable``
239 Next WSGI handler
240
241 `_factory` : `ResponseFactory`
242 Response factory
243 """
244
245 - def __init__(self, svc, x_sendfile, func):
246 """
247 Initialization
248
249 :Parameters:
250 `svc` : `StaticService`
251 The static service instance
252
253 `x_sendfile` : ``str``
254 X-Sendfile-Header name or ``None``
255
256 `func` : ``callable``
257 Next WSGI handler
258 """
259 self._factory = ResponseFactory(svc, x_sendfile)
260 self._func = func
261
262 - def __call__(self, environ, start_response):
263 """
264 Middleware handler
265
266 :Parameters:
267 `environ` : ``dict``
268 WSGI environment
269
270 `start_response` : ``callable``
271 Start response callable
272
273 :Return: WSGI response iterable
274 :Rtype: ``iterable``
275 """
276 environ = self._factory.update_env(environ)
277 return self._func(environ, start_response)
278
281 """ Actual global service object for static delivery """
282
284 """ Initialization """
285 self._svc = svc
286
288 """
289 Factory for a simple static file delivery controller
290
291 If `group` is set and not ``None``, the controller assumes
292 to be attached to a dynamic map and grabs
293 ``request.match.group(group)`` as the (relative) filename to
294 deliver. If it's ``None``, the filename resulting from ``request.url``
295 is used.
296
297 :Parameters:
298 - `resource`: Resource token configured for the base
299 directory/directories of the delivered files. The directories
300 are tried in order.
301 - `group`: Regex group
302
303 :Types:
304 - `resource`: ``str``
305 - `group`: ``unicode`` or ``int``
306
307 :return: Delivery controller
308 :rtype: ``callable``
309 """
310 return Controller(self._svc, resource, group)
311
314 """
315 Static resources delivery
316 """
317 __implements__ = [_services.ServiceInterface]
318
319 - def __init__(self, config, opts, args):
320 """
321 Initialization
322
323 :See: `wtf.services.ServiceInterface.__init__`
324 """
325 default, typefiles = u'application/octet-stream', ()
326 if 'static' in config:
327 default = config.static('default_type', default)
328 typefiles = config.static('mime_types', typefiles)
329 x_sendfile = config.static('x_sendfile', None) or None
330 else:
331 x_sendfile = None
332 self._x_sendfile = x_sendfile
333 self.mime_type = MimeTypes(default.encode('utf-8'), typefiles)
334
336 """ :See: `wtf.services.ServiceInterface.shutdown` """
337 pass
338
340 """ :See: `wtf.services.ServiceInterface.global_service` """
341 return 'wtf.static', GlobalStatic(self)
342
344 """ :See: `wtf.services.ServiceInterface.middleware` """
345 return Middleware(self, self._x_sendfile, func)
346
349 """
350 MIME type query object
351
352 :IVariables:
353 - `_types`: Extension->Type mapping (``{u'ext': 'type', ...}``)
354 - `_default`: Default type
355
356 :Types:
357 - `_types`: ``dict``
358 - `_default`: ``str``
359 """
360
362 """
363 Initialization
364
365 :Parameters:
366 - `default`: Default type
367 - `filelist`: List of files of mime.type format
368
369 :Types:
370 - `default`: ``str``
371 - `filelist`: ``iterable``
372 """
373 types = {}
374 for name in filelist:
375 types.update(self._parse(_resource(name, isfile=True)))
376 self._types = types
377 self._default = default
378
380 """
381 Parse a single mime.types file
382
383 :Parameters:
384 - `resource`: file resource
385
386 :Types:
387 - `resource`: `wtf.app.services.resource.FileResource`
388
389 :return: Ext->Type mapping (``{u'ext': 'type', ...}``)
390 :rtype: ``dict``
391 """
392 types = {}
393 stream = resource.open()
394 try:
395 for line in stream:
396 parts = line.decode('utf-8').split()
397 if len(parts) > 1:
398 mtype, exts = parts[0], parts[1:]
399 types.update(dict(
400 (ext.decode('utf-8'), mtype.encode('utf-8'))
401 for ext in exts
402 ))
403 finally:
404 stream.close()
405 return types
406
407 - def __call__(self, filename, default=None):
408 """
409 Retrieve MIME type for a file
410
411 :Parameters:
412 - `filename`: filename to inspect
413 - `default`: Default type override
414
415 :Types:
416 - `filename`: ``unicode``
417 - `default`: ``str``
418
419 :return: The mime type
420 :rtype: ``str``
421 """
422 if default is None:
423 default = self._default
424 parts = _os.path.basename(_os.path.normpath(unicode(
425 filename).encode('utf-8'))).decode('utf-8').split(u'.')
426 parts.reverse()
427 while not parts[-1]:
428 parts.pop()
429 parts.pop()
430 mtype = default
431 while parts:
432 mtype = self._types.get(parts.pop(), mtype)
433 return mtype
434