"""
Static file serving with cache-busting support.
This module provides utilities for serving static files with automatic
cache-busting via versioned URLs. When the application starts, a unique
cache-breaker token is generated based on the current timestamp.
Features
--------
- Automatic cache-busting URLs for static assets
- Graceful handling of old cache-breaker values (301 redirect)
- Graceful handling of future cache-breaker values (503 retry)
Example
-------
Setting up static files with cache-busting::
from tet.config import application_factory
@application_factory()
def main(config):
config.include("tet.static")
config.add_static_view_with_breaker(
name="static/{breaker}",
path="myapp:static",
)
config.scan()
In templates, use the versioned URL::
<link href="${request.static_url('myapp:static/style.css')}" rel="stylesheet">
"""
import os
import time
from pipes import quote
from pyramid.httpexceptions import (
HTTPMovedPermanently,
HTTPNotFound,
HTTPServiceUnavailable,
)
# todo: use other versioning where possible
cachebreaker = None
[docs]
def set_cachebreaker(config, cachebreaker):
"""Set a custom cache-breaker value."""
config.registry.cachebreaker = cachebreaker
[docs]
def make_redirector(redirected_route):
"""Create a view that redirects to the correct cache-breaker URL."""
def redirect_breaker(request):
current_breaker = request.registry.cachebreaker
breaker = request.matchdict["breaker"]
path = request.matchdict["path"]
if breaker < current_breaker:
return HTTPMovedPermanently(
request.route_url(redirected_route, breaker=current_breaker, path=path)
)
# too recent breaker
if breaker > current_breaker:
# return 503 Service Unavailable - retry after 3 seconds
rv = HTTPServiceUnavailable()
rv.retry_after = 3
return rv
# finally come here, if no match found in static assets
return HTTPNotFound("No such asset")
return redirect_breaker
[docs]
def add_static_view_with_breaker(config, name, path, **kw):
"""
Add a static view with cache-busting support.
:param config: Pyramid Configurator
:param name: URL pattern with ``{breaker}`` placeholder
:param path: Asset specification for static files
:param kw: Additional arguments passed to add_static_view
"""
if "{breaker}" not in name:
raise ValueError(
"Invalid path to add_static_view_with_breaker: missing {breaker}"
)
url = name.replace("{breaker}", config.registry.cachebreaker)
config.add_static_view(name=url, path=path, **kw)
redirected_route = name + "-redirect"
redirected_url = name.rstrip("/") + "/*path"
config.add_route(name=name + "-breaker", pattern=redirected_url)
config.add_view(route_name=name + "-breaker", view=make_redirector(redirected_route))
config.add_route(name=redirected_route, pattern=redirected_url, static=True)
[docs]
def includeme(config):
"""
Pyramid includeme for static file cache-busting.
Adds ``config.set_cachebreaker()`` and ``config.add_static_view_with_breaker()``
directives.
"""
config.registry.cachebreaker = "%012d" % int(time.time() * 1000)
config.add_directive("set_cachebreaker", set_cachebreaker)
config.add_directive("add_static_view_with_breaker", add_static_view_with_breaker)