Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
rewrite the static cache hooks
Browse files Browse the repository at this point in the history
use etags instead of version and datetimes
  • Loading branch information
Changaco authored and chadwhitacre committed Nov 3, 2014
1 parent ae71f4e commit a91435c
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 87 deletions.
4 changes: 2 additions & 2 deletions configure-aspen.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def add_stuff_to_context(request):
, algorithm['dispatch_request_to_filesystem']
, algorithm['apply_typecasters_to_path']

, cache_static.try_to_serve_304
, cache_static.try_to_serve_304 if website.cache_static else lambda: None

, algorithm['get_resource_for_request']
, algorithm['get_response_for_resource']
Expand All @@ -172,7 +172,7 @@ def add_stuff_to_context(request):
, gratipay.set_misc_headers
, authentication.add_auth_to_response
, csrf.add_csrf_token_to_response
, cache_static.add_caching_to_response
, cache_static.add_caching_to_response if website.cache_static else lambda: None
, x_frame_options

, algorithm['log_traceback_for_5xx']
Expand Down
124 changes: 39 additions & 85 deletions gratipay/utils/cache_static.py
Original file line number Diff line number Diff line change
@@ -1,125 +1,79 @@
"""
Handles caching of static resources.
"""
import os
from calendar import timegm
from email.utils import parsedate
from wsgiref.handlers import format_date_time
from base64 import b64encode
from hashlib import md5

from aspen import Response


def version_is_available(version, path):
"""Return a boolean, whether we have the version they asked for.
"""
return path['version'] == version if 'version' in path else True

ETAGS = {}

def version_is_dash(request):
"""Return a boolean, whether the version they asked for is -.
"""
return request.line.uri.path.get('version') == '-'


def get_last_modified(fs_path):
"""Get the last modified time, as int, of the file pointed to by fs_path.
"""
return int(os.path.getmtime(fs_path))
def asset_etag(website, path):
if path.endswith('.spt'):
return ''
if website.cache_static and path in ETAGS:
h = ETAGS[path]
else:
with open(path) as f:
h = ETAGS[path] = b64encode(md5(f.read()).digest(), '-_').replace('=', '~')
return h


# algorithm functions

def try_to_serve_304(website, request, dispatch_result):
"""Try to serve a 304 for resources under assets/.
def try_to_serve_304(dispatch_result, request, website):
"""Try to serve a 304 for static resources.
"""
uri = request.line.uri

if not uri.startswith('/assets/'):

# Only apply to the assets/ directory.

return request

if version_is_dash(request):

# Special-case a version of '-' to never 304/404 here.

return request

if not version_is_available(website.version, request.line.uri.path):

# Don't serve one version of a file as if it were another.

raise Response(404)

ims = request.headers.get('If-Modified-Since')
if not ims:

# This client doesn't care about when the file was modified.

return request

if dispatch_result.match.endswith('.spt'):

# This is a requests for a dynamic resource. Perhaps in the future
# we'll delegate to such resources to compute a sensible Last-Modified
# or E-Tag, but for now we punt. This is okay, because we expect to
# put our dynamic assets behind a CDN in production.

# This is a request for a dynamic resource.
return request

etag = request.etag = asset_etag(website, dispatch_result.match)

try:
ims = timegm(parsedate(ims))
except:

# Malformed If-Modified-Since header. Proceed with the request.
qs_etag = request.line.uri.querystring.get('etag')
if qs_etag and qs_etag != etag:
# Don't serve one version of a file as if it were another.
raise Response(410)

headers_etag = request.headers.get('If-None-Match')
if not headers_etag:
# This client doesn't want a 304.
return request

last_modified = get_last_modified(dispatch_result.match)
if ims < last_modified:

# The file has been modified since. Serve the whole thing.

if headers_etag != etag:
# Cache miss, the client sent an old or invalid etag.
return request


# Huzzah!
# =======
# We can serve a 304! :D

response = Response(304)
response.headers['Last-Modified'] = format_date_time(last_modified)
response.headers['Cache-Control'] = 'no-cache'
raise response
raise Response(304)


def add_caching_to_response(response, website, request=None, dispatch_result=None):
"""Set caching headers for resources under assets/.
"""Set caching headers for static resources.
"""
if dispatch_result is None:
return # early parsing must've failed
assert request is not None # we can't have a dispatch_result without a request

uri = request.line.uri

if not uri.startswith('/assets/'):
if dispatch_result.match.endswith('.spt'):
return response

if response.code != 200:
return response

if website.cache_static:

# https://developers.google.com/speed/docs/best-practices/caching
response.headers['Cache-Control'] = 'public'
response.headers['Vary'] = 'accept-encoding'

response.headers['Access-Control-Allow-Origin'] = 'https://gratipay.com'

# all assets are versioned, so it's fine to cache them

response.headers['Expires'] = 'Sun, 17 Jan 2038 19:14:07 GMT'
last_modified = get_last_modified(dispatch_result.match)
response.headers['Last-Modified'] = format_date_time(last_modified)
# https://developers.google.com/speed/docs/best-practices/caching
response.headers['Access-Control-Allow-Origin'] = 'https://gratipay.com'
response.headers['Vary'] = 'accept-encoding'
response.headers['Etag'] = request.etag

if request.line.uri.querystring.get('etag'):
# We can cache "indefinitely" when the querystring contains the etag.
response.headers['Cache-Control'] = 'public, max-age=31536000'
else:
# Otherwise we cache for 5 seconds
response.headers['Cache-Control'] = 'public, max-age=5'

0 comments on commit a91435c

Please sign in to comment.