Fixed #16966 -- Stopped CachedStaticFilesStorage from choking on querystrings and path fragments.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17068 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-11-03 10:51:02 +00:00
parent 0e2c543979
commit a82488bf4b
3 changed files with 77 additions and 52 deletions

View File

@ -3,6 +3,8 @@ import hashlib
import os import os
import posixpath import posixpath
import re import re
from urllib import unquote
from urlparse import urlsplit, urlunsplit
from django.conf import settings from django.conf import settings
from django.core.cache import (get_cache, InvalidCacheBackendError, from django.core.cache import (get_cache, InvalidCacheBackendError,
@ -64,23 +66,28 @@ class CachedFilesMixin(object):
self._patterns.setdefault(extension, []).append(compiled) self._patterns.setdefault(extension, []).append(compiled)
def hashed_name(self, name, content=None): def hashed_name(self, name, content=None):
parsed_name = urlsplit(unquote(name))
clean_name = parsed_name.path
if content is None: if content is None:
if not self.exists(name): if not self.exists(clean_name):
raise ValueError("The file '%s' could not be found with %r." % raise ValueError("The file '%s' could not be found with %r." %
(name, self)) (clean_name, self))
try: try:
content = self.open(name) content = self.open(clean_name)
except IOError: except IOError:
# Handle directory paths # Handle directory paths
return name return name
path, filename = os.path.split(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 # Get the MD5 hash of the file
md5 = hashlib.md5() md5 = hashlib.md5()
for chunk in content.chunks(): for chunk in content.chunks():
md5.update(chunk) md5.update(chunk)
md5sum = md5.hexdigest()[:12] md5sum = md5.hexdigest()[:12]
return os.path.join(path, u"%s.%s%s" % (root, md5sum, ext)) hashed_name = os.path.join(path, u"%s.%s%s" % (root, md5sum, ext))
unparsed_name = list(parsed_name)
unparsed_name[2] = hashed_name
return urlunsplit(unparsed_name)
def cache_key(self, name): def cache_key(self, name):
return u'staticfiles:cache:%s' % name return u'staticfiles:cache:%s' % name
@ -98,7 +105,7 @@ class CachedFilesMixin(object):
hashed_name = self.hashed_name(name) hashed_name = self.hashed_name(name)
# set the cache if there was a miss (e.g. if cache server goes down) # set the cache if there was a miss (e.g. if cache server goes down)
self.cache.set(cache_key, hashed_name) self.cache.set(cache_key, hashed_name)
return super(CachedFilesMixin, self).url(hashed_name) return unquote(super(CachedFilesMixin, self).url(hashed_name))
def url_converter(self, name): def url_converter(self, name):
""" """
@ -132,9 +139,9 @@ class CachedFilesMixin(object):
else: else:
start, end = 1, sub_level - 1 start, end = 1, sub_level - 1
joined_result = '/'.join(name_parts[:-start] + url_parts[end:]) joined_result = '/'.join(name_parts[:-start] + url_parts[end:])
hashed_url = self.url(joined_result, force=True) hashed_url = self.url(unquote(joined_result), force=True)
# Return the hashed and normalized version to the file # Return the hashed and normalized version to the file
return 'url("%s")' % hashed_url return 'url("%s")' % unquote(hashed_url)
return converter return converter
def post_process(self, paths, dry_run=False, **options): def post_process(self, paths, dry_run=False, **options):

View File

@ -1,5 +1,6 @@
@import url("../cached/styles.css"); @import url("../cached/styles.css");
@import url("absolute.css"); @import url("absolute.css");
@import url("absolute.css#eggs");
body { body {
background: #d3d6d8 url(img/relative.png); background: #d3d6d8 url(img/relative.png);
} }

View File

@ -61,7 +61,7 @@ class BaseStaticFilesTestCase(object):
self.addCleanup(os.unlink, _nonascii_filepath) self.addCleanup(os.unlink, _nonascii_filepath)
def assertFileContains(self, filepath, text): def assertFileContains(self, filepath, text):
self.assertTrue(text in self._get_file(smart_unicode(filepath)), self.assertIn(text, self._get_file(smart_unicode(filepath)),
u"'%s' not in '%s'" % (text, filepath)) u"'%s' not in '%s'" % (text, filepath))
def assertFileNotFound(self, filepath): def assertFileNotFound(self, filepath):
@ -72,11 +72,15 @@ class BaseStaticFilesTestCase(object):
template = loader.get_template_from_string(template) template = loader.get_template_from_string(template)
return template.render(Context(kwargs)).strip() return template.render(Context(kwargs)).strip()
def assertTemplateRenders(self, template, result, **kwargs): def static_template_snippet(self, path):
return "{%% load static from staticfiles %%}{%% static '%s' %%}" % path
def assertStaticRenders(self, path, result, **kwargs):
template = self.static_template_snippet(path)
self.assertEqual(self.render_template(template, **kwargs), result) self.assertEqual(self.render_template(template, **kwargs), result)
def assertTemplateRaises(self, exc, template, result, **kwargs): def assertStaticRaises(self, exc, path, result, **kwargs):
self.assertRaises(exc, self.assertTemplateRenders, template, result, **kwargs) self.assertRaises(exc, self.assertStaticRenders, path, result, **kwargs)
class StaticFilesTestCase(BaseStaticFilesTestCase, TestCase): class StaticFilesTestCase(BaseStaticFilesTestCase, TestCase):
@ -99,8 +103,8 @@ class BaseCollectionTestCase(BaseStaticFilesTestCase):
settings.STATIC_ROOT = tempfile.mkdtemp() settings.STATIC_ROOT = tempfile.mkdtemp()
self.run_collectstatic() self.run_collectstatic()
# Use our own error handler that can handle .svn dirs on Windows # Use our own error handler that can handle .svn dirs on Windows
self.addCleanup(shutil.rmtree, settings.STATIC_ROOT, #self.addCleanup(shutil.rmtree, settings.STATIC_ROOT,
ignore_errors=True, onerror=rmtree_errorhandler) # ignore_errors=True, onerror=rmtree_errorhandler)
def tearDown(self): def tearDown(self):
settings.STATIC_ROOT = self.old_root settings.STATIC_ROOT = self.old_root
@ -194,8 +198,8 @@ class TestFindStatic(CollectionTestCase, TestDefaults):
finally: finally:
sys.stdout = _stdout sys.stdout = _stdout
self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line
self.assertTrue('project' in lines[1]) self.assertIn('project', lines[1])
self.assertTrue('apps' in lines[2]) self.assertIn('apps', lines[2])
class TestCollection(CollectionTestCase, TestDefaults): class TestCollection(CollectionTestCase, TestDefaults):
@ -284,73 +288,90 @@ class TestCollectionCachedStorage(BaseCollectionTestCase,
""" """
Tests for the Cache busting storage Tests for the Cache busting storage
""" """
def cached_file_path(self, relpath): def cached_file_path(self, path):
template = "{%% load static from staticfiles %%}{%% static '%s' %%}" fullpath = self.render_template(self.static_template_snippet(path))
fullpath = self.render_template(template % relpath)
return fullpath.replace(settings.STATIC_URL, '') return fullpath.replace(settings.STATIC_URL, '')
def test_template_tag_return(self): def test_template_tag_return(self):
""" """
Test the CachedStaticFilesStorage backend. Test the CachedStaticFilesStorage backend.
""" """
self.assertTemplateRaises(ValueError, """ self.assertStaticRaises(ValueError,
{% load static from staticfiles %}{% static "does/not/exist.png" %} "does/not/exist.png",
""", "/static/does/not/exist.png") "/static/does/not/exist.png")
self.assertTemplateRenders(""" self.assertStaticRenders("test/file.txt",
{% load static from staticfiles %}{% static "test/file.txt" %} "/static/test/file.dad0999e4f8f.txt")
""", "/static/test/file.dad0999e4f8f.txt") self.assertStaticRenders("cached/styles.css",
self.assertTemplateRenders(""" "/static/cached/styles.93b1147e8552.css")
{% load static from staticfiles %}{% static "cached/styles.css" %}
""", "/static/cached/styles.93b1147e8552.css")
def test_template_tag_simple_content(self): def test_template_tag_simple_content(self):
relpath = self.cached_file_path("cached/styles.css") relpath = self.cached_file_path("cached/styles.css")
self.assertEqual(relpath, "cached/styles.93b1147e8552.css") self.assertEqual(relpath, "cached/styles.93b1147e8552.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read() content = relfile.read()
self.assertFalse("cached/other.css" in content, content) self.assertNotIn("cached/other.css", content)
self.assertTrue("/static/cached/other.d41d8cd98f00.css" in content) self.assertIn("/static/cached/other.d41d8cd98f00.css", content)
def test_path_with_querystring(self):
relpath = self.cached_file_path("cached/styles.css?spam=eggs")
self.assertEqual(relpath,
"cached/styles.93b1147e8552.css?spam=eggs")
with storage.staticfiles_storage.open(
"cached/styles.93b1147e8552.css") as relfile:
content = relfile.read()
self.assertNotIn("cached/other.css", content)
self.assertIn("/static/cached/other.d41d8cd98f00.css", content)
def test_path_with_fragment(self):
relpath = self.cached_file_path("cached/styles.css#eggs")
self.assertEqual(relpath, "cached/styles.93b1147e8552.css#eggs")
with storage.staticfiles_storage.open(
"cached/styles.93b1147e8552.css") as relfile:
content = relfile.read()
self.assertNotIn("cached/other.css", content)
self.assertIn("/static/cached/other.d41d8cd98f00.css", content)
def test_template_tag_absolute(self): def test_template_tag_absolute(self):
relpath = self.cached_file_path("cached/absolute.css") relpath = self.cached_file_path("cached/absolute.css")
self.assertEqual(relpath, "cached/absolute.cc80cb5e2eb1.css") self.assertEqual(relpath, "cached/absolute.cc80cb5e2eb1.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read() content = relfile.read()
self.assertFalse("/static/cached/styles.css" in content) self.assertNotIn("/static/cached/styles.css", content)
self.assertTrue("/static/cached/styles.93b1147e8552.css" in content) self.assertIn("/static/cached/styles.93b1147e8552.css", content)
def test_template_tag_denorm(self): def test_template_tag_denorm(self):
relpath = self.cached_file_path("cached/denorm.css") relpath = self.cached_file_path("cached/denorm.css")
self.assertEqual(relpath, "cached/denorm.363de96e9b4b.css") self.assertEqual(relpath, "cached/denorm.363de96e9b4b.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read() content = relfile.read()
self.assertFalse("..//cached///styles.css" in content) self.assertNotIn("..//cached///styles.css", content)
self.assertTrue("/static/cached/styles.93b1147e8552.css" in content) self.assertIn("/static/cached/styles.93b1147e8552.css", content)
def test_template_tag_relative(self): def test_template_tag_relative(self):
relpath = self.cached_file_path("cached/relative.css") relpath = self.cached_file_path("cached/relative.css")
self.assertEqual(relpath, "cached/relative.8dffb45d91f5.css") self.assertEqual(relpath, "cached/relative.2217ea7273c2.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read() content = relfile.read()
self.assertFalse("../cached/styles.css" in content) self.assertNotIn("../cached/styles.css", content)
self.assertFalse('@import "styles.css"' in content) self.assertNotIn('@import "styles.css"', content)
self.assertTrue("/static/cached/styles.93b1147e8552.css" in content) self.assertIn("/static/cached/styles.93b1147e8552.css", content)
self.assertFalse("url(img/relative.png)" in content) self.assertNotIn("url(img/relative.png)", content)
self.assertTrue("/static/cached/img/relative.acae32e4532b.png" in content) self.assertIn("/static/cached/img/relative.acae32e4532b.png", content)
self.assertIn("/static/cached/absolute.cc80cb5e2eb1.css#eggs", content)
def test_template_tag_deep_relative(self): def test_template_tag_deep_relative(self):
relpath = self.cached_file_path("cached/css/window.css") relpath = self.cached_file_path("cached/css/window.css")
self.assertEqual(relpath, "cached/css/window.9db38d5169f3.css") self.assertEqual(relpath, "cached/css/window.9db38d5169f3.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
content = relfile.read() content = relfile.read()
self.assertFalse('url(img/window.png)' in content) self.assertNotIn('url(img/window.png)', content)
self.assertTrue('url("/static/cached/css/img/window.acae32e4532b.png")' in content) self.assertIn('url("/static/cached/css/img/window.acae32e4532b.png")', content)
def test_template_tag_url(self): def test_template_tag_url(self):
relpath = self.cached_file_path("cached/url.css") relpath = self.cached_file_path("cached/url.css")
self.assertEqual(relpath, "cached/url.615e21601e4b.css") self.assertEqual(relpath, "cached/url.615e21601e4b.css")
with storage.staticfiles_storage.open(relpath) as relfile: with storage.staticfiles_storage.open(relpath) as relfile:
self.assertTrue("https://" in relfile.read()) self.assertIn("https://", relfile.read())
def test_cache_invalidation(self): def test_cache_invalidation(self):
name = "cached/styles.css" name = "cached/styles.css"
@ -367,7 +388,6 @@ class TestCollectionCachedStorage(BaseCollectionTestCase,
cached_name = storage.staticfiles_storage.cache.get(cache_key) cached_name = storage.staticfiles_storage.cache.get(cache_key)
self.assertEqual(cached_name, hashed_name) self.assertEqual(cached_name, hashed_name)
# we set DEBUG to False here since the template tag wouldn't work otherwise # we set DEBUG to False here since the template tag wouldn't work otherwise
TestCollectionCachedStorage = override_settings(**dict(TEST_SETTINGS, TestCollectionCachedStorage = override_settings(**dict(TEST_SETTINGS,
STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage', STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage',
@ -517,9 +537,9 @@ class TestMiscFinder(TestCase):
default_storage._wrapped = empty default_storage._wrapped = empty
def test_get_finder(self): def test_get_finder(self):
self.assertTrue(isinstance(finders.get_finder( self.assertIsInstance(finders.get_finder(
'django.contrib.staticfiles.finders.FileSystemFinder'), 'django.contrib.staticfiles.finders.FileSystemFinder'),
finders.FileSystemFinder)) finders.FileSystemFinder)
def test_get_finder_bad_classname(self): def test_get_finder_bad_classname(self):
self.assertRaises(ImproperlyConfigured, finders.get_finder, self.assertRaises(ImproperlyConfigured, finders.get_finder,
@ -545,9 +565,6 @@ class TestMiscFinder(TestCase):
class TestTemplateTag(StaticFilesTestCase): class TestTemplateTag(StaticFilesTestCase):
def test_template_tag(self): def test_template_tag(self):
self.assertTemplateRenders(""" self.assertStaticRenders("does/not/exist.png",
{% load static from staticfiles %}{% static "does/not/exist.png" %} "/static/does/not/exist.png")
""", "/static/does/not/exist.png") self.assertStaticRenders("testfile.txt", "/static/testfile.txt")
self.assertTemplateRenders("""
{% load static from staticfiles %}{% static "testfile.txt" %}
""", "/static/testfile.txt")