Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/static.py : 39%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2import json
3import os
5from os.path import getmtime, normcase, normpath, join, isdir, exists
7from pkg_resources import resource_exists, resource_filename, resource_isdir
9from pyramid.asset import abspath_from_asset_spec, resolve_asset_spec
11from pyramid.compat import lru_cache, text_
13from pyramid.httpexceptions import HTTPNotFound, HTTPMovedPermanently
15from pyramid.path import caller_package
17from pyramid.response import _guess_type, FileResponse
19from pyramid.traversal import traversal_path_info
21slash = text_('/')
24class static_view(object):
25 """ An instance of this class is a callable which can act as a
26 :app:`Pyramid` :term:`view callable`; this view will serve
27 static files from a directory on disk based on the ``root_dir``
28 you provide to its constructor.
30 The directory may contain subdirectories (recursively); the static
31 view implementation will descend into these directories as
32 necessary based on the components of the URL in order to resolve a
33 path into a response.
35 You may pass an absolute or relative filesystem path or a
36 :term:`asset specification` representing the directory
37 containing static files as the ``root_dir`` argument to this
38 class' constructor.
40 If the ``root_dir`` path is relative, and the ``package_name``
41 argument is ``None``, ``root_dir`` will be considered relative to
42 the directory in which the Python file which *calls* ``static``
43 resides. If the ``package_name`` name argument is provided, and a
44 relative ``root_dir`` is provided, the ``root_dir`` will be
45 considered relative to the Python :term:`package` specified by
46 ``package_name`` (a dotted path to a Python package).
48 ``cache_max_age`` influences the ``Expires`` and ``Max-Age``
49 response headers returned by the view (default is 3600 seconds or
50 one hour).
52 ``use_subpath`` influences whether ``request.subpath`` will be used as
53 ``PATH_INFO`` when calling the underlying WSGI application which actually
54 serves the static files. If it is ``True``, the static application will
55 consider ``request.subpath`` as ``PATH_INFO`` input. If it is ``False``,
56 the static application will consider request.environ[``PATH_INFO``] as
57 ``PATH_INFO`` input. By default, this is ``False``.
59 .. note::
61 If the ``root_dir`` is relative to a :term:`package`, or is a
62 :term:`asset specification` the :app:`Pyramid`
63 :class:`pyramid.config.Configurator` method can be used to override
64 assets within the named ``root_dir`` package-relative directory.
65 However, if the ``root_dir`` is absolute, configuration will not be able
66 to override the assets it contains.
67 """
69 def __init__(
70 self,
71 root_dir,
72 cache_max_age=3600,
73 package_name=None,
74 use_subpath=False,
75 index='index.html',
76 ):
77 # package_name is for bw compat; it is preferred to pass in a
78 # package-relative path as root_dir
79 # (e.g. ``anotherpackage:foo/static``).
80 self.cache_max_age = cache_max_age
81 if package_name is None:
82 package_name = caller_package().__name__
83 package_name, docroot = resolve_asset_spec(root_dir, package_name)
84 self.use_subpath = use_subpath
85 self.package_name = package_name
86 self.docroot = docroot
87 self.norm_docroot = normcase(normpath(docroot))
88 self.index = index
90 def __call__(self, context, request):
91 if self.use_subpath:
92 path_tuple = request.subpath
93 else:
94 path_tuple = traversal_path_info(request.environ['PATH_INFO'])
95 path = _secure_path(path_tuple)
97 if path is None:
98 raise HTTPNotFound('Out of bounds: %s' % request.url)
100 if self.package_name: # package resource
101 resource_path = '%s/%s' % (self.docroot.rstrip('/'), path)
102 if resource_isdir(self.package_name, resource_path):
103 if not request.path_url.endswith('/'):
104 self.add_slash_redirect(request)
105 resource_path = '%s/%s' % (
106 resource_path.rstrip('/'),
107 self.index,
108 )
110 if not resource_exists(self.package_name, resource_path):
111 raise HTTPNotFound(request.url)
112 filepath = resource_filename(self.package_name, resource_path)
114 else: # filesystem file
116 # os.path.normpath converts / to \ on windows
117 filepath = normcase(normpath(join(self.norm_docroot, path)))
118 if isdir(filepath):
119 if not request.path_url.endswith('/'):
120 self.add_slash_redirect(request)
121 filepath = join(filepath, self.index)
122 if not exists(filepath):
123 raise HTTPNotFound(request.url)
125 content_type, content_encoding = _guess_type(filepath)
126 return FileResponse(
127 filepath,
128 request,
129 self.cache_max_age,
130 content_type,
131 content_encoding=None,
132 )
134 def add_slash_redirect(self, request):
135 url = request.path_url + '/'
136 qs = request.query_string
137 if qs:
138 url = url + '?' + qs
139 raise HTTPMovedPermanently(url)
142_seps = set(['/', os.sep])
145def _contains_slash(item):
146 for sep in _seps:
147 if sep in item:
148 return True
151_has_insecure_pathelement = set(['..', '.', '']).intersection
154@lru_cache(1000)
155def _secure_path(path_tuple):
156 if _has_insecure_pathelement(path_tuple):
157 # belt-and-suspenders security; this should never be true
158 # unless someone screws up the traversal_path code
159 # (request.subpath is computed via traversal_path too)
160 return None
161 if any([_contains_slash(item) for item in path_tuple]):
162 return None
163 encoded = slash.join(path_tuple) # will be unicode
164 return encoded
167class QueryStringCacheBuster(object):
168 """
169 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
170 a token for cache busting in the query string of an asset URL.
172 The optional ``param`` argument determines the name of the parameter added
173 to the query string and defaults to ``'x'``.
175 To use this class, subclass it and provide a ``tokenize`` method which
176 accepts ``request, pathspec, kw`` and returns a token.
178 .. versionadded:: 1.6
179 """
181 def __init__(self, param='x'):
182 self.param = param
184 def __call__(self, request, subpath, kw):
185 token = self.tokenize(request, subpath, kw)
186 query = kw.setdefault('_query', {})
187 if isinstance(query, dict):
188 query[self.param] = token
189 else:
190 kw['_query'] = tuple(query) + ((self.param, token),)
191 return subpath, kw
194class QueryStringConstantCacheBuster(QueryStringCacheBuster):
195 """
196 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
197 an arbitrary token for cache busting in the query string of an asset URL.
199 The ``token`` parameter is the token string to use for cache busting and
200 will be the same for every request.
202 The optional ``param`` argument determines the name of the parameter added
203 to the query string and defaults to ``'x'``.
205 .. versionadded:: 1.6
206 """
208 def __init__(self, token, param='x'):
209 super(QueryStringConstantCacheBuster, self).__init__(param=param)
210 self._token = token
212 def tokenize(self, request, subpath, kw):
213 return self._token
216class ManifestCacheBuster(object):
217 """
218 An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
219 uses a supplied manifest file to map an asset path to a cache-busted
220 version of the path.
222 The ``manifest_spec`` can be an absolute path or a :term:`asset
223 specification` pointing to a package-relative file.
225 The manifest file is expected to conform to the following simple JSON
226 format:
228 .. code-block:: json
230 {
231 "css/main.css": "css/main-678b7c80.css",
232 "images/background.png": "images/background-a8169106.png",
233 }
235 By default, it is a JSON-serialized dictionary where the keys are the
236 source asset paths used in calls to
237 :meth:`~pyramid.request.Request.static_url`. For example:
239 .. code-block:: pycon
241 >>> request.static_url('myapp:static/css/main.css')
242 "http://www.example.com/static/css/main-678b7c80.css"
244 The file format and location can be changed by subclassing and overriding
245 :meth:`.parse_manifest`.
247 If a path is not found in the manifest it will pass through unchanged.
249 If ``reload`` is ``True`` then the manifest file will be reloaded when
250 changed. It is not recommended to leave this enabled in production.
252 If the manifest file cannot be found on disk it will be treated as
253 an empty mapping unless ``reload`` is ``False``.
255 .. versionadded:: 1.6
256 """
258 exists = staticmethod(exists) # testing
259 getmtime = staticmethod(getmtime) # testing
261 def __init__(self, manifest_spec, reload=False):
262 package_name = caller_package().__name__
263 self.manifest_path = abspath_from_asset_spec(
264 manifest_spec, package_name
265 )
266 self.reload = reload
268 self._mtime = None
269 if not reload:
270 self._manifest = self.get_manifest()
272 def get_manifest(self):
273 with open(self.manifest_path, 'rb') as fp:
274 return self.parse_manifest(fp.read())
276 def parse_manifest(self, content):
277 """
278 Parse the ``content`` read from the ``manifest_path`` into a
279 dictionary mapping.
281 Subclasses may override this method to use something other than
282 ``json.loads`` to load any type of file format and return a conforming
283 dictionary.
285 """
286 return json.loads(content.decode('utf-8'))
288 @property
289 def manifest(self):
290 """ The current manifest dictionary."""
291 if self.reload:
292 if not self.exists(self.manifest_path):
293 return {}
294 mtime = self.getmtime(self.manifest_path)
295 if self._mtime is None or mtime > self._mtime:
296 self._manifest = self.get_manifest()
297 self._mtime = mtime
298 return self._manifest
300 def __call__(self, request, subpath, kw):
301 subpath = self.manifest.get(subpath, subpath)
302 return (subpath, kw)