summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Rossi <chris@archimedeanco.com>2014-07-14 15:59:05 -0400
committerChris Rossi <chris@archimedeanco.com>2014-07-14 15:59:05 -0400
commit0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d (patch)
treebe06e6fbafaa8687107d1f1c3f9ab9d4e89239d6
parentb648516a5dd61b3ce155586465f473338c230bf9 (diff)
downloadpyramid-0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d.tar.gz
pyramid-0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d.tar.bz2
pyramid-0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d.zip
Try this impl on and see how it feels.
-rw-r--r--pyramid/cachebust.py34
-rw-r--r--pyramid/config/views.py19
-rw-r--r--pyramid/interfaces.py57
-rw-r--r--pyramid/static.py7
4 files changed, 104 insertions, 13 deletions
diff --git a/pyramid/cachebust.py b/pyramid/cachebust.py
new file mode 100644
index 000000000..69c7eb1d2
--- /dev/null
+++ b/pyramid/cachebust.py
@@ -0,0 +1,34 @@
+import hashlib
+import pkg_resources
+
+from zope.interface import implementer
+
+from .interfaces import ICacheBuster
+
+from pyramid.asset import resolve_asset_spec
+
+
+def generate_md5(spec):
+ package, filename = resolve_asset_spec(spec)
+ md5 = hashlib.md5()
+ with pkg_resources.resource_stream(package, filename) as stream:
+ for block in iter(lambda: stream.read(4096), ''):
+ md5.update(block)
+ return md5.hexdigest()
+
+
+@implementer(ICacheBuster)
+class DefaultCacheBuster(object):
+
+ def generate_token(self, request, pathspec):
+ token_cache = request.registry.setdefault('md5-token-cache', {})
+ token = token_cache.get(pathspec)
+ if not token:
+ token_cache[pathspec] = token = generate_md5(pathspec)
+ return token
+
+ def pregenerate_url(self, request, token, subpath, kw):
+ return token + '/' + subpath, kw
+
+ def match_url(self, request, path_elements):
+ return path_elements[1:]
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index d938a7632..78c415b14 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -34,6 +34,7 @@ from pyramid.interfaces import (
)
from pyramid import renderers
+from pyramid.cachebust import DefaultCacheBuster
from pyramid.compat import (
string_types,
@@ -1905,11 +1906,16 @@ class StaticURLInfo(object):
registry = request.registry
except AttributeError: # bw compat (for tests)
registry = get_current_registry()
- for (url, spec, route_name) in self._get_registrations(registry):
+ registrations = self._get_registrations(registry)
+ for (url, spec, route_name, cachebust) in registrations:
if path.startswith(spec):
subpath = path[len(spec):]
if WIN: # pragma: no cover
subpath = subpath.replace('\\', '/') # windows
+ if cachebust:
+ token = cachebust.generate_token(request, spec + subpath)
+ subpath, kw = cachebust.pregenerate_url(
+ request, token, subpath, kw)
if url is None:
kw['subpath'] = subpath
return request.route_url(route_name, **kw)
@@ -1949,6 +1955,10 @@ class StaticURLInfo(object):
# make sure it ends with a slash
name = name + '/'
+ cachebust = extra.pop('cachebust', None)
+ if cachebust is True:
+ cachebust = DefaultCacheBuster()
+
if url_parse(name).netloc:
# it's a URL
# url, spec, route_name
@@ -1958,9 +1968,12 @@ class StaticURLInfo(object):
# it's a view name
url = None
cache_max_age = extra.pop('cache_max_age', None)
+ if cache_max_age is None and cachebust:
+ cache_max_age = 10 * 365 * 24 * 60 * 60 # Ten(ish) years
+
# create a view
view = static_view(spec, cache_max_age=cache_max_age,
- use_subpath=True)
+ use_subpath=True, cachebust=cachebust)
# Mutate extra to allow factory, etc to be passed through here.
# Treat permission specially because we'd like to default to
@@ -2001,7 +2014,7 @@ class StaticURLInfo(object):
registrations.pop(idx)
# url, spec, route_name
- registrations.append((url, spec, route_name))
+ registrations.append((url, spec, route_name, cachebust))
intr = config.introspectable('static views',
name,
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index aa2dbdafd..e60898dbc 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -708,7 +708,7 @@ class IRoute(Interface):
pregenerator = Attribute('This attribute should either be ``None`` or '
'a callable object implementing the '
'``IRoutePregenerator`` interface')
-
+
def match(path):
"""
If the ``path`` passed to this function can be matched by the
@@ -803,7 +803,7 @@ class IContextURL(IResourceURL):
# <__main__.Fudge object at 0x1cda890>
# <object object at 0x7fa678f3e2a0> <object object at 0x7fa678f3e2a0>
# <__main__.Another object at 0x1cda850>
-
+
def virtual_root():
""" Return the virtual root related to a request and the
current context"""
@@ -837,9 +837,9 @@ class IPEP302Loader(Interface):
def get_code(fullname):
""" Return the code object for the module identified by 'fullname'.
-
+
Return 'None' if it's a built-in or extension module.
-
+
If the loader doesn't have the code object but it does have the source
code, return the compiled source code.
@@ -848,16 +848,16 @@ class IPEP302Loader(Interface):
def get_source(fullname):
""" Return the source code for the module identified by 'fullname'.
-
+
Return a string, using newline characters for line endings, or None
if the source is not available.
-
+
Raise ImportError if the module can't be found by the importer at all.
"""
def get_filename(fullname):
""" Return the value of '__file__' if the named module was loaded.
-
+
If the module is not found, raise ImportError.
"""
@@ -1164,6 +1164,49 @@ class IJSONAdapter(Interface):
class IPredicateList(Interface):
""" Interface representing a predicate list """
+class ICacheBuster(Interface):
+ """
+ An instance of a class which implements this interface may be passed as the
+ ``cachebust`` argument to
+ :meth:`pyramid.config.Configurator.add_static_view` to add cache busting
+ capability to a static view.
+ """
+ def generate_token(request, pathspec):
+ """
+ Return a token string for a static asset to be used to rewrite a
+ static asset URL for cache busting.
+
+ The ``pathspec`` argument is the path specification for the asset we're
+ generating a token for.
+ """
+
+ def pregenerate_url(request, token, subpath, kw):
+ """
+ Modifies the elements and/or keywords used to generate the URL for a
+ given static asset.
+
+ The ``token`` argument is the result of calling
+ :meth:`~pyramid.interfaces.ICacheBuster.generate_token` for a static
+ asset.
+
+ The ``subpath`` argument is the subpath in the static asset URL that
+ would normally be generated without cache busting. The ``kw``
+ argument is the keywords dict that would be passed to
+ :meth:`~pyramid.request.Request.route_url`.
+ The return value should be a two-tuple of elements ``(subpath, kw)``
+ which are modified from the incoming arguments.
+ """
+
+ def match_url(request, path_elements):
+ """
+ Undo any modification to the subpath which may have been done by
+ :meth:`~pyramid.interfaces.ICacheBuster.pregenerate_url`. The
+ ``path_elements`` argument is a tuple of path elements that represent
+ the subpath of the asset request URL. The return value should be
+ a modified (or not) version of ``path_elements``, which will be used
+ ultimately to find the asset.
+ """
+
# configuration phases: a lower phase number means the actions associated
# with this phase will be executed earlier than those with later phase
# numbers. The default phase number is 0, FTR.
diff --git a/pyramid/static.py b/pyramid/static.py
index aa67568d3..be191971a 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -78,7 +78,7 @@ class static_view(object):
"""
def __init__(self, root_dir, cache_max_age=3600, package_name=None,
- use_subpath=False, index='index.html'):
+ use_subpath=False, index='index.html', cachebust=None):
# package_name is for bw compat; it is preferred to pass in a
# package-relative path as root_dir
# (e.g. ``anotherpackage:foo/static``).
@@ -91,13 +91,15 @@ class static_view(object):
self.docroot = docroot
self.norm_docroot = normcase(normpath(docroot))
self.index = index
+ self.cachebust = cachebust
def __call__(self, context, request):
if self.use_subpath:
path_tuple = request.subpath
else:
path_tuple = traversal_path_info(request.environ['PATH_INFO'])
-
+ if self.cachebust:
+ path_tuple = self.cachebust.match_url(request, path_tuple)
path = _secure_path(path_tuple)
if path is None:
@@ -153,4 +155,3 @@ def _secure_path(path_tuple):
return None
encoded = slash.join(path_tuple) # will be unicode
return encoded
-