Merge pull request #17 from novagile/16-assert-response

Closes #16 - Introduced nginx.assert_x_accel_redirect() utility function
This commit is contained in:
Benoît Bryon 2012-12-10 02:07:15 -08:00
commit dd687d148b
18 changed files with 405 additions and 324 deletions

2
README
View file

@ -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.

View file

@ -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
**********

View file

@ -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')

View file

@ -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')

View file

@ -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<slug>[a-zA-Z0-9_-]+)/$', 'download_document',
name='download_document'),
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
'download_document_nginx', name='download_document_nginx'),
)

View file

@ -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')

View file

@ -0,0 +1 @@
"""Nginx optimizations applied to demoproject.download."""

View file

View file

@ -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')

View file

@ -0,0 +1,8 @@
"""URL mapping."""
from django.conf.urls import patterns, include, url
urlpatterns = patterns('demoproject.nginx.views',
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
'download_document_nginx', name='download_document_nginx'),
)

View file

@ -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')

View file

@ -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',

View file

@ -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')
)

View file

@ -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

View file

@ -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()

View file

@ -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
-------------------

View file

@ -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
**********

View file

@ -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 </demo>`:
: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 </demo>`.
***********
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<pk>[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/<filename>``.
.. 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<pk>[0-9]+/download/$', download, name='download'),
)
*************
Common issues
*************