This repository has been archived by the owner on Feb 8, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 309
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
use etags instead of version and datetimes
- Loading branch information
1 parent
ae71f4e
commit a91435c
Showing
2 changed files
with
41 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |