Fixed #17896 -- Added file_hash method to CachedStaticFilesStorage to be able to customize the way the hashed name of a file is created. Thanks to mkai for the initial patch.

This commit is contained in:
Jannis Leidel 2012-05-16 13:21:46 +02:00
parent 085c03e08b
commit 5f75ac91df
4 changed files with 71 additions and 8 deletions

View File

@ -64,6 +64,17 @@ class CachedFilesMixin(object):
compiled = re.compile(pattern) compiled = re.compile(pattern)
self._patterns.setdefault(extension, []).append(compiled) self._patterns.setdefault(extension, []).append(compiled)
def file_hash(self, name, content=None):
"""
Retuns a hash of the file with the given name and optional content.
"""
if content is None:
return None
md5 = hashlib.md5()
for chunk in content.chunks():
md5.update(chunk)
return md5.hexdigest()[:12]
def hashed_name(self, name, content=None): def hashed_name(self, name, content=None):
parsed_name = urlsplit(unquote(name)) parsed_name = urlsplit(unquote(name))
clean_name = parsed_name.path.strip() clean_name = parsed_name.path.strip()
@ -78,13 +89,11 @@ class CachedFilesMixin(object):
return name return name
path, filename = os.path.split(clean_name) path, filename = os.path.split(clean_name)
root, ext = os.path.splitext(filename) root, ext = os.path.splitext(filename)
# Get the MD5 hash of the file file_hash = self.file_hash(clean_name, content)
md5 = hashlib.md5() if file_hash is not None:
for chunk in content.chunks(): file_hash = u".%s" % file_hash
md5.update(chunk) hashed_name = os.path.join(path, u"%s%s%s" %
md5sum = md5.hexdigest()[:12] (root, file_hash, ext))
hashed_name = os.path.join(path, u"%s.%s%s" %
(root, md5sum, ext))
unparsed_name = list(parsed_name) unparsed_name = list(parsed_name)
unparsed_name[2] = hashed_name unparsed_name[2] = hashed_name
# Special casing for a @font-face hack, like url(myfont.eot?#iefix") # Special casing for a @font-face hack, like url(myfont.eot?#iefix")

View File

@ -348,6 +348,15 @@ CachedStaticFilesStorage
:setting:`CACHES` setting named ``'staticfiles'``. It falls back to using :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using
the ``'default'`` cache backend. the ``'default'`` cache backend.
.. method:: file_hash(name, content=None)
.. versionadded:: 1.5
The method that is used when creating the hashed name of a file.
Needs to return a hash for the given file name and content.
By default it calculates a MD5 hash from the content's chunks as
mentioned above.
.. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires .. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
.. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import .. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
.. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri .. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri

View File

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from django.core.files import storage from django.core.files import storage
from django.contrib.staticfiles.storage import CachedStaticFilesStorage
class DummyStorage(storage.Storage): class DummyStorage(storage.Storage):
""" """
@ -17,3 +18,9 @@ class DummyStorage(storage.Storage):
def modified_time(self, name): def modified_time(self, name):
return datetime.date(1970, 1, 1) return datetime.date(1970, 1, 1)
class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage):
def file_hash(self, name, content=None):
return 'deploy12345'

View File

@ -10,7 +10,7 @@ from StringIO import StringIO
from django.template import loader, Context from django.template import loader, Context
from django.conf import settings from django.conf import settings
from django.core.cache.backends.base import BaseCache, CacheKeyWarning from django.core.cache.backends.base import BaseCache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.management import call_command from django.core.management import call_command
@ -515,6 +515,44 @@ class TestCollectionCachedStorage(BaseCollectionTestCase,
self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351') self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351')
# we set DEBUG to False here since the template tag wouldn't work otherwise
@override_settings(**dict(TEST_SETTINGS,
STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.SimpleCachedStaticFilesStorage',
DEBUG=False,
))
class TestCollectionSimpleCachedStorage(BaseCollectionTestCase,
BaseStaticFilesTestCase, TestCase):
"""
Tests for the Cache busting storage
"""
def cached_file_path(self, path):
fullpath = self.render_template(self.static_template_snippet(path))
return fullpath.replace(settings.STATIC_URL, '')
def test_template_tag_return(self):
"""
Test the CachedStaticFilesStorage backend.
"""
self.assertStaticRaises(ValueError,
"does/not/exist.png",
"/static/does/not/exist.png")
self.assertStaticRenders("test/file.txt",
"/static/test/file.deploy12345.txt")
self.assertStaticRenders("cached/styles.css",
"/static/cached/styles.deploy12345.css")
self.assertStaticRenders("path/",
"/static/path/")
self.assertStaticRenders("path/?query",
"/static/path/?query")
def test_template_tag_simple_content(self):
relpath = self.cached_file_path("cached/styles.css")
self.assertEqual(relpath, "cached/styles.deploy12345.css")
with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read()
self.assertNotIn("cached/other.css", content)
self.assertIn("other.deploy12345.css", content)
if sys.platform != 'win32': if sys.platform != 'win32':
class TestCollectionLinks(CollectionTestCase, TestDefaults): class TestCollectionLinks(CollectionTestCase, TestDefaults):