Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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.
  • Loading branch information...
commit 5f75ac91df2ef1c19946f65e08aa50165f061cd4 1 parent 085c03e
Jannis Leidel authored May 16, 2012
23  django/contrib/staticfiles/storage.py
@@ -64,6 +64,17 @@ def __init__(self, *args, **kwargs):
64 64
                 compiled = re.compile(pattern)
65 65
                 self._patterns.setdefault(extension, []).append(compiled)
66 66
 
  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
+
67 78
     def hashed_name(self, name, content=None):
68 79
         parsed_name = urlsplit(unquote(name))
69 80
         clean_name = parsed_name.path.strip()
@@ -78,13 +89,11 @@ def hashed_name(self, name, content=None):
78 89
                 return name
79 90
         path, filename = os.path.split(clean_name)
80 91
         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))
88 97
         unparsed_name = list(parsed_name)
89 98
         unparsed_name[2] = hashed_name
90 99
         # Special casing for a @font-face hack, like url(myfont.eot?#iefix")
9  docs/ref/contrib/staticfiles.txt
@@ -348,6 +348,15 @@ CachedStaticFilesStorage
348 348
     :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using
349 349
     the ``'default'`` cache backend.
350 350
 
  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
+
351 360
 .. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
352 361
 .. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
353 362
 .. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri
7  tests/regressiontests/staticfiles_tests/storage.py
... ...
@@ -1,5 +1,6 @@
1 1
 from datetime import datetime
2 2
 from django.core.files import storage
  3
+from django.contrib.staticfiles.storage import CachedStaticFilesStorage
3 4
 
4 5
 class DummyStorage(storage.Storage):
5 6
     """
@@ -17,3 +18,9 @@ def exists(self, name):
17 18
 
18 19
     def modified_time(self, name):
19 20
         return datetime.date(1970, 1, 1)
  21
+
  22
+
  23
+class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage):
  24
+
  25
+    def file_hash(self, name, content=None):
  26
+        return 'deploy12345'
40  tests/regressiontests/staticfiles_tests/tests.py
@@ -10,7 +10,7 @@
10 10
 
11 11
 from django.template import loader, Context
12 12
 from django.conf import settings
13  
-from django.core.cache.backends.base import BaseCache, CacheKeyWarning
  13
+from django.core.cache.backends.base import BaseCache
14 14
 from django.core.exceptions import ImproperlyConfigured
15 15
 from django.core.files.storage import default_storage
16 16
 from django.core.management import call_command
@@ -515,6 +515,44 @@ def test_cache_key_memcache_validation(self):
515 515
         self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351')
516 516
 
517 517
 
  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
+
518 556
 if sys.platform != 'win32':
519 557
 
520 558
     class TestCollectionLinks(CollectionTestCase, TestDefaults):

1 note on commit 5f75ac9

Matthew Dapena-Tretter

@jezdez What do you think about removing the content arg from file_hash() and moving the file reading into it? That way, you would be able to get rid of the potential network operations of exists() and open() just by overriding file_hash(). (I believe the original reason behind this patch was to avoid network operations, but it seems that—with those exists() and open() calls still present in hashed_name()—it hasn't quite gone far enough.)

As an example of why this would be rad, I'm thinking of a CachedStaticFilesStorage that overrides file_hash() and uses the local static files to compute the hash, instead of reading over the network, a la CachedFilesMixin.post_process().

Please sign in to comment.
Something went wrong with that request. Please try again.