Introduce new way of adding a suffix to URLs in CssAbsoluteFilter, by hashing the content of the corresponding filenames.
Set COMPRESS_CSS_HASHING_METHOD to 'hash' to use. Fixes #105.
This commit is contained in:

committed by
Jannis Leidel

parent
a32c7e779d
commit
df4ea4b4fa
@@ -24,6 +24,8 @@ class CompressorSettings(AppSettings):
|
||||
ROOT = None
|
||||
|
||||
CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
|
||||
CSS_HASHING_METHOD = 'mtime'
|
||||
|
||||
JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
|
||||
PRECOMPILERS = (
|
||||
# ('text/coffeescript', 'coffee --compile --stdio'),
|
||||
|
@@ -1 +1,2 @@
|
||||
from compressor.filters.base import (FilterBase, CallbackOutputFilter, CompilerFilter, FilterError)
|
||||
from compressor.filters.base import (FilterBase, CallbackOutputFilter,
|
||||
CompilerFilter, FilterError)
|
||||
|
@@ -2,7 +2,7 @@ import os
|
||||
import re
|
||||
import posixpath
|
||||
|
||||
from compressor.cache import get_hashed_mtime
|
||||
from compressor.cache import get_hexdigest, get_hashed_mtime
|
||||
from compressor.conf import settings
|
||||
from compressor.filters import FilterBase
|
||||
from compressor.utils import staticfiles
|
||||
@@ -27,7 +27,6 @@ class CssAbsoluteFilter(FilterBase):
|
||||
return self.content
|
||||
self.path = basename.replace(os.sep, '/')
|
||||
self.path = self.path.lstrip('/')
|
||||
self.mtime = get_hashed_mtime(filename)
|
||||
if self.url.startswith(('http://', 'https://')):
|
||||
self.has_scheme = True
|
||||
parts = self.url.split('/')
|
||||
@@ -54,24 +53,36 @@ class CssAbsoluteFilter(FilterBase):
|
||||
filename = os.path.join(self.root, local_path.lstrip(os.sep))
|
||||
return os.path.exists(filename) and filename
|
||||
|
||||
def add_mtime(self, url):
|
||||
def add_suffix(self, url):
|
||||
filename = self.guess_filename(url)
|
||||
mtime = filename and get_hashed_mtime(filename) or self.mtime
|
||||
if mtime is None:
|
||||
suffix = None
|
||||
if filename:
|
||||
if settings.COMPRESS_CSS_HASHING_METHOD == "mtime":
|
||||
suffix = get_hashed_mtime(filename)
|
||||
elif settings.COMPRESS_CSS_HASHING_METHOD == "hash":
|
||||
hash_file = open(filename)
|
||||
try:
|
||||
suffix = get_hexdigest(hash_file.read(), 12)
|
||||
finally:
|
||||
hash_file.close()
|
||||
else:
|
||||
raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
|
||||
'with an unknown method (%s).')
|
||||
if suffix is None:
|
||||
return url
|
||||
if url.startswith(('http://', 'https://', '/')):
|
||||
if "?" in url:
|
||||
url = "%s&%s" % (url, mtime)
|
||||
url = "%s&%s" % (url, suffix)
|
||||
else:
|
||||
url = "%s?%s" % (url, mtime)
|
||||
url = "%s?%s" % (url, suffix)
|
||||
return url
|
||||
|
||||
def url_converter(self, matchobj):
|
||||
url = matchobj.group(1)
|
||||
url = url.strip(' \'"')
|
||||
if url.startswith(('http://', 'https://', '/', 'data:')):
|
||||
return "url('%s')" % self.add_mtime(url)
|
||||
return "url('%s')" % self.add_suffix(url)
|
||||
full_url = posixpath.normpath('/'.join([str(self.directory_name), url]))
|
||||
if self.has_scheme:
|
||||
full_url = "%s%s" % (self.protocol, full_url)
|
||||
return "url('%s')" % self.add_mtime(full_url)
|
||||
return "url('%s')" % self.add_suffix(full_url)
|
||||
|
@@ -70,6 +70,11 @@ Possible options are:
|
||||
|
||||
A filter that normalizes the URLs used in ``url()`` CSS statements.
|
||||
|
||||
- ``COMPRESS_CSS_HASHING_METHOD`` -- The method to use when calculating
|
||||
the hash to append to processed URLs. Either ``'mtime'`` (default) or
|
||||
``'hash'``. Use the latter in case you're using multiple server to serve
|
||||
your static files.
|
||||
|
||||
``compressor.filters.csstidy.CSSTidyFilter``
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
p { background: url('../../../images/test.png'); }
|
||||
p { background: url(../../../images/test.png); }
|
||||
p { background: url( ../../../images/test.png ); }
|
||||
p { background: url( '../../../images/test.png' ); }
|
||||
p { background: url('../../../img/add.png'); }
|
||||
p { background: url(../../../img/add.png); }
|
||||
p { background: url( ../../../img/add.png ); }
|
||||
p { background: url( '../../../img/add.png' ); }
|
||||
|
@@ -1,4 +1,4 @@
|
||||
p { background: url('../../images/test.png'); }
|
||||
p { background: url(../../images/test.png); }
|
||||
p { background: url( ../../images/test.png ); }
|
||||
p { background: url( '../../images/test.png' ); }
|
||||
p { background: url('../../img/python.png'); }
|
||||
p { background: url(../../img/python.png); }
|
||||
p { background: url( ../../img/python.png ); }
|
||||
p { background: url( '../../img/python.png' ); }
|
||||
|
@@ -86,62 +86,88 @@ class CssAbsolutizingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/url/url1.css" type="text/css">
|
||||
<link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def suffix_method(self, filename):
|
||||
return get_hashed_mtime(filename)
|
||||
|
||||
def test_css_absolute_filter(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
content = "p { background: url('../../img/python.png') }"
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'http://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_https(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
content = "p { background: url('../../img/python.png') }"
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_relative_path(self):
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
|
||||
content = "p { background: url('../../images/image.gif') }"
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
content = "p { background: url('../../img/python.png') }"
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
settings.COMPRESS_URL = 'https://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
|
||||
output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_hunks(self):
|
||||
hash_dict = {
|
||||
'hash1': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/url1.css')),
|
||||
'hash2': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/2/url2.css')),
|
||||
'hash1': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
|
||||
'hash2': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
|
||||
}
|
||||
out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict,
|
||||
u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict]
|
||||
out = [u"p { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\n" % hash_dict,
|
||||
u"p { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\n" % hash_dict]
|
||||
hunks = [h for m, h in self.css_node.hunks()]
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
|
||||
|
||||
class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
|
||||
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
settings.COMPRESS_CSS_HASHING_METHOD = 'hash'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/url/url1.css" type="text/css" charset="utf-8">
|
||||
<link rel="stylesheet" href="/media/css/url/2/url2.css" type="text/css" charset="utf-8">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
def suffix_method(self, filename):
|
||||
f = open(filename)
|
||||
suffix = "H%s" % (get_hexdigest(f.read(), 12), )
|
||||
f.close()
|
||||
return suffix
|
||||
|
||||
|
||||
class CssDataUriTestCase(TestCase):
|
||||
def setUp(self):
|
||||
settings.COMPRESS_ENABLED = True
|
||||
@@ -150,6 +176,7 @@ class CssDataUriTestCase(TestCase):
|
||||
'compressor.filters.datauri.CssDataUriFilter',
|
||||
]
|
||||
settings.COMPRESS_URL = '/media/'
|
||||
settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/media/css/datauri.css" type="text/css">
|
||||
"""
|
||||
|
Reference in New Issue
Block a user