From 0445bf2ac9c4cb7862464f1ce8f42c640c11ea7d Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 14 Jul 2014 15:59:05 -0400 Subject: Try this impl on and see how it feels. --- pyramid/cachebust.py | 34 +++++++++++++++++++++++++++++ pyramid/config/views.py | 19 ++++++++++++++--- pyramid/interfaces.py | 57 +++++++++++++++++++++++++++++++++++++++++++------ pyramid/static.py | 7 +++--- 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 pyramid/cachebust.py 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> # # <__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 - -- cgit v1.2.3