import os.path, hashlib, subprocess, re, shutil
from django import template
from django.conf import settings
from django.core.cache import cache
register = template.Library()
js_or_css = re.compile("(.*)\.(js|css)")
is_path = re.compile("\"(.*)\"")
# Locations of things for YUI compressor
JAVA = "/usr/bin/java"
YUI_COMPRESSOR_JAR = "/path/to/yuicompressor-2.3.6/build/yuicompressor-2.3.6.jar"
MINIFY = True
class CacheableNode(template.Node):
def __init__(self, rpath, path_literal=False):
self.path_literal = path_literal
if path_literal:
self.rpath = self.cache_resource(rpath)
else:
self.rpath = template.Variable(rpath)
def cache_resource(self, rpath):
if not settings.DEBUG:
cached_res = cache.get(rpath, False)
if not cached_res:
if rpath[0] == "/":
rpath = rpath[1:]
res_path = os.path.join(settings.DOCUMENT_ROOT, rpath)
if os.path.exists(res_path):
# Open the file for hashing
fin = open(res_path, "r")
s = fin.read()
fin.close()
# Update the MD5
hash = hashlib.md5()
hash.update(s)
(path, fname) = os.path.split(res_path)
cache_name = os.path.join(path, hash.hexdigest() + ".cache" + os.path.splitext(fname)[1])
if not os.path.exists(cache_name):
if js_or_css.match(fname) and MINIFY:
# Write out the cachable file
minified = open(cache_name, "w")
yuic = subprocess.Popen((JAVA, "-jar", YUI_COMPRESSOR_JAR, res_path), stdout=minified)
rtncode = yuic.wait()
minified.write("\n")
minified.close()
# If an error happened during minification, just copy the file over
if rtncode != 0:
print "ERROR Minifying %s" % res_path
shutil.copyfile(res_path, cache_name)
else:
shutil.copyfile(res_path, cache_name)
# Strip DOCUMENT_ROOT off file, leaving relative path
cache_path = cache_name.replace(settings.DOCUMENT_ROOT, "")
cache.set(rpath, cache_path, 3600) # Cache mapping for an hour
if cache_path[0] != "/":
cache_path = "/%s" % cache_path
return cache_path
else:
return rpath
# Return mapped path to cachable resource
return cached_res
return rpath
def render(self, context):
if self.path_literal:
return self.rpath
else:
rpath = self.cache_resource(self.rpath.resolve(context))
return rpath
@register.tag
def cachable(parser, token):
try:
tag_name, rpath = token.split_contents()
if is_path.match(rpath):
path_literal = True
else:
path_literal = False
rpath = rpath.replace("\"", "")
except ValueError:
raise template.TemplateSyntaxError, "%r tag requires a valid path relative to settings.DOCUMENT_ROOT" % token.contents.split()[0]
return CacheableNode(rpath, path_literal=path_literal)
Comments
I posted a simpler version of this, http://www.djangosnippets.org/snippets/946/ which appends the modification time to the url. The only downside I guess is if you are load balancing your media through different servers.
I like django_templatecomponents to do the minification and grouping of js and css files.
#
Actually, I think this template tag is quite a bit different than versioned_media. Besides YUI compressor support, I wanted forever caching based on the file contents' MD5 sum, not the filesystem's modification time, which can get updated at any time, even when the file's contents don't change. This also provides clean urls rather than using query string parameters.
I say forever caching, but that's not entirely true because it is strongly suggested that HTTP/1.1 responses not have future expire dates longer than 1 year. Despite that, a version of your file that is based on the MD5 sum will still be cacheable if you upgrade servers, move directories, use it in load-balancing situations, or other such server shenanigans. I'm using MD5 sums throughout my application for ETag support, so it was natural to choose that method for the template tag.
#