diff --git a/README b/README index 39eade4..9780d40 100644 --- a/README +++ b/README @@ -10,7 +10,7 @@ Example, in some urls.py: from django.conf.urls import url, url_patterns from django_downloadview import ObjectDownloadView - from demoproject.download.models import Document # A model with a FileField. + from demoproject.download.models import Document # A model with a FileField # ObjectDownloadView inherits from django.views.generic.BaseDetailView. diff --git a/demo/README b/demo/README index 11f42a2..a826a1f 100644 --- a/demo/README +++ b/demo/README @@ -2,8 +2,8 @@ Demo project ############ -The :file:`demo/` folder holds a demo project to illustrate (and test) -django-downloadview usage. +The :file:`demo/` folder holds a demo project to illustrate django-downloadview +usage. *********************** @@ -47,6 +47,44 @@ at http://localhost:8000/ Browse and use :file:`demo/demoproject/` as a sandbox. +********************************* +Base example provided in the demo +********************************* + +In the "demoproject" project, there is an application called "download". + +:file:`demo/demoproject/settings.py`: + +.. literalinclude:: ../demo/demoproject/settings.py + :language: python + :lines: 33-49 + :emphasize-lines: 44 + +This application holds a ``Document`` model. + +:file:`demo/demoproject/download/models.py`: + +.. literalinclude:: ../demo/demoproject/download/models.py + :language: python + +.. note:: + + The ``storage`` is the default one, i.e. it uses ``settings.MEDIA_ROOT``. + Combined to this ``upload_to`` configuration, files for ``Document`` model + live in :file:`var/media/document/` folder, relative to your + django-downloadview clone root. + +There is a download view named "download_document" for this model: + +:file:`demo/demoproject/download/urls.py`: + +.. literalinclude:: ../demo/demoproject/download/urls.py + :language: python + +As is, Django is to serve the files, i.e. load chunks into memory and stream +them. + + ********** References ********** diff --git a/demo/demoproject/download/models.py b/demo/demoproject/download/models.py index 4c89ae5..0ff248a 100644 --- a/demo/demoproject/download/models.py +++ b/demo/demoproject/download/models.py @@ -1,8 +1,7 @@ from django.db import models -from django.utils.translation import ugettext_lazy as _ class Document(models.Model): """A sample model with a FileField.""" - slug = models.SlugField(verbose_name=_('slug')) - file = models.FileField(verbose_name=_('file'), upload_to='document') + slug = models.SlugField(verbose_name='slug') + file = models.FileField(verbose_name='file', upload_to='document') diff --git a/demo/demoproject/download/tests.py b/demo/demoproject/download/tests.py index 6e426f5..ee631b5 100644 --- a/demo/demoproject/download/tests.py +++ b/demo/demoproject/download/tests.py @@ -1,16 +1,12 @@ -"""Test suite for django-downloadview.""" +"""Test suite for demoproject.download.""" from os import listdir from os.path import abspath, dirname, join -import shutil -import tempfile -from django.conf import settings from django.core.files import File from django.core.urlresolvers import reverse_lazy as reverse from django.test import TestCase -from django.test.utils import override_settings -from django_downloadview.nginx import XAccelRedirectResponse +from django_downloadview.test import temporary_media_root from demoproject.download.models import Document @@ -19,49 +15,11 @@ app_dir = dirname(abspath(__file__)) fixtures_dir = join(app_dir, 'fixtures') -class temporary_media_root(override_settings): - """Context manager or decorator to override settings.MEDIA_ROOT. - - >>> from django.conf import settings - >>> global_media_root = settings.MEDIA_ROOT - >>> with temporary_media_root(): - ... global_media_root == settings.MEDIA_ROOT - False - >>> global_media_root == settings.MEDIA_ROOT - True - - >>> @temporary_media_root - ... def use_temporary_media_root(): - ... return settings.MEDIA_ROOT - >>> tmp_media_root = use_temporary_media_root() - >>> global_media_root == tmp_media_root - False - >>> global_media_root == settings.MEDIA_ROOT - True - - """ - def enable(self): - """Create a temporary directory and use it to override - settings.MEDIA_ROOT.""" - tmp_dir = tempfile.mkdtemp() - self.options['MEDIA_ROOT'] = tmp_dir - super(temporary_media_root, self).enable() - - def disable(self): - """Remove directory settings.MEDIA_ROOT then restore original - setting.""" - shutil.rmtree(settings.MEDIA_ROOT) - super(temporary_media_root, self).disable() - - class DownloadTestCase(TestCase): """Base class for download tests.""" def setUp(self): """Common setup.""" super(DownloadTestCase, self).setUp() - self.download_hello_world_url = reverse('download_hello_world') - self.download_document_url = reverse('download_document', - kwargs={'slug': 'hello-world'}) self.files = {} for f in listdir(fixtures_dir): self.files[f] = abspath(join(fixtures_dir, f)) @@ -70,8 +28,11 @@ class DownloadTestCase(TestCase): class DownloadViewTestCase(DownloadTestCase): """Test generic DownloadView.""" def test_download_hello_world(self): - """Download_hello_world view returns hello-world.txt as attachement.""" - response = self.client.get(self.download_hello_world_url) + """'download_hello_world' view returns hello-world.txt as attachement. + + """ + download_url = reverse('download_hello_world') + response = self.client.get(download_url) self.assertEquals(response.status_code, 200) self.assertEquals(response['Content-Type'], 'text/plain; charset=utf-8') @@ -86,12 +47,14 @@ class ObjectDownloadViewTestCase(DownloadTestCase): """Test generic ObjectDownloadView.""" @temporary_media_root() def test_download_hello_world(self): - """Download_hello_world view returns hello-world.txt as attachement.""" + """'download_document' view returns hello-world.txt as attachement.""" + slug = 'hello-world' + download_url = reverse('download_document', kwargs={'slug': slug}) document = Document.objects.create( - slug='hello-world', + slug=slug, file=File(open(self.files['hello-world.txt'])), ) - response = self.client.get(self.download_document_url) + response = self.client.get(download_url) self.assertEquals(response.status_code, 200) self.assertEquals(response['Content-Type'], 'text/plain; charset=utf-8') @@ -100,24 +63,3 @@ class ObjectDownloadViewTestCase(DownloadTestCase): 'attachment; filename=hello-world.txt') self.assertEqual(open(self.files['hello-world.txt']).read(), response.content) - - -class XAccelRedirectDecoratorTestCase(DownloadTestCase): - @temporary_media_root() - def test_response(self): - document = Document.objects.create( - slug='hello-world', - file=File(open(self.files['hello-world.txt'])), - ) - download_url = reverse('download_document_nginx', - kwargs={'slug': 'hello-world'}) - response = self.client.get(download_url) - self.assertEquals(response.status_code, 200) - self.assertTrue(isinstance(response, XAccelRedirectResponse)) - self.assertEquals(response['Content-Type'], - 'text/plain; charset=utf-8') - self.assertFalse('ContentEncoding' in response) - self.assertEquals(response['Content-Disposition'], - 'attachment; filename=hello-world.txt') - self.assertEquals(response['X-Accel-Redirect'], - '/download-optimized/document/hello-world.txt') diff --git a/demo/demoproject/download/urls.py b/demo/demoproject/download/urls.py index 32da47d..53e4296 100644 --- a/demo/demoproject/download/urls.py +++ b/demo/demoproject/download/urls.py @@ -1,4 +1,4 @@ -"""URLconf for tests.""" +"""URL mapping.""" from django.conf.urls import patterns, include, url @@ -7,6 +7,4 @@ urlpatterns = patterns('demoproject.download.views', name='download_hello_world'), url(r'^document/(?P[a-zA-Z0-9_-]+)/$', 'download_document', name='download_document'), - url(r'^document-nginx/(?P[a-zA-Z0-9_-]+)/$', - 'download_document_nginx', name='download_document_nginx'), ) diff --git a/demo/demoproject/download/views.py b/demo/demoproject/download/views.py index 6c46508..6ba9854 100644 --- a/demo/demoproject/download/views.py +++ b/demo/demoproject/download/views.py @@ -1,7 +1,6 @@ from os.path import abspath, dirname, join from django_downloadview import DownloadView, ObjectDownloadView -from django_downloadview.nginx import x_accel_redirect from demoproject.download.models import Document @@ -15,7 +14,3 @@ download_hello_world = DownloadView.as_view(filename=hello_world_file, storage=None) download_document = ObjectDownloadView.as_view(model=Document) - -download_document_nginx = x_accel_redirect(download_document, - media_root='/var/www/files', - media_url='/download-optimized') diff --git a/demo/demoproject/nginx/__init__.py b/demo/demoproject/nginx/__init__.py new file mode 100644 index 0000000..53fd31e --- /dev/null +++ b/demo/demoproject/nginx/__init__.py @@ -0,0 +1 @@ +"""Nginx optimizations applied to demoproject.download.""" diff --git a/demo/demoproject/nginx/models.py b/demo/demoproject/nginx/models.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/demoproject/nginx/tests.py b/demo/demoproject/nginx/tests.py new file mode 100644 index 0000000..9a872ef --- /dev/null +++ b/demo/demoproject/nginx/tests.py @@ -0,0 +1,39 @@ +"""Test suite for demoproject.nginx.""" +from django.core.files import File +from django.core.urlresolvers import reverse_lazy as reverse + +from django_downloadview.nginx import assert_x_accel_redirect +from django_downloadview.test import temporary_media_root + +from demoproject.download.models import Document +from demoproject.download.tests import DownloadTestCase + + +class XAccelRedirectDecoratorTestCase(DownloadTestCase): + @temporary_media_root() + def test_response(self): + """'download_document_nginx' view returns a valid X-Accel response.""" + document = Document.objects.create( + slug='hello-world', + file=File(open(self.files['hello-world.txt'])), + ) + download_url = reverse('download_document_nginx', + kwargs={'slug': 'hello-world'}) + response = self.client.get(download_url) + self.assertEquals(response.status_code, 200) + # Validation shortcut: assert_x_accel_redirect. + assert_x_accel_redirect( + self, + response, + content_type="text/plain; charset=utf-8", + charset="utf-8", + basename="hello-world.txt", + redirect_url="/download-optimized/document/hello-world.txt", + expires=None, + with_buffering=None, + limit_rate=None) + # Check some more items, because this test is part of + # django-downloadview tests. + self.assertFalse('ContentEncoding' in response) + self.assertEquals(response['Content-Disposition'], + 'attachment; filename=hello-world.txt') diff --git a/demo/demoproject/nginx/urls.py b/demo/demoproject/nginx/urls.py new file mode 100644 index 0000000..b36d6c2 --- /dev/null +++ b/demo/demoproject/nginx/urls.py @@ -0,0 +1,8 @@ +"""URL mapping.""" +from django.conf.urls import patterns, include, url + + +urlpatterns = patterns('demoproject.nginx.views', + url(r'^document-nginx/(?P[a-zA-Z0-9_-]+)/$', + 'download_document_nginx', name='download_document_nginx'), +) diff --git a/demo/demoproject/nginx/views.py b/demo/demoproject/nginx/views.py new file mode 100644 index 0000000..68a104b --- /dev/null +++ b/demo/demoproject/nginx/views.py @@ -0,0 +1,9 @@ +"""Views.""" +from django_downloadview.nginx import x_accel_redirect + +from demoproject.download.views import download_document + + +download_document_nginx = x_accel_redirect(download_document, + media_root='/var/www/files', + media_url='/download-optimized') diff --git a/demo/demoproject/settings.py b/demo/demoproject/settings.py index 9328293..57ccda1 100755 --- a/demo/demoproject/settings.py +++ b/demo/demoproject/settings.py @@ -1,164 +1,73 @@ -# Django settings for Django-DownloadView demo project. +"""Django settings for Django-DownloadView demo project.""" from os.path import abspath, dirname, join + +# Configure some relative directories. demoproject_dir = dirname(abspath(__file__)) demo_dir = dirname(demoproject_dir) root_dir = dirname(demo_dir) data_dir = join(root_dir, 'var') -DEBUG = True -TEMPLATE_DEBUG = DEBUG -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) +# Mandatory settings. +ROOT_URLCONF = 'demoproject.urls' +WSGI_APPLICATION = 'demoproject.wsgi.application' -MANAGERS = ADMINS +# Database. DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': join(data_dir, 'db.sqlite'), - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', } } -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# In a Windows environment this must be set to your system time zone. -TIME_ZONE = 'America/Chicago' -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale. -USE_L10N = True - -# If you set this to False, Django will not use timezone-aware datetimes. -USE_TZ = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" +# Media and static files. MEDIA_ROOT = join(data_dir, 'media') - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" +MEDIA_URL = '/media/' STATIC_ROOT = join(data_dir, 'static') - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" STATIC_URL = '/static/' -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '123456789' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'demoproject.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'demoproject.wsgi.application' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) +# Applications. INSTALLED_APPS = ( + # Standard Django applications. 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', + # The actual django-downloadview demo. 'demoproject', - 'demoproject.download', + 'demoproject.download', # Sample standard download views. + 'demoproject.nginx', # Sample optimizations for Nginx. + # For test purposes. The demo project is part of django-downloadview + # test suite. 'django_nose', ) -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} +# Default middlewares. You may alter the list later. +MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + + +# Uncomment the following lines to enable global Nginx optimizations. +#MIDDLEWARE_CLASSES.append('django_downloadview.nginx.XAccelRedirectMiddleware') +#NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT +#NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = "/proxied-download" + + +# Development configuratio. +DEBUG = True +TEMPLATE_DEBUG = DEBUG TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' NOSE_ARGS = ['--verbose', '--nocapture', diff --git a/demo/demoproject/urls.py b/demo/demoproject/urls.py index 2057628..d7597a1 100755 --- a/demo/demoproject/urls.py +++ b/demo/demoproject/urls.py @@ -6,6 +6,10 @@ home = TemplateView.as_view(template_name='home.html') urlpatterns = patterns('', + # Standard download views. url(r'^download/', include('demoproject.download.urls')), + # Nginx optimizations. + url(r'^nginx/', include('demoproject.nginx.urls')), + # An informative page. url(r'', home, name='home') ) diff --git a/django_downloadview/nginx.py b/django_downloadview/nginx.py index 37342b3..7c8c623 100644 --- a/django_downloadview/nginx.py +++ b/django_downloadview/nginx.py @@ -69,8 +69,8 @@ class XAccelRedirectResponse(HttpResponse): with_buffering=None, limit_rate=None): """Return a HttpResponse with headers for Nginx X-Accel-Redirect.""" super(XAccelRedirectResponse, self).__init__(content_type=content_type) - basename = basename or redirect_url.split('/')[-1] - self['Content-Disposition'] = 'attachment; filename=%s' % basename + self.basename = basename or redirect_url.split('/')[-1] + self['Content-Disposition'] = 'attachment; filename=%s' % self.basename self['X-Accel-Redirect'] = redirect_url self['X-Accel-Charset'] = content_type_to_charset(content_type) if with_buffering is not None: @@ -85,8 +85,119 @@ class XAccelRedirectResponse(HttpResponse): or 'off' +class XAccelRedirectValidator(object): + """Utility class to validate XAccelRedirectResponse instances. + + See also :py:func:`assert_x_accel_redirect` shortcut function. + + """ + def __call__(self, test_case, response, **assertions): + """Assert that ``response`` is a valid X-Accel-Redirect response. + + Optional ``assertions`` dictionary can be used to check additional + items: + + * ``basename``: the basename of the file in the response. + + * ``content_type``: the value of "Content-Type" header. + + * ``redirect_url``: the value of "X-Accel-Redirect" header. + + * ``charset``: the value of ``X-Accel-Charset`` header. + + * ``with_buffering``: the value of ``X-Accel-Buffering`` header. + If ``False``, then makes sure that the header disables buffering. + If ``None``, then makes sure that the header is not set. + + * ``expires``: the value of ``X-Accel-Expires`` header. + If ``False``, then makes sure that the header disables expiration. + If ``None``, then makes sure that the header is not set. + + * ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header. + If ``False``, then makes sure that the header disables limit rate. + If ``None``, then makes sure that the header is not set. + + """ + self.assert_x_accel_redirect_response(test_case, response) + for key, value in assertions.iteritems(): + assert_func = getattr(self, 'assert_%s' % key) + assert_func(test_case, response, value) + + def assert_x_accel_redirect_response(self, test_case, response): + test_case.assertTrue(isinstance(response, XAccelRedirectResponse)) + + def assert_basename(self, test_case, response, value): + test_case.assertEqual(response.basename, value) + + def assert_content_type(self, test_case, response, value): + test_case.assertEqual(response['Content-Type'], value) + + def assert_redirect_url(self, test_case, response, value): + test_case.assertEqual(response['X-Accel-Redirect'], value) + + def assert_charset(self, test_case, response, value): + test_case.assertEqual(response['X-Accel-Charset'], value) + + def assert_with_buffering(self, test_case, response, value): + header = 'X-Accel-Buffering' + if value is None: + test_case.assertFalse(header in response) + elif value: + test_case.assertEqual(header, 'yes') + else: + test_case.assertEqual(header, 'no') + + def assert_expires(self, test_case, response, value): + header = 'X-Accel-Expires' + if value is None: + test_case.assertFalse(header in response) + elif not value: + test_case.assertEqual(header, 'off') + else: + test_case.assertEqual(header, value) + + def assert_limit_rate(self, test_case, response, value): + header = 'X-Accel-Limit-Rate' + if value is None: + test_case.assertFalse(header in response) + elif not value: + test_case.assertEqual(header, 'off') + else: + test_case.assertEqual(header, value) + + +def assert_x_accel_redirect(test_case, response, **assertions): + """Make ``test_case`` assert that ``response`` is a XAccelRedirectResponse. + + Optional ``assertions`` dictionary can be used to check additional items: + + * ``basename``: the basename of the file in the response. + + * ``content_type``: the value of "Content-Type" header. + + * ``redirect_url``: the value of "X-Accel-Redirect" header. + + * ``charset``: the value of ``X-Accel-Charset`` header. + + * ``with_buffering``: the value of ``X-Accel-Buffering`` header. + If ``False``, then makes sure that the header disables buffering. + If ``None``, then makes sure that the header is not set. + + * ``expires``: the value of ``X-Accel-Expires`` header. + If ``False``, then makes sure that the header disables expiration. + If ``None``, then makes sure that the header is not set. + + * ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header. + If ``False``, then makes sure that the header disables limit rate. + If ``None``, then makes sure that the header is not set. + + """ + validator = XAccelRedirectValidator() + return validator(test_case, response, **assertions) + + class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware): - """Looks like a middleware, but it is configurable. + """Configurable middleware, for use in decorators or in global middlewares. Standard Django middlewares are configured globally via settings. Instances of this class are to be configured individually. It makes it possible to diff --git a/django_downloadview/test.py b/django_downloadview/test.py new file mode 100644 index 0000000..fa9b2f0 --- /dev/null +++ b/django_downloadview/test.py @@ -0,0 +1,42 @@ +"""Testing utilities.""" +import shutil +import tempfile + +from django.conf import settings +from django.test.utils import override_settings + + +class temporary_media_root(override_settings): + """Context manager or decorator to override settings.MEDIA_ROOT. + + >>> from django_downloadview.test import temporary_media_root + >>> from django.conf import settings + >>> global_media_root = settings.MEDIA_ROOT + >>> with temporary_media_root(): + ... global_media_root == settings.MEDIA_ROOT + False + >>> global_media_root == settings.MEDIA_ROOT + True + + >>> @temporary_media_root() + ... def use_temporary_media_root(): + ... return settings.MEDIA_ROOT + >>> tmp_media_root = use_temporary_media_root() + >>> global_media_root == tmp_media_root + False + >>> global_media_root == settings.MEDIA_ROOT + True + + """ + def enable(self): + """Create a temporary directory and use it to override + settings.MEDIA_ROOT.""" + tmp_dir = tempfile.mkdtemp() + self.options['MEDIA_ROOT'] = tmp_dir + super(temporary_media_root, self).enable() + + def disable(self): + """Remove directory settings.MEDIA_ROOT then restore original + setting.""" + shutil.rmtree(settings.MEDIA_ROOT) + super(temporary_media_root, self).disable() diff --git a/docs/api/django_downloadview.txt b/docs/api/django_downloadview.txt index c790b25..b919a52 100644 --- a/docs/api/django_downloadview.txt +++ b/docs/api/django_downloadview.txt @@ -41,6 +41,14 @@ django_downloadview Package :undoc-members: :show-inheritance: +:mod:`test` Module +------------------ + +.. automodule:: django_downloadview.test + :members: + :undoc-members: + :show-inheritance: + :mod:`utils` Module ------------------- diff --git a/docs/dev.txt b/docs/dev.txt index 96d21bf..9657a73 100644 --- a/docs/dev.txt +++ b/docs/dev.txt @@ -95,6 +95,14 @@ Test and build Use `the Makefile`_. +********************* +Demo project included +********************* + +The :doc:`/demo` is part of the tests. Maintain it along with code and +documentation. + + ********** References ********** diff --git a/docs/optimizations/nginx.txt b/docs/optimizations/nginx.txt index 4971b52..a9eb83d 100644 --- a/docs/optimizations/nginx.txt +++ b/docs/optimizations/nginx.txt @@ -15,70 +15,104 @@ See `Nginx X-accel documentation`_ for details. Configure some download view **************************** -As an example, let's consider an application called "myapp". +Let's start in the situation described in the :doc:`demo application `: -:file:`settings.py`: +* a project "demoproject" +* an application "demoproject.download" +* a :py:class:`django_downloadview.views.ObjectDownloadView` view serves files + of a "Document" model. + +We are to make it more efficient with Nginx. + +.. note:: + + Examples below are taken from the :doc:`demo project `. + + +*********** +Write tests +*********** + +Use :py:func:`django_downloadview.nginx.assert_x_accel_redirect` function as +a shortcut in your tests. + +:file:`demo/demoproject/nginx/tests.py`: + +.. literalinclude:: ../../demo/demoproject/nginx/tests.py + :language: python + :emphasize-lines: 5, 25-34 + +Right now, this test should fail, since you haven't implemented the view yet. + + +************ +Setup Django +************ + +At the end of this setup, the test should pass, but you still have to `setup +Nginx`_! + +You have two options: global setup with a middleware, or per-view setup with +decorators. + +Global delegation, with XAccelRedirectMiddleware +================================================ + +If you want to delegate all file downloads to Nginx, then use +:py:class:`django_downloadview.nginx.XAccelRedirectMiddleware`. + +Register it in your settings: .. code-block:: python - INSTALLED_APPS = ( + MIDDLEWARE_CLASSES = ( # ... - 'myapp', + 'django_downloadview.nginx.XAccelRedirectMiddleware', # ... ) - MYAPP_STORAGE_LOCATION = '/var/www/files/' # Could be MEDIA_ROOT for public - # files. -This application holds a ``Document`` model. - -:file:`myapp/models.py`: +Setup the middleware: .. code-block:: python - from django.conf import settings - from django.core.files.storage import FileSystemStorage - from django.db import models + NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT # Could be elsewhere. + NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = '/proxied-download' - - storage = FileSystemStorage(location=settings.MYAPP_STORAGE_LOCATION) - - - class Document(models.Model): - file = models.FileField(storage=storage, upload_to='document') - -Notice the ``storage`` and ``upload_to`` parameters: files for ``Document`` -model live in :file:`/var/www/files/document/` folder. - -Then we configured a download view for this model, restricted to authenticated -users: - -:file:`myapp/urls.py`: +Optionally fine-tune the middleware. Default values are ``None``, which means +"use Nginx's defaults". .. code-block:: python - from django.conf.urls import url, url_patterns - from django.contrib.auth.decorators import login_required + NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES = False # Force no expiration. + NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING = False # Force buffering off. + NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE = False # Force limit rate off. - from django_downloadview import ObjectDownloadView +Local delegation, with x_accel_redirect decorator +================================================= - from myapp.models import Document +If you want to delegate file downloads to Nginx on a per-view basis, then use +:py:func:`django_downloadview.nginx.x_accel_redirect` decorator. + +:file:`demo/demoproject/nginx/views.py`: + +.. literalinclude:: ../../demo/demoproject/nginx/views.py + :language: python + +And use it in som URL conf, as an example in +:file:`demo/demoproject/nginx/urls.py`: + +.. literalinclude:: ../../demo/demoproject/nginx/urls.py + :language: python + +.. note:: + + In real life, you'd certainly want to replace the "download_document" view + instead of registering a new view. - download = login_required(ObjectDownloadView.as_view(model=Document)) - - url_patterns = ('', - url('^document/(?P[0-9]+/download/$', download, name='download'), - ) - -As is, Django is to serve the files, i.e. load chunks into memory and stream -them. - -Nginx is much more efficient for the actual streaming... Let's use it! - - -*************** -Configure Nginx -*************** +*********** +Setup Nginx +*********** See `Nginx X-accel documentation`_ for details. @@ -103,7 +137,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`: # # See http://wiki.nginx.org/X-accel # and https://github.com/benoitbryon/django-downloadview - location /optimized-download { + location /proxied-download { internal; # Location to files on disk. # See Django's settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT @@ -124,7 +158,7 @@ section. .. note:: - ``/optimized-download`` is not available for the client, i.e. users + ``/proxied-download`` is not available for the client, i.e. users won't be able to download files via ``/optimized-download/``. .. warning:: @@ -132,70 +166,6 @@ section. Make sure Nginx can read the files to download! Check permissions. -************************************************ -Global delegation, with XAccelRedirectMiddleware -************************************************ - -If you want to delegate all file downloads to Nginx, then use -:py:class:`django_downloadview.nginx.XAccelRedirectMiddleware`. - -Register it in your settings: - -.. code-block:: python - - MIDDLEWARE_CLASSES = ( - # ... - 'django_downloadview.nginx.XAccelRedirectMiddleware', - # ... - ) - -Setup the middleware: - -.. code-block:: python - - NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MYAPP_STORAGE_LOCATION - NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = '/optimized-download' - -Optionally fine-tune the middleware. Default values are ``None``, which means -"use Nginx's defaults". - -.. code-block:: python - - NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES = False # Force no expiration. - NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING = False # Force buffering off. - NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE = False # Force limit rate off. - - -************************************************* -Local delegation, with x_accel_redirect decorator -************************************************* - -If you want to delegate file downloads to Nginx on a per-view basis, then use -:py:func:`django_downloadview.nginx.x_accel_redirect` decorator. - -Adapt :file:`myapp/urls.py`: - -.. code-block:: diff - - from django.conf.urls import url, url_patterns - from django.contrib.auth.decorators import login_required - - from django_downloadview import ObjectDownloadView - + from django_downloadview.nginx import x_accel_redirect - - from myapp.models import Document - - - download = login_required(ObjectDownloadView.as_view(model=Document)) - + download = x_accel_redirect(download, - + media_root=settings.MY_APP_STORAGE_LOCATION, - + media_url='/optimized-download') - - url_patterns = ('', - url('^document/(?P[0-9]+/download/$', download, name='download'), - ) - - ************* Common issues *************