Commit 248c105e by andreas.pelme

Merge branch 'master' of git://github.com/jsmits/django-compress

This adds custom versioning, e.g. hashes instead of mtime.

Documentation is still forthcoming.

git-svn-id: https://django-compress.googlecode.com/svn/trunk@81 98d35234-f74b-0410-9e22-51d878bdf110
parents 2552b07f 32452010
.AppleDouble .AppleDouble
*.pyc *.pyc
:2e_* :2e_*
/build
*.tmproj
...@@ -6,6 +6,7 @@ COMPRESS_AUTO = getattr(settings, 'COMPRESS_AUTO', True) ...@@ -6,6 +6,7 @@ COMPRESS_AUTO = getattr(settings, 'COMPRESS_AUTO', True)
COMPRESS_VERSION = getattr(settings, 'COMPRESS_VERSION', False) COMPRESS_VERSION = getattr(settings, 'COMPRESS_VERSION', False)
COMPRESS_VERSION_PLACEHOLDER = getattr(settings, 'COMPRESS_VERSION_PLACEHOLDER', '?') COMPRESS_VERSION_PLACEHOLDER = getattr(settings, 'COMPRESS_VERSION_PLACEHOLDER', '?')
COMPRESS_VERSION_DEFAULT = getattr(settings, 'COMPRESS_VERSION_DEFAULT', '0') COMPRESS_VERSION_DEFAULT = getattr(settings, 'COMPRESS_VERSION_DEFAULT', '0')
COMPRESS_VERSIONING = getattr(settings, 'COMPRESS_VERSIONING', 'compress.versioning.mtime.MTimeVersioning')
COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compress.filters.csstidy.CSSTidyFilter']) COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compress.filters.csstidy.CSSTidyFilter'])
COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compress.filters.jsmin.JSMinFilter']) COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compress.filters.jsmin.JSMinFilter'])
...@@ -17,6 +18,3 @@ if COMPRESS_CSS_FILTERS is None: ...@@ -17,6 +18,3 @@ if COMPRESS_CSS_FILTERS is None:
if COMPRESS_JS_FILTERS is None: if COMPRESS_JS_FILTERS is None:
COMPRESS_JS_FILTERS = [] COMPRESS_JS_FILTERS = []
if COMPRESS_VERSION and not COMPRESS_AUTO:
raise ImproperlyConfigured('COMPRESS_AUTO needs to be True when using COMPRESS_VERSION.')
\ No newline at end of file
...@@ -21,7 +21,8 @@ class Command(NoArgsCommand): ...@@ -21,7 +21,8 @@ class Command(NoArgsCommand):
from compress.utils import needs_update, filter_css, filter_js from compress.utils import needs_update, filter_css, filter_js
for name, css in settings.COMPRESS_CSS.items(): for name, css in settings.COMPRESS_CSS.items():
u, version = needs_update(css['output_filename'], css['source_filenames']) u, version = needs_update(css['output_filename'],
css['source_filenames'])
if (force or u) or verbosity >= 2: if (force or u) or verbosity >= 2:
msg = 'CSS Group \'%s\'' % name msg = 'CSS Group \'%s\'' % name
...@@ -36,7 +37,8 @@ class Command(NoArgsCommand): ...@@ -36,7 +37,8 @@ class Command(NoArgsCommand):
print print
for name, js in settings.COMPRESS_JS.items(): for name, js in settings.COMPRESS_JS.items():
u, version = needs_update(js['output_filename'], js['source_filenames']) u, version = needs_update(js['output_filename'],
js['source_filenames'])
if (force or u) or verbosity >= 2: if (force or u) or verbosity >= 2:
msg = 'JavaScript Group \'%s\'' % name msg = 'JavaScript Group \'%s\'' % name
......
...@@ -5,7 +5,7 @@ from django import template ...@@ -5,7 +5,7 @@ from django import template
from django.conf import settings as django_settings from django.conf import settings as django_settings
from compress.conf import settings from compress.conf import settings
from compress.utils import media_root, media_url, needs_update, filter_css, filter_js, get_output_filename, get_version from compress.utils import media_root, media_url, needs_update, filter_css, filter_js, get_output_filename, get_version, get_version_from_file
register = template.Library() register = template.Library()
...@@ -44,10 +44,15 @@ class CompressedCSSNode(template.Node): ...@@ -44,10 +44,15 @@ class CompressedCSSNode(template.Node):
version = None version = None
if settings.COMPRESS_AUTO: if settings.COMPRESS_AUTO:
u, version = needs_update(css['output_filename'], css['source_filenames']) u, version = needs_update(css['output_filename'],
css['source_filenames'])
if u: if u:
filter_css(css) filter_css(css)
else:
filename_base, filename = os.path.split(css['output_filename'])
path_name = media_root(filename_base)
version = get_version_from_file(path_name, filename)
return render_css(css, css['output_filename'], version) return render_css(css, css['output_filename'], version)
else: else:
# output source files # output source files
...@@ -80,9 +85,14 @@ class CompressedJSNode(template.Node): ...@@ -80,9 +85,14 @@ class CompressedJSNode(template.Node):
version = None version = None
if settings.COMPRESS_AUTO: if settings.COMPRESS_AUTO:
u, version = needs_update(js['output_filename'], js['source_filenames']) u, version = needs_update(js['output_filename'],
js['source_filenames'])
if u: if u:
filter_js(js) filter_js(js)
else:
filename_base, filename = os.path.split(js['output_filename'])
path_name = media_root(filename_base)
version = get_version_from_file(path_name, filename)
return render_js(js, js['output_filename'], version) return render_js(js, js['output_filename'], version)
else: else:
......
...@@ -9,22 +9,22 @@ from django.dispatch import dispatcher ...@@ -9,22 +9,22 @@ from django.dispatch import dispatcher
from compress.conf import settings from compress.conf import settings
from compress.signals import css_filtered, js_filtered from compress.signals import css_filtered, js_filtered
def get_filter(compressor_class): def get_class(class_string):
""" """
Convert a string version of a function name to the callable object. Convert a string version of a function name to the callable object.
""" """
if not hasattr(compressor_class, '__bases__'): if not hasattr(class_string, '__bases__'):
try: try:
compressor_class = compressor_class.encode('ascii') class_string = class_string.encode('ascii')
mod_name, class_name = get_mod_func(compressor_class) mod_name, class_name = get_mod_func(class_string)
if class_name != '': if class_name != '':
compressor_class = getattr(__import__(mod_name, {}, {}, ['']), class_name) class_string = getattr(__import__(mod_name, {}, {}, ['']), class_name)
except (ImportError, AttributeError): except (ImportError, AttributeError):
raise Exception('Failed to import filter %s' % compressor_class) raise Exception('Failed to import filter %s' % class_string)
return compressor_class return class_string
def get_mod_func(callback): def get_mod_func(callback):
""" """
...@@ -38,21 +38,21 @@ def get_mod_func(callback): ...@@ -38,21 +38,21 @@ def get_mod_func(callback):
return callback, '' return callback, ''
return callback[:dot], callback[dot+1:] return callback[:dot], callback[dot+1:]
def needs_update(output_file, source_files): def needs_update(output_file, source_files, verbosity=0):
""" """
Scan the source files for changes and returns True if the output_file needs to be updated. Scan the source files for changes and returns True if the output_file needs to be updated.
""" """
mtime = max_mtime(source_files) version = get_version(source_files)
version = get_version(mtime)
on = get_output_filename(output_file, version)
compressed_file_full = media_root(get_output_filename(output_file, version)) compressed_file_full = media_root(on)
if not os.path.exists(compressed_file_full): if not os.path.exists(compressed_file_full):
return True, version return True, version
# Check if the output file is outdated update_needed = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'needs_update')(output_file, source_files, version)
return (os.stat(compressed_file_full).st_mtime < mtime), mtime return update_needed
def media_root(filename): def media_root(filename):
""" """
...@@ -68,13 +68,11 @@ def concat(filenames, separator=''): ...@@ -68,13 +68,11 @@ def concat(filenames, separator=''):
Concatenate the files from the list of the ``filenames``, ouput separated with ``separator``. Concatenate the files from the list of the ``filenames``, ouput separated with ``separator``.
""" """
r = '' r = ''
for filename in filenames: for filename in filenames:
fd = open(media_root(filename), 'rb') fd = open(media_root(filename), 'rb')
r += fd.read() r += fd.read()
r += separator r += separator
fd.close() fd.close()
return r return r
def max_mtime(files): def max_mtime(files):
...@@ -87,19 +85,23 @@ def save_file(filename, contents): ...@@ -87,19 +85,23 @@ def save_file(filename, contents):
def get_output_filename(filename, version): def get_output_filename(filename, version):
if settings.COMPRESS_VERSION and version is not None: if settings.COMPRESS_VERSION and version is not None:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, get_version(version)) return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, version)
else: else:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT) return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT)
def get_version(version): def get_version(source_files, verbosity=0):
try: version = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'get_version')(source_files)
return str(int(version)) return version
except ValueError:
return str(version) def get_version_from_file(path, filename):
regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'([A-Za-z0-9]+)'))))
for f in os.listdir(path):
result = regex.match(f)
if result and result.groups():
return result.groups()[0]
def remove_files(path, filename, verbosity=0): def remove_files(path, filename, verbosity=0):
regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'\d+')))) regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'[A-Za-z0-9]+'))))
for f in os.listdir(path): for f in os.listdir(path):
if regex.match(f): if regex.match(f):
if verbosity >= 1: if verbosity >= 1:
...@@ -109,7 +111,8 @@ def remove_files(path, filename, verbosity=0): ...@@ -109,7 +111,8 @@ def remove_files(path, filename, verbosity=0):
def filter_common(obj, verbosity, filters, attr, separator, signal): def filter_common(obj, verbosity, filters, attr, separator, signal):
output = concat(obj['source_filenames'], separator) output = concat(obj['source_filenames'], separator)
filename = get_output_filename(obj['output_filename'], get_version(max_mtime(obj['source_filenames'])))
filename = get_output_filename(obj['output_filename'], get_version(obj['source_filenames']))
if settings.COMPRESS_VERSION: if settings.COMPRESS_VERSION:
remove_files(os.path.dirname(media_root(filename)), obj['output_filename'], verbosity) remove_files(os.path.dirname(media_root(filename)), obj['output_filename'], verbosity)
...@@ -118,7 +121,7 @@ def filter_common(obj, verbosity, filters, attr, separator, signal): ...@@ -118,7 +121,7 @@ def filter_common(obj, verbosity, filters, attr, separator, signal):
print "Saving %s" % filename print "Saving %s" % filename
for f in filters: for f in filters:
output = getattr(get_filter(f)(verbose=(verbosity >= 2)), attr)(output) output = getattr(get_class(f)(verbose=(verbosity >= 2)), attr)(output)
save_file(filename, output) save_file(filename, output)
signal.send(None) signal.send(None)
...@@ -127,4 +130,4 @@ def filter_css(css, verbosity=0): ...@@ -127,4 +130,4 @@ def filter_css(css, verbosity=0):
return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered) return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered)
def filter_js(js, verbosity=0): def filter_js(js, verbosity=0):
return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator=';', signal=js_filtered) return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator='', signal=js_filtered)
class VersioningBase(object):
def get_version(self, source_files):
raise NotImplementedError
def needs_update(self, output_file, source_files, version):
raise NotImplementedError
class VersioningError(Exception):
"""
This exception is raised when version creation fails
"""
pass
\ No newline at end of file
import cStringIO
from hashlib import md5, sha1
import os
from compress.conf import settings
from compress.utils import concat, get_output_filename
from compress.versioning.base import VersioningBase
class HashVersioningBase(VersioningBase):
def __init__(self, hash_method):
self.hash_method = hash_method
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
ph = settings.COMPRESS_VERSION_PLACEHOLDER
of = output_file
try:
phi = of.index(ph)
old_version = output_file_name[phi:phi+len(ph)-len(of)]
return (version != old_version), version
except ValueError:
# no placeholder found, do not update, manual update if needed
return False, version
def get_version(self, source_files):
buf = concat(source_files)
s = cStringIO.StringIO(buf)
version = self.get_hash(s)
s.close()
return version
def get_hash(self, f, CHUNK=2**16):
m = self.hash_method()
while 1:
chunk = f.read(CHUNK)
if not chunk:
break
m.update(chunk)
return m.hexdigest()
class MD5Versioning(HashVersioningBase):
def __init__(self):
super(MD5Versioning, self).__init__(md5)
class SHA1Versioning(HashVersioningBase):
def __init__(self):
super(SHA1Versioning, self).__init__(sha1)
\ No newline at end of file
import os
from compress.utils import get_output_filename, media_root
from compress.versioning.base import VersioningBase
def max_mtime(files):
return int(max([os.stat(media_root(f)).st_mtime for f in files]))
class MTimeVersioning(VersioningBase):
def get_version(self, source_files):
mtime = max_mtime(source_files)
try:
return str(int(mtime))
except ValueError:
return str(mtime)
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
compressed_file_full = media_root(output_file_name)
return (os.stat(compressed_file_full).st_mtime < version), version
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment