From 5f75ac91df2ef1c19946f65e08aa50165f061cd4 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 16 May 2012 13:21:46 +0200 Subject: [PATCH] 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. --- django/contrib/staticfiles/storage.py | 23 +++++++---- docs/ref/contrib/staticfiles.txt | 9 +++++ .../staticfiles_tests/storage.py | 7 ++++ .../staticfiles_tests/tests.py | 40 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 7b5866a699..fd8f9efb02 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -64,6 +64,17 @@ class CachedFilesMixin(object): compiled = re.compile(pattern) 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): parsed_name = urlsplit(unquote(name)) clean_name = parsed_name.path.strip() @@ -78,13 +89,11 @@ class CachedFilesMixin(object): return name path, filename = os.path.split(clean_name) root, ext = os.path.splitext(filename) - # Get the MD5 hash of the file - md5 = hashlib.md5() - for chunk in content.chunks(): - md5.update(chunk) - md5sum = md5.hexdigest()[:12] - hashed_name = os.path.join(path, u"%s.%s%s" % - (root, md5sum, ext)) + file_hash = self.file_hash(clean_name, content) + if file_hash is not None: + file_hash = u".%s" % file_hash + hashed_name = os.path.join(path, u"%s%s%s" % + (root, file_hash, ext)) unparsed_name = list(parsed_name) unparsed_name[2] = hashed_name # Special casing for a @font-face hack, like url(myfont.eot?#iefix") diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 1dbd00b299..2bdf825316 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -348,6 +348,15 @@ CachedStaticFilesStorage :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using 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 .. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import .. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri diff --git a/tests/regressiontests/staticfiles_tests/storage.py b/tests/regressiontests/staticfiles_tests/storage.py index 8358f833c8..4d49e6fbb2 100644 --- a/tests/regressiontests/staticfiles_tests/storage.py +++ b/tests/regressiontests/staticfiles_tests/storage.py @@ -1,5 +1,6 @@ from datetime import datetime from django.core.files import storage +from django.contrib.staticfiles.storage import CachedStaticFilesStorage class DummyStorage(storage.Storage): """ @@ -17,3 +18,9 @@ class DummyStorage(storage.Storage): def modified_time(self, name): return datetime.date(1970, 1, 1) + + +class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage): + + def file_hash(self, name, content=None): + return 'deploy12345' diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index 7f30cb987a..0c247e4a17 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -10,7 +10,7 @@ from StringIO import StringIO from django.template import loader, Context 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.files.storage import default_storage from django.core.management import call_command @@ -515,6 +515,44 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, 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': class TestCollectionLinks(CollectionTestCase, TestDefaults):