Package wtf :: Package app :: Package services :: Module static
[hide private]
[frames] | no frames]

Source Code for Module wtf.app.services.static

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2007-2012 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 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 
39 40 41 -class Controller(object):
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')
74 - def __call__(self, request, response):
75 """ Actual controller implementation """ 76 if self._group is None: 77 filename = request.url.path 78 else: 79 filename = request.match.group(self._group) 80 81 for rsc in self._resource: 82 try: 83 stream = rsc.open(filename, blockiter=0) 84 except IOError: 85 continue 86 else: 87 break 88 else: 89 response.raise_error(404) 90 91 response.content_length(len(stream)) 92 response.last_modified(stream.last_modified) 93 response.content_type(self._svc.mime_type(filename)) 94 return stream
95
96 97 -class ResponseFactory(object):
98 """ 99 Response hint factory collection 100 101 :IVariables: 102 `_env` : ``dict`` 103 ENV update dict 104 """ 105
106 - def __init__(self, svc, x_sendfile):
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
126 - def update_env(self, env):
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
140 - def _sendfile_factory(self, response, svc, x_sendfile):
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: # pylint: disable = W0702 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
232 233 -class Middleware(object):
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
279 280 -class GlobalStatic(object):
281 """ Actual global service object for static delivery """ 282
283 - def __init__(self, svc):
284 """ Initialization """ 285 self._svc = svc
286
287 - def controller(self, resource, group=None):
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
312 313 -class StaticService(object):
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
335 - def shutdown(self):
336 """ :See: `wtf.services.ServiceInterface.shutdown` """ 337 pass
338
339 - def global_service(self):
340 """ :See: `wtf.services.ServiceInterface.global_service` """ 341 return 'wtf.static', GlobalStatic(self)
342
343 - def middleware(self, func):
344 """ :See: `wtf.services.ServiceInterface.middleware` """ 345 return Middleware(self, self._x_sendfile, func)
346
347 348 -class MimeTypes(object):
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
361 - def __init__(self, default, filelist):
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
379 - def _parse(self, resource):
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