Created
July 23, 2012 20:39
-
-
Save valtron/3166089 to your computer and use it in GitHub Desktop.
Manages invalidation of css/js files to the browser by using the md5 hash, and can track dependencies between files.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import re | |
from django.conf import settings | |
from django.utils.safestring import mark_safe | |
class ResourceType(object): | |
def __init__(self, folder, extension, html): | |
self.folder = folder | |
self.extension = extension | |
self.html = html | |
ResourceType.JS = ResourceType( | |
folder = 'js', extension = 'js', | |
html = u'<script src="{url}?h={hash}" type="text/javascript"></script>', | |
) | |
ResourceType.CSS = ResourceType( | |
folder = 'css', extension = 'css', | |
html = u'<link href="{url}?h={hash}" rel="stylesheet" type="text/css"/>', | |
) | |
class Resource(object): | |
def __init__(self, type, file, dependencies): | |
self.file = file | |
self.hash = _calc_file_hash(os.path.join(settings.STATIC_DOC_ROOT, file)) | |
self.type = type | |
self.dependencies = dependencies | |
self.url = '{}{}'.format( | |
settings.MEDIA_URL, self.file | |
) | |
def __html__(self): | |
return mark_safe(self.type.html.format(url = self.url, hash = self.hash)) | |
def __str__(self): | |
return self.__html__() | |
def __unicode__(self): | |
return str(self) | |
def __hash__(self): | |
return hash(self.file) | |
def __repr__(self): | |
return u"Resource('{}')".format(self.file) | |
class ResourceSet(object): | |
def __init__(self): | |
self._all = get_resources() | |
self._included = [] | |
def add(self, resources): | |
for resource in resources: | |
resource = _normalize_resource_filename(resource) | |
if resource not in self._all: | |
raise MissingResourceException(resource) | |
self._included.append(self._all[resource]) | |
def __html__(self): | |
ret = '' | |
closure = set() | |
closure_list = [] | |
for resource in self._included: | |
self._resource_closure(closure, closure_list, resource) | |
for resource in closure_list: | |
ret += resource.__html__() | |
return mark_safe(ret) | |
def as_html(self): | |
return self.__html__() | |
def _resource_closure(self, out, out_list, resource): | |
if resource in out: | |
return | |
for dep in resource.dependencies: | |
self._resource_closure(out, out_list, self._all[dep]) | |
out.add(resource) | |
out_list.append(resource) | |
_resources_cache = None | |
def get_resources(): | |
global _resources_cache | |
if _resources_cache is None: | |
_resources_cache = _get_resources() | |
return _resources_cache | |
def _get_resources(): | |
static_dir = settings.STATIC_DOC_ROOT | |
resources = {} | |
resource_types = [ | |
ResourceType.JS, ResourceType.CSS | |
] | |
for resource_type in resource_types: | |
rootdir = os.path.join(settings.STATIC_DOC_ROOT, resource_type.folder) | |
for file in _list_files_of_type(rootdir, resource_type.extension): | |
deps = _get_file_dependencies(file) | |
relfile = _normalize_resource_filename(file) | |
resources[relfile] = Resource(resource_type, relfile, deps) | |
_validate_dependencies(resources) | |
return resources | |
def _normalize_resource_filename(file): | |
if file.startswith(settings.STATIC_DOC_ROOT): | |
relfile = os.path.relpath(file, settings.STATIC_DOC_ROOT) | |
else: | |
relfile = file | |
return relfile.replace('\\', '/') | |
def _validate_dependencies(resources): | |
dependency_problems = [] | |
for resource in resources.values(): | |
for dep in resource.dependencies: | |
if dep not in resources: | |
dependency_problems.append((resource, dep)) | |
if dependency_problems: | |
raise MissingResourceDependencyException(dependency_problems) | |
class ResourceException(Exception): | |
pass | |
class MissingResourceDependencyException(ResourceException): | |
def __init__(self, dependency_problems): | |
self.dependency_problems = dependency_problems | |
def __str__(self): | |
return 'Several resource dependencies cannot be satisfied:\n{}'.format( | |
repr(self.dependency_problems) | |
) | |
class MissingResourceException(ResourceException): | |
def __init__(self, resource): | |
self.resource = resource | |
def __str__(self): | |
return 'Missing resource: {}'.format(self.resource) | |
def _list_files_of_type(rootdir, ext): | |
dotext = '.' + ext | |
for r, _, files in os.walk(rootdir): | |
for file in files: | |
if file.endswith(ext): | |
yield os.path.join(r, file) | |
def _get_file_dependencies(file): | |
deps = [] | |
for line in open(file, 'rb'): | |
m = re.search(r'#require\s+(\S+)', line) | |
if m: | |
deps.append(m.group(1)) | |
return deps | |
def _calc_file_hash(filename): | |
hash = _md5_for_file(filename) | |
return hash[0:6] | |
def _md5_for_file(filename): | |
import hashlib | |
block_size = 131072 | |
md5 = hashlib.md5() | |
f = open(filename, 'rb') | |
while True: | |
data = f.read(block_size) | |
if not data: | |
break | |
md5.update(data) | |
return md5.hexdigest() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment