diff --git a/AUTHORS b/AUTHORS index ba1a119..db40ae7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,4 +2,9 @@ Authors & contributors ###################### -* Benoit Bryon +Original code by `Novapost `_ team: + +* Nicolas Tobo +* Lauréline Guérin +* Gregory Tappero +* Benoît Bryon diff --git a/CHANGELOG b/CHANGELOG index e69de29..8141863 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -0,0 +1,9 @@ +Changelog +========= + +1.0 (unreleased) +---------------- + +- Introduced optimizations for Nginx X-Accel: a middleware and a decorator +- Introduced generic views: DownloadView and ObjectDownloadView +- Initialized project diff --git a/INSTALL b/INSTALL index e69de29..f9aa018 100644 --- a/INSTALL +++ b/INSTALL @@ -0,0 +1,25 @@ +############ +Installation +############ + +This project is open-source, published under BSD license. +See :doc:`/about/license` for details. + +If you want to install a development environment, you should go to :doc:`/dev` +documentation. + +Install the package with your favorite Python installer. As an example, with +pip: + +.. code-block:: sh + + pip install django-downloadview + +There is no need to register this application in your Django's +``INSTALLED_APPS`` setting. + +Next, you'll have to setup some download view(s). See :doc:`demo project +` for examples, and :doc:`API documentation `. + +Optionally, you may setup additional :doc:`server optimizations +`. diff --git a/MANIFEST.in b/MANIFEST.in index 094d702..c14ef6b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ recursive-include django_downloadview * global-exclude *.pyc .*.swp include *.txt -include *.rst include AUTHORS CHANGELOG INSTALL LICENSE README VERSION diff --git a/Makefile b/Makefile index 4d4052c..4b805ed 100644 --- a/Makefile +++ b/Makefile @@ -4,37 +4,29 @@ SHELL = /bin/bash PROJECT = 'django-downloadview' ROOT_DIR = $(shell pwd) DATA_DIR = $(ROOT_DIR)/var -VIRTUALENV = virtualenv -VIRTUALENV_DIR = $(ROOT_DIR)/lib/virtualenv -PIP = $(VIRTUALENV_DIR)/bin/pip +WGET = wget +PYTHON = python +BUILDOUT_BOOTSTRAP_URL = https://raw.github.com/buildout/buildout/1.6.3/bootstrap/bootstrap.py +BUILDOUT_BOOTSTRAP = $(ROOT_DIR)/lib/buildout/bootstrap.py BUILDOUT = $(ROOT_DIR)/bin/buildout BUILDOUT_ARGS = -N -virtualenv: - if [ ! -x $(PIP) ]; then \ - if [[ "`$(VIRTUALENV) --version`" < "`echo '1.8'`" ]]; then \ - $(VIRTUALENV) --no-site-packages --distribute $(VIRTUALENV_DIR); \ - else \ - $(VIRTUALENV) $(VIRTUALENV_DIR); \ - fi; \ - $(PIP) install -U pip; \ - fi - - buildout: - # Install zc.buildout. - if [ ! -x $(BUILDOUT) ]; then \ + # Download zc.buildout bootstrap. + if [ ! -f $(BUILDOUT_BOOTSTRAP) ]; then \ mkdir -p $(ROOT_DIR)/lib/buildout; \ - $(PIP) install zc.buildout; \ - mkdir -p `dirname $(BUILDOUT)`; \ - ln -s $(VIRTUALENV_DIR)/bin/buildout $(BUILDOUT); \ + $(WGET) $(BUILDOUT_BOOTSTRAP_URL) -O $(BUILDOUT_BOOTSTRAP); \ + fi + # Bootstrap buildout. + if [ ! -f $(BUILDOUT) ]; then \ + $(PYTHON) $(BUILDOUT_BOOTSTRAP) --distribute; \ fi # Run zc.buildout. $(BUILDOUT) $(BUILDOUT_ARGS) -develop: virtualenv buildout +develop: buildout update: develop @@ -43,11 +35,11 @@ update: develop clean: find $(ROOT_DIR)/ -name "*.pyc" -delete find $(ROOT_DIR)/ -name ".noseids" -delete - rm nosetests.xml distclean: clean rm -rf $(ROOT_DIR)/*.egg-info + rm -rf $(ROOT_DIR)/demo/*.egg-info maintainer-clean: distclean @@ -60,7 +52,9 @@ test: apidoc: + cp docs/api/index.txt docs/api-backup.txt rm -rf docs/api/* + mv docs/api-backup.txt docs/api/index.txt bin/sphinx-apidoc --suffix txt --output-dir $(ROOT_DIR)/docs/api django_downloadview @@ -71,5 +65,12 @@ sphinx: documentation: apidoc sphinx +runserver: + bin/demo runserver + + +demo: develop runserver + + release: bin/fullrelease diff --git a/README b/README index 4a52468..39eade4 100644 --- a/README +++ b/README @@ -4,11 +4,6 @@ Django-DownloadView Django-DownloadView provides (class-based) generic download views for Django. -.. warning:: - - This project is experimental. It may be renamed or modified without - notices. - Example, in some urls.py: .. code-block:: python diff --git a/VERSION b/VERSION index 49d5957..d822536 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1 +1.0dev diff --git a/buildout.cfg b/buildout.cfg index e487687..950eee4 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -19,17 +19,16 @@ parts = django-downloadview directories releaser -eggs = + +[django-downloadview] +recipe = z3c.recipe.scripts +eggs = django-downloadview-demo bpython nose rednose coverage sphinx - -[django-downloadview] -recipe = z3c.recipe.scripts -eggs = ${buildout:eggs} initialization = import os os.environ['DJANGO_SETTINGS_MODULE'] = 'demoproject.settings' @@ -46,7 +45,7 @@ recipe = z3c.recipe.scripts eggs = zest.releaser [versions] -Django = 1.4.1 +Django = 1.4.2 Jinja2 = 2.6 Sphinx = 1.1.3 bpython = 0.11 diff --git a/demo/README b/demo/README index 589b43e..11f42a2 100644 --- a/demo/README +++ b/demo/README @@ -1,5 +1,60 @@ -############################ -Demo for Django-DownloadView -############################ +############ +Demo project +############ -This is a demo project to illustrate (and test) Django-DownloadView usage. +The :file:`demo/` folder holds a demo project to illustrate (and test) +django-downloadview usage. + + +*********************** +Browse demo code online +*********************** + +See `demo folder in project's repository`_. + + +*************** +Deploy the demo +*************** + +System requirements: + +* `Python`_ version 2.6 or 2.7, available as ``python`` command. + + .. note:: + + You may use `Virtualenv`_ to make sure the active ``python`` is the right + one. + +* ``make`` and ``wget`` to use the provided :file:`Makefile`. + +Execute: + +.. code-block:: sh + + git clone git@github.com:benoitbryon/django-downloadview.git + cd django-downloadview/ + make demo + +It installs and runs the demo server on localhost, port 8000. So have a look +at http://localhost:8000/ + +.. note:: + + If you cannot execute the Makefile, read it and adapt the few commands it + contains to your needs. + +Browse and use :file:`demo/demoproject/` as a sandbox. + + +********** +References +********** + +.. target-notes:: + +.. _`demo folder in project's repository`: + https://github.com/benoitbryon/django-downloadview/tree/master/demo/demoproject/ + +.. _`Python`: http://python.org +.. _`Virtualenv`: http://virtualenv.org diff --git a/django_downloadview/__init__.py b/django_downloadview/__init__.py index 5702055..b09a8b1 100644 --- a/django_downloadview/__init__.py +++ b/django_downloadview/__init__.py @@ -2,5 +2,8 @@ from django_downloadview.views import DownloadView, ObjectDownloadView -#: Implement :pep:`396` -__version__ = '0.1' +pkg_resources = __import__('pkg_resources') +distribution = pkg_resources.get_distribution('django-downloadview') + +#: Module version, as defined in PEP-0396. +__version__ = distribution.version diff --git a/django_downloadview/decorators.py b/django_downloadview/decorators.py index 3e93ec3..9d1a1bc 100644 --- a/django_downloadview/decorators.py +++ b/django_downloadview/decorators.py @@ -1,4 +1,9 @@ -"""View decorators.""" +"""View decorators. + +See also decorators provided by server-specific modules, such as +:py:func:`django_downloadview.nginx.x_accel_redirect`. + +""" class DownloadDecorator(object): diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index 31b983f..bfe1210 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -3,17 +3,24 @@ from django_downloadview.response import is_download_response class BaseDownloadMiddleware(object): - """Base (abstract) Django middleware that process download responses. + """Base (abstract) Django middleware that handles download responses. - Subclasses **must** implement ``process_download_response`` method. + Subclasses **must** implement :py:meth:`process_download_response` method. """ def is_download_response(self, response): - """Return True if ``response`` can be considered as a file download.""" + """Return True if ``response`` can be considered as a file download. + + By default, this method uses + :py:func:`django_downloadview.response.is_download_response`. + Override this method if you want a different behaviour. + + """ return is_download_response(response) def process_response(self, request, response): - """Call ``process_download_response()`` if ``response`` is download.""" + """Call :py:meth:`process_download_response` if ``response`` is + download.""" if self.is_download_response(response): return self.process_download_response(request, response) return response diff --git a/django_downloadview/nginx.py b/django_downloadview/nginx.py index 8f69011..37342b3 100644 --- a/django_downloadview/nginx.py +++ b/django_downloadview/nginx.py @@ -1,6 +1,8 @@ -"""Let Nginx serve files for increased performance. +"""Optimizations for Nginx. -See `Nginx X-accel documentation `_. +See also `Nginx X-accel documentation `_ and +:doc:`narrative documentation about Nginx optimizations +`. """ from datetime import datetime, timedelta @@ -15,6 +17,16 @@ from django_downloadview.utils import content_type_to_charset #: Default value for X-Accel-Buffering header. +#: Also default value for +#: ``settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``. +#: +#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Buffering +#: +#: Default value is None, which means "let Nginx choose", i.e. use Nginx +#: defaults or specific configuration. +#: +#: If set to ``False``, Nginx buffering is disabled. +#: If set to ``True``, Nginx buffering is enabled. DEFAULT_WITH_BUFFERING = None if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'): setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING', @@ -22,19 +34,37 @@ if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'): #: Default value for X-Accel-Limit-Rate header. +#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``. +#: +#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Rate +#: +#: Default value is None, which means "let Nginx choose", i.e. use Nginx +#: defaults or specific configuration. +#: +#: If set to ``False``, Nginx limit rate is disabled. +#: Else, it indicates the limit rate in bytes. DEFAULT_LIMIT_RATE = None if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'): setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE', DEFAULT_LIMIT_RATE) -#: Default value for X-Accel-Limit-Rate header. +#: Default value for X-Accel-Limit-Expires header. +#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``. +#: +#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Expires +#: +#: Default value is None, which means "let Nginx choose", i.e. use Nginx +#: defaults or specific configuration. +#: +#: If set to ``False``, Nginx buffering is disabled. +#: Else, it indicates the expiration delay, in seconds. DEFAULT_EXPIRES = None if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'): setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES', DEFAULT_EXPIRES) class XAccelRedirectResponse(HttpResponse): - """Http response that delegate serving file to Nginx.""" + """Http response that delegates serving file to Nginx.""" def __init__(self, redirect_url, content_type, basename=None, expires=None, with_buffering=None, limit_rate=None): """Return a HttpResponse with headers for Nginx X-Accel-Redirect.""" @@ -56,7 +86,14 @@ class XAccelRedirectResponse(HttpResponse): class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware): - """Looks like a middleware, but configurable.""" + """Looks like a middleware, but it is configurable. + + Standard Django middlewares are configured globally via settings. Instances + of this class are to be configured individually. It makes it possible to + use this class as the factory in + :py:class:`django_downloadview.decorators.DownloadDecorator`. + + """ def __init__(self, media_root, media_url, expires=None, with_buffering=None, limit_rate=None): """Constructor.""" @@ -92,11 +129,7 @@ class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware): class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware): - """Apply X-Accel-Redirect globally. - - XAccelRedirectResponseHandler with django settings. - - """ + """Apply X-Accel-Redirect globally, via Django settings.""" def __init__(self): """Use Django settings as configuration.""" try: @@ -122,6 +155,6 @@ class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware): #: Apply BaseXAccelRedirectMiddleware to ``view_func`` response. #: #: Proxies additional arguments (``*args``, ``**kwargs``) to -#: :py:meth:`django_downloadview.nginx.BaseXAccelRedirectMiddleware.__init__`: -#: ``expires``, ``with_buffering``, and ``limit_rate``. +#: :py:class:`BaseXAccelRedirectMiddleware` constructor (``expires``, +#: ``with_buffering``, and ``limit_rate``). x_accel_redirect = DownloadDecorator(BaseXAccelRedirectMiddleware) diff --git a/django_downloadview/response.py b/django_downloadview/response.py index 565a478..eac6927 100644 --- a/django_downloadview/response.py +++ b/django_downloadview/response.py @@ -3,7 +3,12 @@ from django.http import HttpResponse class DownloadResponse(HttpResponse): - """File download response.""" + """File download response. + + ``content`` attribute is supposed to be a file object wrapper, which makes + this response "lazy". + + """ def __init__(self, content, content_type, content_length, basename, status=200, content_encoding=None, expires=None, filename=None, url=None): @@ -68,7 +73,7 @@ def is_download_response(response): """Return ``True`` if ``response`` is a download response. Current implementation returns True if ``response`` is an instance of - :py:class:`django_downloadview.DownloadResponse`. + :py:class:`django_downloadview.response.DownloadResponse`. """ return isinstance(response, DownloadResponse) diff --git a/docs/about/alternatives.txt b/docs/about/alternatives.txt new file mode 100644 index 0000000..24eba82 --- /dev/null +++ b/docs/about/alternatives.txt @@ -0,0 +1,33 @@ +################################# +Alternatives and related projects +################################# + +This document presents other projects that provide similar or complementary +functionalities. It focuses on differences with django-downloadview. + + +************************* +Django's static file view +************************* + +`Django has a builtin static file view`_. It can stream files. As explained in +Django documentation, it is designed for development purposes. For production, +static files'd better be served by some optimized server. + +Django-downloadview can replace Django's builtin static file view: + +* perform actions with Django when receiving download requests: check + permissions, generate files, gzip, logging, signals... +* delegate actual download to a reverse proxy for increased performance. +* disable optimization middlewares or decorators in development, if you want to + serve files with Django. + + +********** +References +********** + +.. target-notes:: + +.. _`Django has a builtin static file view`: + https://docs.djangoproject.com/en/1.4/ref/contrib/staticfiles/#static-file-development-view diff --git a/docs/about/authors.txt b/docs/about/authors.txt new file mode 100644 index 0000000..5078189 --- /dev/null +++ b/docs/about/authors.txt @@ -0,0 +1 @@ +.. include:: ../../AUTHORS diff --git a/docs/about/changelog.txt b/docs/about/changelog.txt new file mode 100644 index 0000000..e272c5a --- /dev/null +++ b/docs/about/changelog.txt @@ -0,0 +1 @@ +.. include:: ../../CHANGELOG diff --git a/docs/about/index.txt b/docs/about/index.txt new file mode 100644 index 0000000..cee816a --- /dev/null +++ b/docs/about/index.txt @@ -0,0 +1,10 @@ +######################### +About django-downloadview +######################### + +.. toctree:: + + alternatives + license + authors + changelog diff --git a/docs/about/license.txt b/docs/about/license.txt new file mode 100644 index 0000000..bced555 --- /dev/null +++ b/docs/about/license.txt @@ -0,0 +1 @@ +.. include:: ../../LICENSE diff --git a/docs/api/index.txt b/docs/api/index.txt new file mode 100644 index 0000000..e030b47 --- /dev/null +++ b/docs/api/index.txt @@ -0,0 +1,9 @@ +### +API +### + +Here is API documentation, generated from code. + +.. toctree:: + + modules diff --git a/docs/demo.txt b/docs/demo.txt new file mode 100644 index 0000000..e5dd96e --- /dev/null +++ b/docs/demo.txt @@ -0,0 +1 @@ +.. include:: ../demo/README diff --git a/docs/dev.txt b/docs/dev.txt new file mode 100644 index 0000000..96d21bf --- /dev/null +++ b/docs/dev.txt @@ -0,0 +1,111 @@ +########################### +Contributing to the project +########################### + +This document provides guidelines for people who want to contribute to the +project. + + +************** +Create tickets +************** + +Please use the `bugtracker`_ **before** starting some work: + +* check if the bug or feature request has already been filed. It may have been + answered too! + +* else create a new ticket. + +* if you plan to contribute, tell us, so that we are given an opportunity to + give feedback as soon as possible. + +* Then, in your commit messages, reference the ticket with some + ``refs #TICKET-ID`` syntax. + + +*************** +Fork and branch +*************** + +* Work in forks and branches. + +* Prefix your branch with the ticket ID corresponding to the issue. As an + example, if you are working on ticket #23 which is about contribute + documentation, name your branch like ``23-contribute-doc``. + +* If you work in a development branch and want to refresh it with changes from + master, please `rebase`_ or `merge-based rebase`_, i.e. don't merge master. + + +******************************* +Setup a development environment +******************************* + +System requirements: + +* `Python`_ version 2.6 or 2.7, available as ``python`` command. + + .. note:: + + You may use `Virtualenv`_ to make sure the active ``python`` is the right + one. + +* make and wget to use the provided :file:`Makefile`. + +Execute: + +.. code-block:: sh + + git clone git@github.com:benoitbryon/django-downloadview.git + cd django-downloadview/ + make develop + +If you cannot execute the Makefile, read it and adapt the few commands it +contains to your needs. + + +************ +The Makefile +************ + +A :file:`Makefile` is provided to ease development. Use it to: + +* setup the development environment: ``make develop`` +* update it, as an example, after a pull: ``make update`` +* run tests: ``make test`` +* build documentation: ``make documentation`` + +The :file:`Makefile` is intended to be a live reference for the development +environment. + + +************* +Documentation +************* + +Follow `style guide for Sphinx-based documentations`_ when editing the +documentation. + + +************** +Test and build +************** + +Use `the Makefile`_. + + +********** +References +********** + +.. target-notes:: + +.. _`bugtracker`: + https://github.com/benoitbryon/django-downloadview/issues +.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing +.. _`merge-based rebase`: http://tech.novapost.fr/psycho-rebasing-en.html +.. _`Python`: http://python.org +.. _`Virtualenv`: http://virtualenv.org +.. _`style guide for Sphinx-based documentations`: + http://documentation-style-guide-sphinx.readthedocs.org/ diff --git a/docs/index.txt b/docs/index.txt index 9fa5743..e987813 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,8 +1,3 @@ -.. django-downloadview documentation master file, created by - sphinx-quickstart on Mon Aug 27 11:37:23 2012. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - .. include:: ../README @@ -13,8 +8,12 @@ Contents .. toctree:: :maxdepth: 2 - nginx - api/modules + demo + install + optimizations/index + api/index + about/index + dev ****************** @@ -24,4 +23,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/install.txt b/docs/install.txt new file mode 100644 index 0000000..71955cc --- /dev/null +++ b/docs/install.txt @@ -0,0 +1 @@ +.. include:: ../INSTALL diff --git a/docs/optimizations/index.txt b/docs/optimizations/index.txt new file mode 100644 index 0000000..4d1d4ac --- /dev/null +++ b/docs/optimizations/index.txt @@ -0,0 +1,51 @@ +############# +Optimizations +############# + +Some reverse proxies allow applications to delegate actual download to the +proxy: + +* with Django, manage permissions, generate files... +* let the reverse proxy serve the file. + +As a result, you get increased performance: reverse proxies are more efficient +than Django at serving static files. + +.. toctree:: + + nginx + +Currently, only `nginx's X-Accel`_ is supported, but `contributions are +welcome`_! + + +***************** +How does it work? +***************** + +The feature is inspired by `Django's TemplateResponse`_: the download views +return some :py:class:`django_downloadview.response.DownloadResponse` instance. +Such a response doesn't contain file data. + +By default, at the end of Django's request/response handling, Django is to +iterate over the ``content`` attribute of the response. In a +``DownloadResponse``, this ``content`` attribute is a file wrapper. + +It means that decorators and middlewares are given an opportunity to capture +the ``DownloadResponse`` before the content of the file is loaded into memory +As an example, :py:class:`django_downloadview.nginx.XAccelRedirectMiddleware` +replaces ``DownloadResponse`` intance by some +:py:class:`django_downloadview.nginx.XAccelRedirectResponse`. + + +********** +References +********** + +.. target-notes:: + +.. _`nginx's X-Accel`: http://wiki.nginx.org/X-accel +.. _`contributions are welcome`: + https://github.com/benoitbryon/django-downloadview/issues?labels=optimizations +.. _`Django's TemplateResponse`: + https://docs.djangoproject.com/en/1.4/ref/template-response/ diff --git a/docs/nginx.txt b/docs/optimizations/nginx.txt similarity index 66% rename from docs/nginx.txt rename to docs/optimizations/nginx.txt index 6020f60..4971b52 100644 --- a/docs/nginx.txt +++ b/docs/optimizations/nginx.txt @@ -1,6 +1,6 @@ -################### -Nginx optimisations -################### +##### +Nginx +##### If you serve Django behind Nginx, then you can delegate the file download service to Nginx and get increased performance: @@ -15,19 +15,65 @@ See `Nginx X-accel documentation`_ for details. Configure some download view **************************** -As an example, let's consider the following download view: +As an example, let's consider an application called "myapp". -* mapped on ``/document//download`` -* returns DownloadResponse corresponding to Document's model FileField -* Document storage root is :file:`/var/www/files/` -* FileField's ``upload_to`` is "document". +:file:`settings.py`: -Files live in ``/var/www/files/document/`` folder. +.. code-block:: python + + INSTALLED_APPS = ( + # ... + 'myapp', + # ... + ) + MYAPP_STORAGE_LOCATION = '/var/www/files/' # Could be MEDIA_ROOT for public + # files. + +This application holds a ``Document`` model. + +:file:`myapp/models.py`: + +.. code-block:: python + + from django.conf import settings + from django.core.files.storage import FileSystemStorage + from django.db import models + + + 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`: + +.. code-block:: python + + from django.conf.urls import url, url_patterns + from django.contrib.auth.decorators import login_required + + from django_downloadview import ObjectDownloadView + + from myapp.models import Document + + + 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. +Nginx is much more efficient for the actual streaming... Let's use it! *************** @@ -36,17 +82,46 @@ Configure Nginx See `Nginx X-accel documentation`_ for details. -In this documentation, let's suppose we have something like this: +Here is what you could have in :file:`/etc/nginx/sites-available/default`: .. code-block:: nginx - # Will serve /var/www/files/myfile.tar.gz - # When passed URI /protected_files/myfile.tar.gz - location /optimized-download { - internal; - alias /var/www/files; + charset utf-8; + + # Django-powered service. + upstream frontend { + server 127.0.0.1:8000 fail_timeout=0; } + server { + listen 80 default; + + # File-download proxy. + # + # Will serve /var/www/files/myfile.tar.gz when passed URI + # like /optimized-download/myfile.tar.gz + # + # See http://wiki.nginx.org/X-accel + # and https://github.com/benoitbryon/django-downloadview + location /optimized-download { + internal; + # Location to files on disk. + # See Django's settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT + alias /var/www/files/; + } + + # Proxy to Django-powered frontend. + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://frontend; + } + } + +... where specific configuration is the ``location /optimized-download`` +section. + .. note:: ``/optimized-download`` is not available for the client, i.e. users @@ -74,12 +149,18 @@ Register it in your settings: # ... ) -Optionally customize configuration (default is "use Nginx's defaults"). +Setup the middleware: .. code-block:: python - NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT + 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. @@ -92,128 +173,26 @@ 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. -In some urls.py: +Adapt :file:`myapp/urls.py`: -.. code-block:: python - - # ... import Document and django.core.urls - - from django_downloadview import ObjectDownloadView - from django_downloadview.nginx import x_accel_redirect - - - download = x_accel_redirect(ObjectDownloadView.as_view(model=Document), - media_root=settings.MEDIA_ROOT, - media_url='/optimized-download') - - # ... URL patterns using ``download`` - - -******************** -Sample configuration -******************** - -In this sample configuration... - -* we register files in some "myapp.models.Document" model -* store files in :file:`/var/www/private/` folder -* publish files at ``/download//`` URL -* restrict access to authenticated users with the ``login_required`` decorator -* delegate file download to Nginx, via ``/private/`` internal URL. - -Nginx -===== - -:file:`/etc/nginx/sites-available/default`: - -.. code-block:: nginx - - charset utf-8; - - # Django-powered service. - upstream frontend { - server 127.0.0.1:8000 fail_timeout=0; - } - - server { - listen 80 default; - - # File-download proxy. - # See http://wiki.nginx.org/X-accel - # and https://github.com/benoitbryon/django-downloadview - location /private/ { - internal; - # Location to files on disk. - # See Django's settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT - alias /var/www/private/; - } - - # Proxy to Django-powered frontend. - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_pass http://frontend; - } - } - -Django settings -=============== - -:file:`settings.py`: - -.. code-block:: python - - MYAPP_STORAGE_LOCATION = '/var/www/private/' - NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MYAPP_STORAGE_LOCATION - MIDDLEWARE_CLASSES = ( - # ... - 'django_downloadview.nginx.XAccelRedirectMiddleware', - # ... - ) - INSTALLED_APPS = ( - # ... - 'myapp', - # ... - ) - -In some model -============= - -:file:`myapp/models.py`: - -.. code-block:: python - - from django.conf import settings - from django.db import models - from django.core.files.storage import FileSystemStorage - - - storage = FileSystemStorage(location=settings.MYAPP_STORAGE_LOCATION) - - - class Document(models.Model): - file = models.ImageField(storage=storage) - -URL patterns -============ - -:file:`myapp/urls.py`: - -.. code-block:: python +.. 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('^download/(?P[0-9]+/$', download, name='download'), + url('^document/(?P[0-9]+/download/$', download, name='download'), ) diff --git a/setup.py b/setup.py index 6f3c832..1fd4c73 100644 --- a/setup.py +++ b/setup.py @@ -22,13 +22,14 @@ setup(name=NAME, version=VERSION, description='Generic download views for Django.', long_description=README, - classifiers=['Development Status :: 1 - Planning', + classifiers=['Development Status :: 4 - Beta', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.6', 'Framework :: Django', ], - keywords='class-based view, generic view, download', + keywords='class-based view, generic view, download, file, FileField, ' \ + 'ImageField, nginx, x-accel, x-sendfile', author='Benoit Bryon', author_email='benoit@marmelune.net', url='https://github.com/benoitbryon/%s' % NAME,