From 4701bd3a55c409cccb41669b4fc83f46b7b2bc12 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Feb 2010 18:42:28 +0100 Subject: [PATCH] Added support for pluggable storage backends to agnostically handle the actual build location. --- compressor/__init__.py | 44 +++++++++++++-------------- compressor/conf/settings.py | 1 + compressor/storage.py | 20 +++++++++++++ tests/core/tests.py | 59 +++++++++++++++++++++++++++++-------- 4 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 compressor/storage.py diff --git a/compressor/__init__.py b/compressor/__init__.py index c5604e2..1b52154 100644 --- a/compressor/__init__.py +++ b/compressor/__init__.py @@ -5,6 +5,9 @@ from django import template from django.conf import settings as django_settings from django.template.loader import render_to_string +from django.core.files.base import ContentFile +from django.core.files.storage import get_storage_class + from compressor.conf import settings from compressor import filters @@ -42,16 +45,12 @@ class Compressor(object): raise NotImplementedError('split_contents must be defined in a subclass') def get_filename(self, url): - if not url.startswith(settings.MEDIA_URL): - raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, settings.MEDIA_URL)) - # .lstrip used to remove leading slashes because os.path.join - # counterintuitively takes "/foo/bar" and "/baaz" to produce "/baaz", - # not the "/foo/bar/baaz" which you might expect: - basename = url.replace(settings.MEDIA_URL, "", 1).lstrip("/") - filename = os.path.join(settings.MEDIA_ROOT, basename) - if not os.path.exists(filename): - raise UncompressableFileError('"%s" does not exist' % (filename,)) - return filename + if not url.startswith(self.storage.base_url): + raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, self.storage.base_url)) + basename = url.replace(self.storage.base_url, "", 1) + if not self.storage.exists(basename): + raise UncompressableFileError('"%s" does not exist' % self.storage.path(basename)) + return self.storage.path(basename) @property def mtimes(self): @@ -64,6 +63,10 @@ class Compressor(object): cachestr = "".join(cachebits) return "django_compressor.%s" % get_hexdigest(cachestr)[:12] + @property + def storage(self): + return get_storage_class(settings.STORAGE)() + @property def hunks(self): if getattr(self, '_hunks', ''): @@ -119,28 +122,20 @@ class Compressor(object): @property def new_filepath(self): filename = "".join([self.hash, self.extension]) - filepath = "%s/%s/%s" % (settings.OUTPUT_DIR.strip('/'), self.ouput_prefix, filename) - return filepath + return "/".join((settings.OUTPUT_DIR.strip('/'), self.ouput_prefix, filename)) def save_file(self): - filename = "%s/%s" % (settings.MEDIA_ROOT.rstrip('/'), self.new_filepath) - if os.path.exists(filename): + if self.storage.exists(self.new_filepath): return False - dirname = os.path.dirname(filename) - if not os.path.exists(dirname): - os.makedirs(dirname) - fd = open(filename, 'wb+') - fd.write(self.combined) - fd.close() + self.storage.save(self.new_filepath, ContentFile(self.combined)) return True def output(self): if not settings.COMPRESS: return self.content - url = "%s/%s" % (settings.MEDIA_URL.rstrip('/'), self.new_filepath) self.save_file() context = getattr(self, 'extra_context', {}) - context['url'] = url + context['url'] = self.storage.url(self.new_filepath) return render_to_string(self.template_name, context) @@ -149,7 +144,10 @@ class CssCompressor(Compressor): def __init__(self, content, ouput_prefix="css"): self.extension = ".css" self.template_name = "compressor/css.html" - self.filters = ['compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssMediaFilter'] + self.filters = [ + 'compressor.filters.css_default.CssAbsoluteFilter', + 'compressor.filters.css_default.CssMediaFilter', + ] self.filters.extend(settings.COMPRESS_CSS_FILTERS) self.type = 'css' super(CssCompressor, self).__init__(content, ouput_prefix) diff --git a/compressor/conf/settings.py b/compressor/conf/settings.py index a441e61..e0d8b79 100644 --- a/compressor/conf/settings.py +++ b/compressor/conf/settings.py @@ -5,6 +5,7 @@ from django.conf import settings MEDIA_URL = getattr(settings, 'COMPRESS_URL', settings.MEDIA_URL) MEDIA_ROOT = getattr(settings, 'COMPRESS_ROOT', settings.MEDIA_ROOT) OUTPUT_DIR = getattr(settings, 'COMPRESS_OUTPUT_DIR', 'CACHE') +STORAGE = getattr(settings, 'COMPRESS_STORAGE', 'compressor.storage.CompressorFileStorage') COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG) COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', []) diff --git a/compressor/storage.py b/compressor/storage.py new file mode 100644 index 0000000..57c5ba7 --- /dev/null +++ b/compressor/storage.py @@ -0,0 +1,20 @@ +from django.core.files.storage import FileSystemStorage +from django.core.files.storage import get_storage_class + +from compressor.conf import settings + +class CompressorFileStorage(FileSystemStorage): + """ + Standard file system storage for files handled by django-compressor. + + The defaults for ``location`` and ``base_url`` are ``COMPRESS_ROOT`` and + ``COMPRESS_URL``. + + """ + def __init__(self, location=None, base_url=None, *args, **kwargs): + if location is None: + location = settings.MEDIA_ROOT + if base_url is None: + base_url = settings.MEDIA_URL + super(CompressorFileStorage, self).__init__(location, base_url, + *args, **kwargs) diff --git a/tests/core/tests.py b/tests/core/tests.py index cd09333..53d2d98 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -1,9 +1,12 @@ import os, re +import gzip from django.template import Template, Context from django.test import TestCase from compressor import CssCompressor, JsCompressor from compressor.conf import settings +from compressor.storage import CompressorFileStorage + from django.conf import settings as django_settings from BeautifulSoup import BeautifulSoup @@ -148,19 +151,19 @@ class CssMediaTestCase(TestCase): out = u'@media screen {body { background:#990; }}\n@media print {p { border:5px solid green;}}\n@media all {body { color:#fff; }}' self.assertEqual(out, self.cssNode.combined) +def render(template_string, context_dict=None): + """A shortcut for testing template output.""" + if context_dict is None: + context_dict = {} + + c = Context(context_dict) + t = Template(template_string) + return t.render(c).strip() + class TemplatetagTestCase(TestCase): def setUp(self): settings.COMPRESS = True - def render(self, template_string, context_dict=None): - """A shortcut for testing template output.""" - if context_dict is None: - context_dict = {} - - c = Context(context_dict) - t = Template(template_string) - return t.render(c).strip() - def test_css_tag(self): template = u"""{% load compress %}{% compress css %} @@ -170,7 +173,7 @@ class TemplatetagTestCase(TestCase): """ context = { 'MEDIA_URL': settings.MEDIA_URL } out = u'' - self.assertEqual(out, self.render(template, context)) + self.assertEqual(out, render(template, context)) def test_nonascii_css_tag(self): template = u"""{% load compress %}{% compress css %} @@ -180,7 +183,7 @@ class TemplatetagTestCase(TestCase): """ context = { 'MEDIA_URL': settings.MEDIA_URL } out = '' - self.assertEqual(out, self.render(template, context)) + self.assertEqual(out, render(template, context)) def test_js_tag(self): template = u"""{% load compress %}{% compress js %} @@ -190,5 +193,37 @@ class TemplatetagTestCase(TestCase): """ context = { 'MEDIA_URL': settings.MEDIA_URL } out = u'' - self.assertEqual(out, self.render(template, context)) + self.assertEqual(out, render(template, context)) +class TestStorage(CompressorFileStorage): + """ + Test compressor storage that gzips storage files + """ + def url(self, name): + return u'%s.gz' % super(TestStorage, self).url(name) + + def save(self, filename, content): + filename = super(TestStorage, self).save(filename, content) + out = gzip.open(u'%s.gz' % self.path(filename), 'wb') + out.writelines(open(self.path(filename), 'rb')) + out.close() + +class StorageTestCase(TestCase): + def setUp(self): + self._storage = settings.STORAGE + settings.STORAGE = 'core.tests.TestStorage' + settings.COMPRESS = True + + def tearDown(self): + settings.STORAGE = self._storage + + def test_css_tag_with_storage(self): + template = u"""{% load compress %}{% compress css %} + + + + {% endcompress %} + """ + context = { 'MEDIA_URL': settings.MEDIA_URL } + out = u'' + self.assertEqual(out, render(template, context))