Skip to content

Commit 5f75ac9

Browse files
committed
Fixed django#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.
1 parent 085c03e commit 5f75ac9

File tree

4 files changed

+71
-8
lines changed

4 files changed

+71
-8
lines changed

django/contrib/staticfiles/storage.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ def __init__(self, *args, **kwargs):
6464
compiled = re.compile(pattern)
6565
self._patterns.setdefault(extension, []).append(compiled)
6666

67+
def file_hash(self, name, content=None):
68+
"""
69+
Retuns a hash of the file with the given name and optional content.
70+
"""
71+
if content is None:
72+
return None
73+
md5 = hashlib.md5()
74+
for chunk in content.chunks():
75+
md5.update(chunk)
76+
return md5.hexdigest()[:12]
77+
6778
def hashed_name(self, name, content=None):
6879
parsed_name = urlsplit(unquote(name))
6980
clean_name = parsed_name.path.strip()
@@ -78,13 +89,11 @@ def hashed_name(self, name, content=None):
7889
return name
7990
path, filename = os.path.split(clean_name)
8091
root, ext = os.path.splitext(filename)
81-
# Get the MD5 hash of the file
82-
md5 = hashlib.md5()
83-
for chunk in content.chunks():
84-
md5.update(chunk)
85-
md5sum = md5.hexdigest()[:12]
86-
hashed_name = os.path.join(path, u"%s.%s%s" %
87-
(root, md5sum, ext))
92+
file_hash = self.file_hash(clean_name, content)
93+
if file_hash is not None:
94+
file_hash = u".%s" % file_hash
95+
hashed_name = os.path.join(path, u"%s%s%s" %
96+
(root, file_hash, ext))
8897
unparsed_name = list(parsed_name)
8998
unparsed_name[2] = hashed_name
9099
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")

docs/ref/contrib/staticfiles.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,15 @@ CachedStaticFilesStorage
348348
:setting:`CACHES` setting named ``'staticfiles'``. It falls back to using
349349
the ``'default'`` cache backend.
350350

351+
.. method:: file_hash(name, content=None)
352+
353+
.. versionadded:: 1.5
354+
355+
The method that is used when creating the hashed name of a file.
356+
Needs to return a hash for the given file name and content.
357+
By default it calculates a MD5 hash from the content's chunks as
358+
mentioned above.
359+
351360
.. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
352361
.. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
353362
.. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri

tests/regressiontests/staticfiles_tests/storage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime
22
from django.core.files import storage
3+
from django.contrib.staticfiles.storage import CachedStaticFilesStorage
34

45
class DummyStorage(storage.Storage):
56
"""
@@ -17,3 +18,9 @@ def exists(self, name):
1718

1819
def modified_time(self, name):
1920
return datetime.date(1970, 1, 1)
21+
22+
23+
class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage):
24+
25+
def file_hash(self, name, content=None):
26+
return 'deploy12345'

tests/regressiontests/staticfiles_tests/tests.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from django.template import loader, Context
1212
from django.conf import settings
13-
from django.core.cache.backends.base import BaseCache, CacheKeyWarning
13+
from django.core.cache.backends.base import BaseCache
1414
from django.core.exceptions import ImproperlyConfigured
1515
from django.core.files.storage import default_storage
1616
from django.core.management import call_command
@@ -515,6 +515,44 @@ def test_cache_key_memcache_validation(self):
515515
self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351')
516516

517517

518+
# we set DEBUG to False here since the template tag wouldn't work otherwise
519+
@override_settings(**dict(TEST_SETTINGS,
520+
STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.SimpleCachedStaticFilesStorage',
521+
DEBUG=False,
522+
))
523+
class TestCollectionSimpleCachedStorage(BaseCollectionTestCase,
524+
BaseStaticFilesTestCase, TestCase):
525+
"""
526+
Tests for the Cache busting storage
527+
"""
528+
def cached_file_path(self, path):
529+
fullpath = self.render_template(self.static_template_snippet(path))
530+
return fullpath.replace(settings.STATIC_URL, '')
531+
532+
def test_template_tag_return(self):
533+
"""
534+
Test the CachedStaticFilesStorage backend.
535+
"""
536+
self.assertStaticRaises(ValueError,
537+
"does/not/exist.png",
538+
"/static/does/not/exist.png")
539+
self.assertStaticRenders("test/file.txt",
540+
"/static/test/file.deploy12345.txt")
541+
self.assertStaticRenders("cached/styles.css",
542+
"/static/cached/styles.deploy12345.css")
543+
self.assertStaticRenders("path/",
544+
"/static/path/")
545+
self.assertStaticRenders("path/?query",
546+
"/static/path/?query")
547+
548+
def test_template_tag_simple_content(self):
549+
relpath = self.cached_file_path("cached/styles.css")
550+
self.assertEqual(relpath, "cached/styles.deploy12345.css")
551+
with storage.staticfiles_storage.open(relpath) as relfile:
552+
content = relfile.read()
553+
self.assertNotIn("cached/other.css", content)
554+
self.assertIn("other.deploy12345.css", content)
555+
518556
if sys.platform != 'win32':
519557

520558
class TestCollectionLinks(CollectionTestCase, TestDefaults):

0 commit comments

Comments
 (0)