Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed #580 -- Added mega support for generating Vary headers, includi…
…ng some view decorators, and changed the CacheMiddleware to account for the Vary header. Also added GZipMiddleware and ConditionalGetMiddleware, which are no longer handled by CacheMiddleware itself. Also updated the cache.txt and middleware.txt docs. Thanks to Hugo and Sune for the excellent patches git-svn-id: http://code.djangoproject.com/svn/django/trunk@810 bcc190cf-cafb-0310-a4f2-bffc1f526a37
- Loading branch information
1 parent
a5a89b5
commit d65526d
Showing
9 changed files
with
297 additions
and
153 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
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -1,88 +1,70 @@ | |||
import copy | |||
from django.conf import settings | from django.conf import settings | ||
from django.core.cache import cache | from django.core.cache import cache | ||
from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers | |||
from django.utils.httpwrappers import HttpResponseNotModified | from django.utils.httpwrappers import HttpResponseNotModified | ||
from django.utils.text import compress_string | |||
import datetime, md5 | |||
|
|
||
class CacheMiddleware: | class CacheMiddleware: | ||
""" | """ | ||
Cache middleware. If this is enabled, each Django-powered page will be | Cache middleware. If this is enabled, each Django-powered page will be | ||
cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. Pages | cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. | ||
with GET or POST parameters are not cached. | |||
If the cache is shared across multiple sites using the same Django | Only parameter-less GET or HEAD-requests with status code 200 are cached. | ||
installation, set the CACHE_MIDDLEWARE_KEY_PREFIX to the name of the site, | |||
or some other string that is unique to this Django instance, to prevent key | |||
collisions. | |||
This middleware will also make the following optimizations: | This middleware expects that a HEAD request is answered with a response | ||
exactly like the corresponding GET request. | |||
* If the CACHE_MIDDLEWARE_GZIP setting is True, the content will be | When a hit occurs, a shallow copy of the original response object is | ||
gzipped. | returned from process_request. | ||
* ETags will be added, using a simple MD5 hash of the page's content. | Pages will be cached based on the contents of the request headers | ||
listed in the response's "Vary" header. This means that pages shouldn't | |||
change their "Vary" header. | |||
This middleware also sets ETag, Last-Modified, Expires and Cache-Control | |||
headers on the response object. | |||
""" | """ | ||
def __init__(self, cache_timeout=None, key_prefix=None): | |||
self.cache_timeout = cache_timeout | |||
if cache_timeout is None: | |||
self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS | |||
self.key_prefix = key_prefix | |||
if key_prefix is None: | |||
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX | |||
|
|||
def process_request(self, request): | def process_request(self, request): | ||
""" | "Checks whether the page is already cached and returns the cached version if available." | ||
Checks whether the page is already cached. If it is, returns the cached | if not request.META['REQUEST_METHOD'] in ('GET', 'HEAD') or request.GET: | ||
version. Also handles ETag stuff. | request._cache_update_cache = False | ||
""" | |||
if request.GET or request.POST: | |||
request._cache_middleware_set_cache = False | |||
return None # Don't bother checking the cache. | return None # Don't bother checking the cache. | ||
|
|
||
accept_encoding = '' | cache_key = get_cache_key(request, self.key_prefix) | ||
if settings.CACHE_MIDDLEWARE_GZIP: | if cache_key is None: | ||
try: | request._cache_update_cache = True | ||
accept_encoding = request.META['HTTP_ACCEPT_ENCODING'] | return None # No cache information available, need to rebuild. | ||
except KeyError: | |||
pass | |||
accepts_gzip = 'gzip' in accept_encoding | |||
request._cache_middleware_accepts_gzip = accepts_gzip | |||
|
|||
# This uses the same cache_key as views.decorators.cache.cache_page, | |||
# so the cache can be shared. | |||
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % \ | |||
(settings.CACHE_MIDDLEWARE_KEY_PREFIX, request.path, accepts_gzip) | |||
request._cache_middleware_key = cache_key | |||
|
|
||
response = cache.get(cache_key, None) | response = cache.get(cache_key, None) | ||
if response is None: | if response is None: | ||
request._cache_middleware_set_cache = True | request._cache_update_cache = True | ||
return None | return None # No cache information available, need to rebuild. | ||
else: |
|
||
request._cache_middleware_set_cache = False | request._cache_update_cache = False | ||
# Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet | return copy.copy(response) | ||
try: | |||
if_none_match = request.META['HTTP_IF_NONE_MATCH'] | |||
except KeyError: | |||
if_none_match = None | |||
try: | |||
if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] | |||
except KeyError: | |||
if_modified_since = None | |||
if if_none_match is None and if_modified_since is None: | |||
pass | |||
elif if_none_match is not None and response['ETag'] != if_none_match: | |||
pass | |||
elif if_modified_since is not None and response['Last-Modified'] != if_modified_since: | |||
pass | |||
else: | |||
return HttpResponseNotModified() | |||
return response | |||
|
|
||
def process_response(self, request, response): | def process_response(self, request, response): | ||
""" | "Sets the cache, if needed." | ||
Sets the cache, if needed. | if not request._cache_update_cache: | ||
""" | # We don't need to update the cache, just return. | ||
if request._cache_middleware_set_cache: | return response | ||
content = response.get_content_as_string(settings.DEFAULT_CHARSET) | if not request.META['REQUEST_METHOD'] == 'GET': | ||
if request._cache_middleware_accepts_gzip: | # This is a stronger requirement than above. It is needed | ||
content = compress_string(content) | # because of interactions between this middleware and the | ||
response.content = content | # HTTPMiddleware, which throws the body of a HEAD-request | ||
response['Content-Encoding'] = 'gzip' | # away before this middleware gets a chance to cache it. | ||
response['ETag'] = md5.new(content).hexdigest() | return response | ||
response['Content-Length'] = '%d' % len(content) | if not response.status_code == 200: | ||
response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') | return response | ||
cache.set(request._cache_middleware_key, response, settings.CACHE_MIDDLEWARE_SECONDS) | patch_response_headers(response, self.cache_timeout) | ||
cache_key = learn_cache_key(request, response, self.cache_timeout, self.key_prefix) | |||
cache.set(cache_key, response, self.cache_timeout) | |||
return response | return response |
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 | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,24 @@ | |||
import re | |||
from django.utils.text import compress_string | |||
from django.utils.cache import patch_vary_headers | |||
|
|||
re_accepts_gzip = re.compile(r'\bgzip\b') | |||
|
|||
class GZipMiddleware: | |||
""" | |||
This middleware compresses content if the browser allows gzip compression. | |||
It sets the Vary header accordingly, so that caches will base their storage | |||
on the Accept-Encoding header. | |||
""" | |||
def process_response(self, request, response): | |||
patch_vary_headers(response, ('Accept-Encoding',)) | |||
if response.has_header('Content-Encoding'): | |||
return response | |||
|
|||
ae = request.META.get('HTTP_ACCEPT_ENCODING', '') | |||
if not re_accepts_gzip.search(ae): | |||
return response | |||
|
|||
response.content = compress_string(response.content) | |||
response['Content-Encoding'] = 'gzip' | |||
return response |
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 | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,37 @@ | |||
import datetime | |||
|
|||
class ConditionalGetMiddleware: | |||
""" | |||
Handles conditional GET operations. If the response has a ETag or | |||
Last-Modified header, and the request has If-None-Match or | |||
If-Modified-Since, the response is replaced by an HttpNotModified. | |||
Removes the content from any response to a HEAD request. | |||
Also sets the Date and Content-Length response-headers. | |||
""" | |||
def process_response(self, request, response): | |||
now = datetime.datetime.utcnow() | |||
response['Date'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT') | |||
if not response.has_header('Content-Length'): | |||
response['Content-Length'] = str(len(response.content)) | |||
|
|||
if response.has_header('ETag'): | |||
if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) | |||
if if_none_match == response['ETag']: | |||
response.status_code = 304 | |||
response.content = '' | |||
response['Content-Length'] = '0' | |||
|
|||
if response.has_header('Last-Modified'): | |||
last_mod = response['Last-Modified'] | |||
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) | |||
if if_modified_since == response['Last-Modified']: | |||
response.status_code = 304 | |||
response.content = '' | |||
response['Content-Length'] = '0' | |||
|
|||
if request.META['REQUEST_METHOD'] == 'HEAD': | |||
response.content = '' | |||
|
|||
return response |
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 | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -1,57 +1,17 @@ | |||
from django.core.cache import cache | """ | ||
from django.utils.httpwrappers import HttpResponseNotModified | Decorator for views that tries getting the page from the cache and | ||
from django.utils.text import compress_string | populates the cache if the page isn't in the cache yet. | ||
from django.conf.settings import DEFAULT_CHARSET | |||
import datetime, md5 | |||
def cache_page(view_func, cache_timeout, key_prefix=''): | The cache is keyed by the URL and some data from the headers. Additionally | ||
""" | there is the key prefix that is used to distinguish different cache areas | ||
Decorator for views that tries getting the page from the cache and | in a multi-site setup. You could use the sites.get_current().domain, for | ||
populates the cache if the page isn't in the cache yet. Also takes care | example, as that is unique across a Django project. | ||
of ETags and gzips the page if the client supports it. | |||
The cache is keyed off of the page's URL plus the optional key_prefix | Additionally, all headers from the response's Vary header will be taken into | ||
variable. Use key_prefix if your Django setup has multiple sites that | account on caching -- just like the middleware does. | ||
use cache; otherwise the cache for one site would affect the other. A good | """ | ||
example of key_prefix is to use sites.get_current().domain, because that's |
|
||
unique across all Django instances on a particular server. | from django.utils.decorators import decorator_from_middleware | ||
""" | from django.middleware.cache import CacheMiddleware | ||
def _check_cache(request, *args, **kwargs): |
|
||
try: | cache_page = decorator_from_middleware(CacheMiddleware) | ||
accept_encoding = request.META['HTTP_ACCEPT_ENCODING'] | |||
except KeyError: | |||
accept_encoding = '' | |||
accepts_gzip = 'gzip' in accept_encoding | |||
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, accepts_gzip) | |||
response = cache.get(cache_key, None) | |||
if response is None: | |||
response = view_func(request, *args, **kwargs) | |||
content = response.get_content_as_string(DEFAULT_CHARSET) | |||
if accepts_gzip: | |||
content = compress_string(content) | |||
response.content = content | |||
response['Content-Encoding'] = 'gzip' | |||
response['ETag'] = md5.new(content).hexdigest() | |||
response['Content-Length'] = '%d' % len(content) | |||
response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') | |||
cache.set(cache_key, response, cache_timeout) | |||
else: | |||
# Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet | |||
try: | |||
if_none_match = request.META['HTTP_IF_NONE_MATCH'] | |||
except KeyError: | |||
if_none_match = None | |||
try: | |||
if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] | |||
except KeyError: | |||
if_modified_since = None | |||
if if_none_match is None and if_modified_since is None: | |||
pass | |||
elif if_none_match is not None and response['ETag'] != if_none_match: | |||
pass | |||
elif if_modified_since is not None and response['Last-Modified'] != if_modified_since: | |||
pass | |||
else: | |||
return HttpResponseNotModified() | |||
return response | |||
return _check_cache |
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 | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,6 @@ | |||
"Decorator for views that gzips pages if the client supports it." | |||
|
|||
from django.utils.decorators import decorator_from_middleware | |||
from django.middleware.gzip import GZipMiddleware | |||
|
|||
gzip_page = decorator_from_middleware(GZipMiddleware) |
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 | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,9 @@ | |||
""" | |||
Decorator for views that supports conditional get on ETag and Last-Modified | |||
headers. | |||
""" | |||
|
|||
from django.utils.decorators import decorator_from_middleware | |||
from django.middleware.http import ConditionalGetMiddleware | |||
|
|||
conditional_page = decorator_from_middleware(ConditionalGetMiddleware) |
Oops, something went wrong.