commit | author | age
|
c55903
|
1 |
# -*- coding: utf-8 -*- |
3a4119
|
2 |
import json |
ac15b5
|
3 |
import os |
0c1c39
|
4 |
|
CM |
5 |
from os.path import ( |
3a4119
|
6 |
getmtime, |
0c1c39
|
7 |
normcase, |
CM |
8 |
normpath, |
|
9 |
join, |
|
10 |
isdir, |
|
11 |
exists, |
|
12 |
) |
|
13 |
|
|
14 |
from pkg_resources import ( |
|
15 |
resource_exists, |
934d7f
|
16 |
resource_filename, |
0c1c39
|
17 |
resource_isdir, |
CM |
18 |
) |
d809ac
|
19 |
|
f2ef79
|
20 |
from repoze.lru import lru_cache |
b29429
|
21 |
|
1e1111
|
22 |
from pyramid.asset import ( |
MM |
23 |
abspath_from_asset_spec, |
|
24 |
resolve_asset_spec, |
|
25 |
) |
0c1c39
|
26 |
|
e6c2d2
|
27 |
from pyramid.compat import text_ |
0c1c39
|
28 |
|
CM |
29 |
from pyramid.httpexceptions import ( |
|
30 |
HTTPNotFound, |
|
31 |
HTTPMovedPermanently, |
|
32 |
) |
|
33 |
|
3a4119
|
34 |
from pyramid.path import caller_package |
ed06c6
|
35 |
|
DG |
36 |
from pyramid.response import ( |
|
37 |
_guess_type, |
|
38 |
FileResponse, |
|
39 |
) |
|
40 |
|
f84147
|
41 |
from pyramid.traversal import traversal_path_info |
e6c2d2
|
42 |
|
CM |
43 |
slash = text_('/') |
b29429
|
44 |
|
CM |
45 |
class static_view(object): |
|
46 |
""" An instance of this class is a callable which can act as a |
fd5ae9
|
47 |
:app:`Pyramid` :term:`view callable`; this view will serve |
b29429
|
48 |
static files from a directory on disk based on the ``root_dir`` |
CM |
49 |
you provide to its constructor. |
|
50 |
|
|
51 |
The directory may contain subdirectories (recursively); the static |
|
52 |
view implementation will descend into these directories as |
|
53 |
necessary based on the components of the URL in order to resolve a |
|
54 |
path into a response. |
|
55 |
|
|
56 |
You may pass an absolute or relative filesystem path or a |
3e2f12
|
57 |
:term:`asset specification` representing the directory |
b29429
|
58 |
containing static files as the ``root_dir`` argument to this |
CM |
59 |
class' constructor. |
|
60 |
|
|
61 |
If the ``root_dir`` path is relative, and the ``package_name`` |
|
62 |
argument is ``None``, ``root_dir`` will be considered relative to |
|
63 |
the directory in which the Python file which *calls* ``static`` |
|
64 |
resides. If the ``package_name`` name argument is provided, and a |
|
65 |
relative ``root_dir`` is provided, the ``root_dir`` will be |
|
66 |
considered relative to the Python :term:`package` specified by |
|
67 |
``package_name`` (a dotted path to a Python package). |
|
68 |
|
|
69 |
``cache_max_age`` influences the ``Expires`` and ``Max-Age`` |
|
70 |
response headers returned by the view (default is 3600 seconds or |
dab748
|
71 |
one hour). |
b29429
|
72 |
|
56d0fe
|
73 |
``use_subpath`` influences whether ``request.subpath`` will be used as |
CM |
74 |
``PATH_INFO`` when calling the underlying WSGI application which actually |
|
75 |
serves the static files. If it is ``True``, the static application will |
|
76 |
consider ``request.subpath`` as ``PATH_INFO`` input. If it is ``False``, |
f84147
|
77 |
the static application will consider request.environ[``PATH_INFO``] as |
CM |
78 |
``PATH_INFO`` input. By default, this is ``False``. |
56d0fe
|
79 |
|
012b97
|
80 |
.. note:: |
M |
81 |
|
|
82 |
If the ``root_dir`` is relative to a :term:`package`, or is a |
|
83 |
:term:`asset specification` the :app:`Pyramid` |
|
84 |
:class:`pyramid.config.Configurator` method can be used to override |
|
85 |
assets within the named ``root_dir`` package-relative directory. |
|
86 |
However, if the ``root_dir`` is absolute, configuration will not be able |
|
87 |
to override the assets it contains. |
|
88 |
""" |
f2ef79
|
89 |
|
56d0fe
|
90 |
def __init__(self, root_dir, cache_max_age=3600, package_name=None, |
3e2a69
|
91 |
use_subpath=False, index='index.html'): |
b29429
|
92 |
# package_name is for bw compat; it is preferred to pass in a |
CM |
93 |
# package-relative path as root_dir |
|
94 |
# (e.g. ``anotherpackage:foo/static``). |
fe1548
|
95 |
self.cache_max_age = cache_max_age |
379b47
|
96 |
if package_name is None: |
CM |
97 |
package_name = caller_package().__name__ |
f2ef79
|
98 |
package_name, docroot = resolve_asset_spec(root_dir, package_name) |
56d0fe
|
99 |
self.use_subpath = use_subpath |
f2ef79
|
100 |
self.package_name = package_name |
CM |
101 |
self.docroot = docroot |
|
102 |
self.norm_docroot = normcase(normpath(docroot)) |
|
103 |
self.index = index |
b29429
|
104 |
|
CM |
105 |
def __call__(self, context, request): |
56d0fe
|
106 |
if self.use_subpath: |
f2ef79
|
107 |
path_tuple = request.subpath |
CM |
108 |
else: |
f84147
|
109 |
path_tuple = traversal_path_info(request.environ['PATH_INFO']) |
1455ba
|
110 |
path = _secure_path(path_tuple) |
f2ef79
|
111 |
|
CM |
112 |
if path is None: |
99e617
|
113 |
raise HTTPNotFound('Out of bounds: %s' % request.url) |
f2ef79
|
114 |
|
CM |
115 |
if self.package_name: # package resource |
25c64c
|
116 |
resource_path = '%s/%s' % (self.docroot.rstrip('/'), path) |
f2ef79
|
117 |
if resource_isdir(self.package_name, resource_path): |
CM |
118 |
if not request.path_url.endswith('/'): |
3d65af
|
119 |
self.add_slash_redirect(request) |
25c64c
|
120 |
resource_path = '%s/%s' % ( |
JA |
121 |
resource_path.rstrip('/'), self.index |
|
122 |
) |
|
123 |
|
f2ef79
|
124 |
if not resource_exists(self.package_name, resource_path): |
99e617
|
125 |
raise HTTPNotFound(request.url) |
f2ef79
|
126 |
filepath = resource_filename(self.package_name, resource_path) |
CM |
127 |
|
|
128 |
else: # filesystem file |
|
129 |
|
|
130 |
# os.path.normpath converts / to \ on windows |
|
131 |
filepath = normcase(normpath(join(self.norm_docroot, path))) |
|
132 |
if isdir(filepath): |
|
133 |
if not request.path_url.endswith('/'): |
3d65af
|
134 |
self.add_slash_redirect(request) |
f2ef79
|
135 |
filepath = join(filepath, self.index) |
CM |
136 |
if not exists(filepath): |
99e617
|
137 |
raise HTTPNotFound(request.url) |
f2ef79
|
138 |
|
ed06c6
|
139 |
content_type, content_encoding = _guess_type(filepath) |
DG |
140 |
return FileResponse( |
|
141 |
filepath, request, self.cache_max_age, |
|
142 |
content_type, content_encoding=None) |
f2ef79
|
143 |
|
CM |
144 |
def add_slash_redirect(self, request): |
|
145 |
url = request.path_url + '/' |
|
146 |
qs = request.query_string |
|
147 |
if qs: |
|
148 |
url = url + '?' + qs |
3d65af
|
149 |
raise HTTPMovedPermanently(url) |
f2ef79
|
150 |
|
3bbe82
|
151 |
_seps = set(['/', os.sep]) |
CM |
152 |
def _contains_slash(item): |
|
153 |
for sep in _seps: |
0ab0b9
|
154 |
if sep in item: |
CM |
155 |
return True |
b1ba8c
|
156 |
|
ac15b5
|
157 |
_has_insecure_pathelement = set(['..', '.', '']).intersection |
CM |
158 |
|
f2ef79
|
159 |
@lru_cache(1000) |
1455ba
|
160 |
def _secure_path(path_tuple): |
3bbe82
|
161 |
if _has_insecure_pathelement(path_tuple): |
05b8a6
|
162 |
# belt-and-suspenders security; this should never be true |
CM |
163 |
# unless someone screws up the traversal_path code |
|
164 |
# (request.subpath is computed via traversal_path too) |
f2ef79
|
165 |
return None |
3bbe82
|
166 |
if any([_contains_slash(item) for item in path_tuple]): |
05b8a6
|
167 |
return None |
e6c2d2
|
168 |
encoded = slash.join(path_tuple) # will be unicode |
c55903
|
169 |
return encoded |
2a1ca8
|
170 |
|
780889
|
171 |
class QueryStringCacheBuster(object): |
MM |
172 |
""" |
|
173 |
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds |
|
174 |
a token for cache busting in the query string of an asset URL. |
2a1ca8
|
175 |
|
780889
|
176 |
The optional ``param`` argument determines the name of the parameter added |
MM |
177 |
to the query string and defaults to ``'x'``. |
|
178 |
|
|
179 |
To use this class, subclass it and provide a ``tokenize`` method which |
4d19b8
|
180 |
accepts ``request, pathspec, kw`` and returns a token. |
780889
|
181 |
|
MM |
182 |
.. versionadded:: 1.6 |
|
183 |
""" |
|
184 |
def __init__(self, param='x'): |
|
185 |
self.param = param |
|
186 |
|
4d19b8
|
187 |
def __call__(self, request, subpath, kw): |
MM |
188 |
token = self.tokenize(request, subpath, kw) |
780889
|
189 |
query = kw.setdefault('_query', {}) |
MM |
190 |
if isinstance(query, dict): |
|
191 |
query[self.param] = token |
|
192 |
else: |
|
193 |
kw['_query'] = tuple(query) + ((self.param, token),) |
|
194 |
return subpath, kw |
2a1ca8
|
195 |
|
780889
|
196 |
class QueryStringConstantCacheBuster(QueryStringCacheBuster): |
f674a8
|
197 |
""" |
CR |
198 |
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds |
|
199 |
an arbitrary token for cache busting in the query string of an asset URL. |
|
200 |
|
|
201 |
The ``token`` parameter is the token string to use for cache busting and |
|
202 |
will be the same for every request. |
|
203 |
|
|
204 |
The optional ``param`` argument determines the name of the parameter added |
|
205 |
to the query string and defaults to ``'x'``. |
6b88bd
|
206 |
|
CM |
207 |
.. versionadded:: 1.6 |
f674a8
|
208 |
""" |
CR |
209 |
def __init__(self, token, param='x'): |
4a9c13
|
210 |
super(QueryStringConstantCacheBuster, self).__init__(param=param) |
f674a8
|
211 |
self._token = token |
CR |
212 |
|
4d19b8
|
213 |
def tokenize(self, request, subpath, kw): |
f674a8
|
214 |
return self._token |
3a4119
|
215 |
|
MM |
216 |
class 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. |
|
221 |
|
3fe24b
|
222 |
The ``manifest_spec`` can be an absolute path or a :term:`asset |
SP |
223 |
specification` pointing to a package-relative file. |
1e1111
|
224 |
|
MM |
225 |
The manifest file is expected to conform to the following simple JSON |
|
226 |
format: |
3a4119
|
227 |
|
MM |
228 |
.. code-block:: json |
|
229 |
|
|
230 |
{ |
|
231 |
"css/main.css": "css/main-678b7c80.css", |
|
232 |
"images/background.png": "images/background-a8169106.png", |
|
233 |
} |
|
234 |
|
edad4c
|
235 |
By default, it is a JSON-serialized dictionary where the keys are the |
3a4119
|
236 |
source asset paths used in calls to |
3e0b1d
|
237 |
:meth:`~pyramid.request.Request.static_url`. For example: |
3a4119
|
238 |
|
a996ac
|
239 |
.. code-block:: pycon |
3a4119
|
240 |
|
MM |
241 |
>>> request.static_url('myapp:static/css/main.css') |
|
242 |
"http://www.example.com/static/css/main-678b7c80.css" |
|
243 |
|
edad4c
|
244 |
The file format and location can be changed by subclassing and overriding |
MM |
245 |
:meth:`.parse_manifest`. |
|
246 |
|
3a4119
|
247 |
If a path is not found in the manifest it will pass through unchanged. |
MM |
248 |
|
|
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. |
|
251 |
|
|
252 |
If the manifest file cannot be found on disk it will be treated as |
|
253 |
an empty mapping unless ``reload`` is ``False``. |
|
254 |
|
|
255 |
.. versionadded:: 1.6 |
|
256 |
""" |
|
257 |
exists = staticmethod(exists) # testing |
|
258 |
getmtime = staticmethod(getmtime) # testing |
|
259 |
|
1e1111
|
260 |
def __init__(self, manifest_spec, reload=False): |
MM |
261 |
package_name = caller_package().__name__ |
|
262 |
self.manifest_path = abspath_from_asset_spec( |
|
263 |
manifest_spec, package_name) |
3a4119
|
264 |
self.reload = reload |
MM |
265 |
|
|
266 |
self._mtime = None |
|
267 |
if not reload: |
edad4c
|
268 |
self._manifest = self.get_manifest() |
3a4119
|
269 |
|
edad4c
|
270 |
def get_manifest(self): |
MM |
271 |
with open(self.manifest_path, 'rb') as fp: |
|
272 |
return self.parse_manifest(fp.read()) |
|
273 |
|
|
274 |
def parse_manifest(self, content): |
3a4119
|
275 |
""" |
edad4c
|
276 |
Parse the ``content`` read from the ``manifest_path`` into a |
MM |
277 |
dictionary mapping. |
3a4119
|
278 |
|
MM |
279 |
Subclasses may override this method to use something other than |
1e1111
|
280 |
``json.loads`` to load any type of file format and return a conforming |
MM |
281 |
dictionary. |
3a4119
|
282 |
|
MM |
283 |
""" |
edad4c
|
284 |
return json.loads(content.decode('utf-8')) |
3a4119
|
285 |
|
MM |
286 |
@property |
|
287 |
def manifest(self): |
|
288 |
""" The current manifest dictionary.""" |
|
289 |
if self.reload: |
|
290 |
if not self.exists(self.manifest_path): |
|
291 |
return {} |
|
292 |
mtime = self.getmtime(self.manifest_path) |
|
293 |
if self._mtime is None or mtime > self._mtime: |
edad4c
|
294 |
self._manifest = self.get_manifest() |
3a4119
|
295 |
self._mtime = mtime |
MM |
296 |
return self._manifest |
|
297 |
|
4d19b8
|
298 |
def __call__(self, request, subpath, kw): |
6e29b4
|
299 |
subpath = self.manifest.get(subpath, subpath) |
MM |
300 |
return (subpath, kw) |