mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-05-19 13:01:13 +00:00
Merge pull request #55 from benoitbryon/api-readability
Closes #54, closes #53, closes #52, closes #51, closes #50, closes #49, closes #48, closes #42, refs #44, closes #40, closes #39 - Merge api-readability branch which contains almost all 1.3 release.
This commit is contained in:
commit
e900c1a253
113 changed files with 3445 additions and 1716 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,3 +14,4 @@
|
|||
|
||||
# Editors' temporary buffers.
|
||||
.*.swp
|
||||
*~
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
install: make configure develop
|
||||
script: make test
|
||||
|
|
|
|||
4
AUTHORS
4
AUTHORS
|
|
@ -9,5 +9,7 @@ Original code by `Novapost <http://www.novapost.fr>`_ team:
|
|||
* Nicolas Tobo <https://github.com/nicolastobo>
|
||||
* Lauréline Guérin <https://github.com/zebuline>
|
||||
* Gregory Tappero <https://github.com/coulix>
|
||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
||||
* Benoît Bryon <benoit@marmelune.net>
|
||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
||||
|
||||
Developers: https://github.com/benoitbryon/django-downloadview/graphs/contributors
|
||||
|
|
|
|||
72
CHANGELOG
72
CHANGELOG
|
|
@ -1,10 +1,63 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
This document describes changes between past releases. For information about
|
||||
future releases, check `milestones`_ and :doc:`/about/vision`.
|
||||
|
||||
|
||||
1.3 (unreleased)
|
||||
----------------
|
||||
|
||||
- Nothing changed yet.
|
||||
Big refactoring around middleware configuration, API readability and
|
||||
documentation.
|
||||
|
||||
- Bugfix #44 - Introduced ``django_downloadview.File``, which patches
|
||||
``django.core.files.base.File.__iter__()`` implementation.
|
||||
See https://code.djangoproject.com/ticket/21321
|
||||
|
||||
- Bugfix #48 - Fixed ``basename`` assertion in ``assert_download_response``:
|
||||
checks ``Content-Disposition`` header.
|
||||
|
||||
- Bugfix #49 - Fixed ``content`` assertion in ``assert_download_response``:
|
||||
checks only response's ``streaming_content`` attribute.
|
||||
|
||||
- Feature #50 - Introduced
|
||||
:class:`~django_downloadview.middlewares.DownloadDispatcherMiddleware` that
|
||||
iterates over a list of configurable download middlewares. Allows to plug
|
||||
several download middlewares with different configurations.
|
||||
|
||||
Deprecates the following settings related to previous single-and-global
|
||||
middleware:
|
||||
|
||||
* ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT``
|
||||
* ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL``
|
||||
* ``NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``
|
||||
* ``NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``
|
||||
* ``NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``
|
||||
|
||||
- Feature #42 - Documentation shows how to stream generated content (yield).
|
||||
Introduced ``django_downloadview.StringIteratorIO``.
|
||||
|
||||
- Refactoring #51 - Dropped support of Python 2.6
|
||||
|
||||
- Refactoring #52 - ObjectDownloadView now inherits from SingleObjectMixin and
|
||||
BaseDownloadView (was DownloadMixin and BaseDetailView).
|
||||
Simplified DownloadMixin.render_to_response() signature.
|
||||
|
||||
- Refactoring #40 - Documentation includes examples from demo project.
|
||||
|
||||
- Refactoring #39 - Documentation focuses on usage, rather than API. Improved
|
||||
narrative documentation.
|
||||
|
||||
- Refactoring #53 - Added base classes in ``django_downloadview.middlewares``,
|
||||
such as ``ProxiedDownloadMiddleware``.
|
||||
|
||||
|
||||
- Refactoring #54 - Expose most Python API directly in `django_downloadview`
|
||||
package. Simplifies ``import`` statements in client applications.
|
||||
Splitted nginx module in a package.
|
||||
|
||||
- Added unit tests, improved code coverage.
|
||||
|
||||
|
||||
1.2 (2013-05-28)
|
||||
|
|
@ -12,10 +65,14 @@ Changelog
|
|||
|
||||
Bugfixes and documentation improvements.
|
||||
|
||||
- Bug #26 - Prevented computation of virtual file's size, unless the file
|
||||
- Bugfix #26 - Prevented computation of virtual file's size, unless the file
|
||||
wrapper implements was_modified_since() method.
|
||||
- Bug #34 - Improved support of files that do not implement modification time.
|
||||
- Bug #35 - Fixed README conversion from reStructuredText to HTML (PyPI).
|
||||
|
||||
- Bugfix #34 - Improved support of files that do not implement modification
|
||||
time.
|
||||
|
||||
- Bugfix #35 - Fixed README conversion from reStructuredText to HTML (PyPI).
|
||||
|
||||
|
||||
1.1 (2013-04-11)
|
||||
----------------
|
||||
|
|
@ -45,3 +102,10 @@ Contains **backward incompatible changes.**
|
|||
- Introduced optimizations for Nginx X-Accel: a middleware and a decorator
|
||||
- Introduced generic views: DownloadView and ObjectDownloadView
|
||||
- Initialized project
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`milestones`: https://github.com/benoitbryon/django-downloadview/issues/milestones
|
||||
|
|
|
|||
32
INSTALL
32
INSTALL
|
|
@ -1,30 +1,32 @@
|
|||
############
|
||||
Installation
|
||||
############
|
||||
#######
|
||||
Install
|
||||
#######
|
||||
|
||||
This project is open-source, published under BSD license.
|
||||
See :doc:`/about/license` for details.
|
||||
.. note::
|
||||
|
||||
If you want to install a development environment, you should go to :doc:`/dev`
|
||||
documentation.
|
||||
If you want to install a development environment, please see :doc:`/dev`.
|
||||
|
||||
System requirements:
|
||||
|
||||
* Python 2.7
|
||||
|
||||
Install the package with your favorite Python installer. As an example, with
|
||||
pip:
|
||||
|
||||
.. code-block:: sh
|
||||
.. code:: sh
|
||||
|
||||
pip install django-downloadview
|
||||
|
||||
.. note::
|
||||
|
||||
Since version 1.1, django-downloadview requires Django>=1.5, which provides
|
||||
StreamingHttpResponse.
|
||||
:py:class:`~django.http.StreamingHttpResponse`.
|
||||
|
||||
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
|
||||
<demo>` for examples, and :doc:`API documentation <api/django_downloadview>`.
|
||||
.. rubric:: Notes & references
|
||||
|
||||
Optionally, you may setup additional :doc:`server optimizations
|
||||
<optimizations/index>`.
|
||||
.. seealso::
|
||||
|
||||
* :doc:`/settings`
|
||||
* :doc:`/about/changelog`
|
||||
* :doc:`/about/license`
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -2,7 +2,7 @@
|
|||
License
|
||||
#######
|
||||
|
||||
Copyright (c) 2012, Benoît Bryon.
|
||||
Copyright (c) 2012-2013, Benoît Bryon.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
|||
27
Makefile
27
Makefile
|
|
@ -7,6 +7,7 @@ DATA_DIR = $(ROOT_DIR)/var
|
|||
WGET = wget
|
||||
PYTHON = $(shell which python)
|
||||
PROJECT = $(shell $(PYTHON) -c "import setup; print setup.NAME")
|
||||
PACKAGE = $(shell $(PYTHON) -c "import setup; print setup.PACKAGES[0]")
|
||||
BUILDOUT_CFG = $(ROOT_DIR)/etc/buildout.cfg
|
||||
BUILDOUT_DIR = $(ROOT_DIR)/lib/buildout
|
||||
BUILDOUT_VERSION = 1.7.0
|
||||
|
|
@ -51,38 +52,36 @@ test: test-app test-demo test-documentation
|
|||
|
||||
|
||||
test-app:
|
||||
$(NOSE) -c $(ROOT_DIR)/etc/nose.cfg --with-coverage --cover-package=django_downloadview django_downloadview tests
|
||||
$(NOSE) -c $(ROOT_DIR)/etc/nose/base.cfg -c $(ROOT_DIR)/etc/nose/$(PACKAGE).cfg
|
||||
mv $(ROOT_DIR)/.coverage $(ROOT_DIR)/var/test/app.coverage
|
||||
|
||||
|
||||
test-demo:
|
||||
$(BIN_DIR)/demo test demo
|
||||
$(BIN_DIR)/demo test --nose-verbosity=2
|
||||
mv $(ROOT_DIR)/.coverage $(ROOT_DIR)/var/test/demo.coverage
|
||||
|
||||
|
||||
test-documentation:
|
||||
$(NOSE) -c $(ROOT_DIR)/etc/nose.cfg sphinxcontrib.testbuild.tests
|
||||
|
||||
|
||||
apidoc:
|
||||
cp docs/api/index.txt docs/api-backup.txt
|
||||
rm -rf docs/api/*
|
||||
mv docs/api-backup.txt docs/api/index.txt
|
||||
$(BIN_DIR)/sphinx-apidoc --suffix txt --output-dir $(ROOT_DIR)/docs/api django_downloadview
|
||||
$(NOSE) -c $(ROOT_DIR)/etc/nose/base.cfg sphinxcontrib.testbuild.tests
|
||||
|
||||
|
||||
sphinx:
|
||||
make --directory=docs clean html doctest
|
||||
|
||||
|
||||
documentation: apidoc sphinx
|
||||
documentation: sphinx
|
||||
|
||||
|
||||
demo: develop
|
||||
mkdir -p var/media/document
|
||||
$(BIN_DIR)/demo syncdb --noinput
|
||||
cp $(ROOT_DIR)/demo/demoproject/download/fixtures/hello-world.txt var/media/document/
|
||||
$(BIN_DIR)/demo loaddata $(ROOT_DIR)/demo/demoproject/download/fixtures/demo.json
|
||||
# Install fixtures.
|
||||
mkdir -p var/media
|
||||
cp -r $(ROOT_DIR)/demo/demoproject/fixtures var/media/object
|
||||
cp -r $(ROOT_DIR)/demo/demoproject/fixtures var/media/object-other
|
||||
cp -r $(ROOT_DIR)/demo/demoproject/fixtures var/media/nginx
|
||||
$(BIN_DIR)/demo loaddata demo.json
|
||||
|
||||
runserver: demo
|
||||
$(BIN_DIR)/demo runserver
|
||||
|
||||
|
||||
|
|
|
|||
61
README
61
README
|
|
@ -1,61 +0,0 @@
|
|||
###################
|
||||
django-downloadview
|
||||
###################
|
||||
|
||||
Django-DownloadView provides generic views to make Django serve files.
|
||||
|
||||
It can serve files from models, storages, local filesystem, arbitrary URL...
|
||||
and even generated files.
|
||||
|
||||
For increased performances, it can delegate the actual streaming to a reverse
|
||||
proxy, via mechanisms such as Nginx's X-Accel.
|
||||
|
||||
|
||||
*******
|
||||
Example
|
||||
*******
|
||||
|
||||
In some ``urls.py``, serve files managed in a model:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.conf.urls import url, url_patterns
|
||||
from django_downloadview import ObjectDownloadView
|
||||
from demoproject.download.models import Document # A model with a FileField
|
||||
|
||||
# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
|
||||
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
||||
|
||||
url_patterns = ('',
|
||||
url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', download, name='download'),
|
||||
)
|
||||
|
||||
More examples in the "demo" documentation!
|
||||
|
||||
|
||||
*****
|
||||
Views
|
||||
*****
|
||||
|
||||
Several views are provided to cover frequent use cases:
|
||||
|
||||
* ``ObjectDownloadView`` when you have a model with a file field.
|
||||
* ``StorageDownloadView`` when you manage files in a storage.
|
||||
* ``PathDownloadView`` when you have an absolute filename on local filesystem.
|
||||
* ``HTTPDownloadView`` when you have an URL (the resource is proxied).
|
||||
* ``VirtualDownloadView`` when you the file is generated on the fly.
|
||||
|
||||
See "views" documentation for details.
|
||||
|
||||
See also "optimizations" documentation to get increased performances.
|
||||
|
||||
|
||||
**********
|
||||
Ressources
|
||||
**********
|
||||
|
||||
* Documentation: http://django-downloadview.readthedocs.org
|
||||
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
||||
* Code repository: https://github.com/benoitbryon/django-downloadview
|
||||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
||||
47
README.rst
Normal file
47
README.rst
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
###################
|
||||
django-downloadview
|
||||
###################
|
||||
|
||||
``django-downloadview`` makes it easy to serve files with Django:
|
||||
|
||||
* you manage files with Django (permissions, search, generation, ...);
|
||||
|
||||
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||
storage, memory...);
|
||||
|
||||
* ``django-downloadview`` helps you stream the files with very little code;
|
||||
|
||||
* ``django-downloadview`` helps you improve performances with reverse proxies,
|
||||
via mechanisms such as Nginx's X-Accel.
|
||||
|
||||
|
||||
*******
|
||||
Example
|
||||
*******
|
||||
|
||||
Let's serve a file stored in a FileField of some model:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.conf.urls import url, url_patterns
|
||||
from django_downloadview import ObjectDownloadView
|
||||
from demoproject.download.models import Document # A model with a FileField
|
||||
|
||||
# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
|
||||
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
||||
|
||||
url_patterns = ('',
|
||||
url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', download, name='download'),
|
||||
)
|
||||
|
||||
|
||||
**********
|
||||
Ressources
|
||||
**********
|
||||
|
||||
* Documentation: http://django-downloadview.readthedocs.org
|
||||
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
||||
* Code repository: https://github.com/benoitbryon/django-downloadview
|
||||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
||||
* Roadmap: https://github.com/benoitbryon/django-downloadview/issues/milestones
|
||||
98
demo/README
98
demo/README
|
|
@ -1,98 +0,0 @@
|
|||
############
|
||||
Demo project
|
||||
############
|
||||
|
||||
The :file:`demo/` folder holds a demo project to illustrate 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.
|
||||
|
||||
|
||||
*********************************
|
||||
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
|
||||
**********
|
||||
|
||||
.. 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
|
||||
72
demo/README.rst
Normal file
72
demo/README.rst
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
############
|
||||
Demo project
|
||||
############
|
||||
|
||||
`Demo folder in project's repository`_ contains a Django project to illustrate
|
||||
`django-downloadview` usage.
|
||||
|
||||
|
||||
*****************************************
|
||||
Documentation includes code from the demo
|
||||
*****************************************
|
||||
|
||||
Almost every example in the documentation comes from the demo:
|
||||
|
||||
* discover examples in the documentation;
|
||||
* browse related code and tests in demo project.
|
||||
|
||||
Examples in documentation are tested via demo project!
|
||||
|
||||
|
||||
***********************
|
||||
Browse demo code online
|
||||
***********************
|
||||
|
||||
See `demo folder in project's repository`_.
|
||||
|
||||
|
||||
***************
|
||||
Deploy the demo
|
||||
***************
|
||||
|
||||
System requirements:
|
||||
|
||||
* `Python`_ version 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 runserver
|
||||
|
||||
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
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
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')
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# coding=utf8
|
||||
"""Test suite for demoproject.download."""
|
||||
from os import listdir
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from django_downloadview.test import temporary_media_root
|
||||
|
||||
from demoproject.download.models import Document
|
||||
|
||||
|
||||
app_dir = dirname(abspath(__file__))
|
||||
fixtures_dir = join(app_dir, 'fixtures')
|
||||
|
||||
|
||||
class DownloadTestCase(TestCase):
|
||||
"""Base class for download tests."""
|
||||
def setUp(self):
|
||||
"""Common setup."""
|
||||
super(DownloadTestCase, self).setUp()
|
||||
self.files = {}
|
||||
for f in listdir(fixtures_dir):
|
||||
self.files[f] = abspath(join(fixtures_dir, f))
|
||||
|
||||
def assertDownloadHelloWorld(self, response, is_attachment=True):
|
||||
"""Assert response is 'hello-world.txt' download."""
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(response['Content-Type'],
|
||||
'text/plain; charset=utf-8')
|
||||
self.assertFalse('ContentEncoding' in response)
|
||||
if is_attachment:
|
||||
self.assertEquals(response['Content-Disposition'],
|
||||
'attachment; filename=hello-world.txt')
|
||||
else:
|
||||
self.assertFalse('Content-Disposition' in response)
|
||||
self.assertEqual(open(self.files['hello-world.txt']).read(),
|
||||
''.join(response.streaming_content))
|
||||
|
||||
|
||||
class PathDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "hello_world" and "hello_world_inline" views."""
|
||||
def test_download_hello_world(self):
|
||||
"""hello_world view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('hello_world')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
def test_download_hello_world_inline(self):
|
||||
"""hello_world view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('hello_world_inline')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response, is_attachment=False)
|
||||
|
||||
|
||||
class CustomPathDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "fixture_from_path" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""fixture_from_path view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('fixture_from_path', args=['hello-world.txt'])
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class StorageDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "fixture_from_storage" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""fixture_from_storage view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('fixture_from_storage',
|
||||
args=['hello-world.txt'])
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class ObjectDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test generic ObjectDownloadView."""
|
||||
@temporary_media_root()
|
||||
def test_download_hello_world(self):
|
||||
"""'download_document' view returns hello-world.txt as attachement."""
|
||||
slug = 'hello-world'
|
||||
download_url = reverse('document', kwargs={'slug': slug})
|
||||
Document.objects.create(slug=slug,
|
||||
file=File(open(self.files['hello-world.txt'])))
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class GeneratedDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "generated_hello_world" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""generated_hello_world view returns hello-world.txt as attachement.
|
||||
|
||||
"""
|
||||
download_url = reverse('generated_hello_world')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class ProxiedDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "http_hello_world" view."""
|
||||
def test_download_readme(self):
|
||||
"""http_hello_world view proxies file from URL."""
|
||||
download_url = reverse('http_hello_world')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# coding=utf8
|
||||
"""URL mapping."""
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'demoproject.download.views',
|
||||
# Model-based downloads.
|
||||
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
'download_document',
|
||||
name='document'),
|
||||
# Storage-based downloads.
|
||||
url(r'^storage/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
'download_fixture_from_storage',
|
||||
name='fixture_from_storage'),
|
||||
# Path-based downloads.
|
||||
url(r'^hello-world\.txt$',
|
||||
'download_hello_world',
|
||||
name='hello_world'),
|
||||
url(r'^hello-world-inline\.txt$',
|
||||
'download_hello_world_inline',
|
||||
name='hello_world_inline'),
|
||||
url(r'^path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
'download_fixture_from_path',
|
||||
name='fixture_from_path'),
|
||||
# URL-based downloads.
|
||||
url(r'^http/readme\.txt$',
|
||||
'download_http_hello_world',
|
||||
name='http_hello_world'),
|
||||
# Generated downloads.
|
||||
url(r'^generated/hello-world\.txt$',
|
||||
'download_generated_hello_world',
|
||||
name='generated_hello_world'),
|
||||
)
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# coding=utf8
|
||||
"""Demo download views."""
|
||||
from cStringIO import StringIO
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
from django_downloadview.files import VirtualFile
|
||||
from django_downloadview import views
|
||||
from demoproject.download.models import Document
|
||||
|
||||
|
||||
# Some initializations.
|
||||
|
||||
#: Directory containing code of :py:module:`demoproject.download.views`.
|
||||
app_dir = dirname(abspath(__file__))
|
||||
|
||||
#: Directory containing files fixtures.
|
||||
fixtures_dir = join(app_dir, 'fixtures')
|
||||
|
||||
#: Path to a text file that says 'Hello world!'.
|
||||
hello_world_path = join(fixtures_dir, 'hello-world.txt')
|
||||
|
||||
#: Storage for fixtures.
|
||||
fixtures_storage = FileSystemStorage(location=fixtures_dir)
|
||||
|
||||
|
||||
# Here are the views.
|
||||
|
||||
#: Pre-configured download view for :py:class:`Document` model.
|
||||
download_document = views.ObjectDownloadView.as_view(model=Document)
|
||||
|
||||
|
||||
#: Same as download_document, but streamed inline, i.e. not as attachments.
|
||||
download_document_inline = views.ObjectDownloadView.as_view(model=Document,
|
||||
attachment=False)
|
||||
|
||||
|
||||
#: Pre-configured view using a storage.
|
||||
download_fixture_from_storage = views.StorageDownloadView.as_view(
|
||||
storage=fixtures_storage)
|
||||
|
||||
|
||||
#: Direct download of one file, based on an absolute path.
|
||||
#:
|
||||
#: You could use this example as a shortcut, inside other views.
|
||||
download_hello_world = views.PathDownloadView.as_view(path=hello_world_path)
|
||||
|
||||
|
||||
#: Direct download of one file, based on an absolute path, not as attachment.
|
||||
download_hello_world_inline = views.PathDownloadView.as_view(
|
||||
path=hello_world_path,
|
||||
attachment=False)
|
||||
|
||||
|
||||
class CustomPathDownloadView(views.PathDownloadView):
|
||||
"""Example of customized PathDownloadView."""
|
||||
def get_path(self):
|
||||
"""Convert relative path (provided in URL) into absolute path.
|
||||
|
||||
Notice that this particularly simple use case is covered by
|
||||
:py:class:`django_downloadview.views.StorageDownloadView`.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you are doing such things, make the path secure! Prevent users
|
||||
to download files anywhere in the filesystem.
|
||||
|
||||
"""
|
||||
path = super(CustomPathDownloadView, self).get_path()
|
||||
return join(fixtures_dir, path)
|
||||
|
||||
#: Pre-configured :py:class:`CustomPathDownloadView`.
|
||||
download_fixture_from_path = CustomPathDownloadView.as_view()
|
||||
|
||||
|
||||
class StringIODownloadView(views.VirtualDownloadView):
|
||||
"""Sample download view using StringIO object."""
|
||||
def get_file(self):
|
||||
"""Return wrapper on StringIO object."""
|
||||
file_obj = StringIO(u"Hello world!\n".encode('utf-8'))
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
||||
#: Pre-configured view that serves "Hello world!" via a StringIO.
|
||||
download_generated_hello_world = StringIODownloadView.as_view()
|
||||
|
||||
|
||||
download_http_hello_world = views.HTTPDownloadView.as_view(
|
||||
url=u'https://raw.github.com/benoitbryon/django-downloadview/master/demo/demoproject/download/fixtures/hello-world.txt',
|
||||
basename=u'hello-world.txt')
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "download.document",
|
||||
"model": "object.document",
|
||||
"fields": {
|
||||
"slug": "hello-world",
|
||||
"file": "document/hello-world.txt"
|
||||
7
demo/demoproject/http/__init__.py
Normal file
7
demo/demoproject/http/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Demo for :class:`django_downloadview.HTTPDownloadView`.
|
||||
|
||||
Code in this package is included in documentation's :doc:`/views/http`.
|
||||
Make sure to maintain both together.
|
||||
|
||||
"""
|
||||
1
demo/demoproject/http/models.py
Normal file
1
demo/demoproject/http/models.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Required to make a Django application."""
|
||||
16
demo/demoproject/http/tests.py
Normal file
16
demo/demoproject/http/tests.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
||||
class SimpleURLTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'simple_url' serves 'hello-world.txt' from Github."""
|
||||
url = reverse('http:simple_url')
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
11
demo/demoproject/http/urls.py
Normal file
11
demo/demoproject/http/urls.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
from demoproject.http import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^simple_url/$',
|
||||
views.simple_url,
|
||||
name='simple_url'),
|
||||
)
|
||||
12
demo/demoproject/http/views.py
Normal file
12
demo/demoproject/http/views.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from django_downloadview import HTTPDownloadView
|
||||
|
||||
|
||||
class SimpleURLDownloadView(HTTPDownloadView):
|
||||
def get_url(self):
|
||||
"""Return URL of hello-world.txt file on GitHub."""
|
||||
return 'https://raw.github.com/benoitbryon/django-downloadview' \
|
||||
'/b7f660c5e3f37d918b106b02c5af7a887acc0111' \
|
||||
'/demo/demoproject/download/fixtures/hello-world.txt'
|
||||
|
||||
|
||||
simple_url = SimpleURLDownloadView.as_view()
|
||||
|
|
@ -1 +1 @@
|
|||
"""Nginx optimizations applied to demoproject.download."""
|
||||
"""Nginx optimizations."""
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""Required to make a Django application."""
|
||||
|
|
@ -1,62 +1,51 @@
|
|||
"""Test suite for demoproject.nginx."""
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse_lazy as reverse
|
||||
import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
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
|
||||
from demoproject.nginx.views import storage, storage_dir
|
||||
|
||||
|
||||
class XAccelRedirectDecoratorTestCase(DownloadTestCase):
|
||||
@temporary_media_root()
|
||||
def setup_file():
|
||||
if not os.path.exists(storage_dir):
|
||||
os.makedirs(storage_dir)
|
||||
storage.save('hello-world.txt', ContentFile(u'Hello world!\n'))
|
||||
|
||||
|
||||
class OptimizedByMiddlewareTestCase(django.test.TestCase):
|
||||
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.
|
||||
"""'nginx:optimized_by_middleware' returns X-Accel response."""
|
||||
setup_file()
|
||||
url = reverse('nginx:optimized_by_middleware')
|
||||
response = self.client.get(url)
|
||||
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",
|
||||
redirect_url="/nginx-optimized-by-middleware/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')
|
||||
|
||||
|
||||
class InlineXAccelRedirectTestCase(DownloadTestCase):
|
||||
@temporary_media_root()
|
||||
class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||
def test_response(self):
|
||||
"""X-Accel optimization respects ``attachment`` attribute."""
|
||||
document = Document.objects.create(
|
||||
slug='hello-world',
|
||||
file=File(open(self.files['hello-world.txt'])),
|
||||
)
|
||||
download_url = reverse('download_document_nginx_inline',
|
||||
kwargs={'slug': 'hello-world'})
|
||||
response = self.client.get(download_url)
|
||||
"""'nginx:optimized_by_decorator' returns X-Accel response."""
|
||||
setup_file()
|
||||
url = reverse('nginx:optimized_by_decorator')
|
||||
response = self.client.get(url)
|
||||
assert_x_accel_redirect(
|
||||
self,
|
||||
response,
|
||||
content_type="text/plain; charset=utf-8",
|
||||
charset="utf-8",
|
||||
attachment=False,
|
||||
redirect_url="/download-optimized/document/hello-world.txt",
|
||||
basename="hello-world.txt",
|
||||
redirect_url="/nginx-optimized-by-decorator/hello-world.txt",
|
||||
expires=None,
|
||||
with_buffering=None,
|
||||
limit_rate=None)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ from django.conf.urls import patterns, url
|
|||
|
||||
urlpatterns = patterns(
|
||||
'demoproject.nginx.views',
|
||||
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
'download_document_nginx', name='download_document_nginx'),
|
||||
url(r'^document-nginx-inline/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
'download_document_nginx_inline',
|
||||
name='download_document_nginx_inline'),
|
||||
url(r'^optimized-by-middleware/$',
|
||||
'optimized_by_middleware',
|
||||
name='optimized_by_middleware'),
|
||||
url(r'^optimized-by-decorator/$',
|
||||
'optimized_by_decorator',
|
||||
name='optimized_by_decorator'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
"""Views."""
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
from django_downloadview import StorageDownloadView
|
||||
from django_downloadview.nginx import x_accel_redirect
|
||||
|
||||
from demoproject.download import views
|
||||
|
||||
storage_dir = os.path.join(settings.MEDIA_ROOT, 'nginx')
|
||||
storage = FileSystemStorage(location=storage_dir,
|
||||
base_url=''.join([settings.MEDIA_URL, 'nginx/']))
|
||||
|
||||
|
||||
download_document_nginx = x_accel_redirect(
|
||||
views.download_document,
|
||||
source_dir='/var/www/files',
|
||||
destination_url='/download-optimized')
|
||||
optimized_by_middleware = StorageDownloadView.as_view(storage=storage,
|
||||
path='hello-world.txt')
|
||||
|
||||
|
||||
download_document_nginx_inline = x_accel_redirect(
|
||||
views.download_document_inline,
|
||||
source_dir=settings.MEDIA_ROOT,
|
||||
destination_url='/download-optimized')
|
||||
optimized_by_decorator = x_accel_redirect(
|
||||
StorageDownloadView.as_view(storage=storage, path='hello-world.txt'),
|
||||
source_url=storage.base_url,
|
||||
destination_url='/nginx-optimized-by-decorator/')
|
||||
|
|
|
|||
7
demo/demoproject/object/__init__.py
Normal file
7
demo/demoproject/object/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Demo for :class:`django_downloadview.ObjectDownloadView`.
|
||||
|
||||
Code in this package is included in documentation's :doc:`/views/object`.
|
||||
Make sure to maintain both together.
|
||||
|
||||
"""
|
||||
8
demo/demoproject/object/models.py
Normal file
8
demo/demoproject/object/models.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
slug = models.SlugField()
|
||||
file = models.FileField(upload_to='object')
|
||||
another_file = models.FileField(upload_to='object-other')
|
||||
basename = models.CharField(max_length=100)
|
||||
70
demo/demoproject/object/tests.py
Normal file
70
demo/demoproject/object/tests.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
from django_downloadview import temporary_media_root, assert_download_response
|
||||
|
||||
from demoproject.object.models import Document
|
||||
|
||||
|
||||
# Fixtures.
|
||||
slug = 'hello-world'
|
||||
basename = 'hello-world.txt'
|
||||
file_name = 'file.txt'
|
||||
another_name = 'another_file.txt'
|
||||
file_content = 'Hello world!\n'
|
||||
another_content = 'Goodbye world!\n'
|
||||
|
||||
|
||||
def setup_document():
|
||||
document = Document(slug=slug, basename=basename)
|
||||
document.file.save(file_name,
|
||||
ContentFile(file_content),
|
||||
save=False)
|
||||
document.another_file.save(another_name,
|
||||
ContentFile(another_content),
|
||||
save=False)
|
||||
document.save()
|
||||
return document
|
||||
|
||||
|
||||
class DefaultFileTestCase(django.test.TestCase):
|
||||
@temporary_media_root()
|
||||
def test_download_response(self):
|
||||
"""'default_file' streams Document.file."""
|
||||
setup_document()
|
||||
url = reverse('object:default_file', kwargs={'slug': slug})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename=file_name,
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class AnotherFileTestCase(django.test.TestCase):
|
||||
@temporary_media_root()
|
||||
def test_download_response(self):
|
||||
"""'another_file' streams Document.another_file."""
|
||||
setup_document()
|
||||
url = reverse('object:another_file', kwargs={'slug': slug})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content=another_content,
|
||||
basename=another_name,
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class DeserializedBasenameTestCase(django.test.TestCase):
|
||||
@temporary_media_root()
|
||||
def test_download_response(self):
|
||||
"'deserialized_basename' streams Document.file with custom basename."
|
||||
setup_document()
|
||||
url = reverse('object:deserialized_basename', kwargs={'slug': slug})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename=basename,
|
||||
mime_type='text/plain')
|
||||
17
demo/demoproject/object/urls.py
Normal file
17
demo/demoproject/object/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
from demoproject.object import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
views.default_file_view,
|
||||
name='default_file'),
|
||||
url(r'^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
views.another_file_view,
|
||||
name='another_file'),
|
||||
url(r'^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
views.deserialized_basename_view,
|
||||
name='deserialized_basename'),
|
||||
)
|
||||
18
demo/demoproject/object/views.py
Normal file
18
demo/demoproject/object/views.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from django_downloadview import ObjectDownloadView
|
||||
|
||||
from demoproject.object.models import Document
|
||||
|
||||
|
||||
#: Serve ``file`` attribute of ``Document`` model.
|
||||
default_file_view = ObjectDownloadView.as_view(model=Document)
|
||||
|
||||
#: Serve ``another_file`` attribute of ``Document`` model.
|
||||
another_file_view = ObjectDownloadView.as_view(
|
||||
model=Document,
|
||||
file_field='another_file')
|
||||
|
||||
#: Serve ``file`` attribute of ``Document`` model, using client-side filename
|
||||
#: from model.
|
||||
deserialized_basename_view = ObjectDownloadView.as_view(
|
||||
model=Document,
|
||||
basename_field='basename')
|
||||
7
demo/demoproject/path/__init__.py
Normal file
7
demo/demoproject/path/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Demo for :class:`django_downloadview.PathDownloadView`.
|
||||
|
||||
Code in this package is included in documentation's :doc:`/views/path`.
|
||||
Make sure to maintain both together.
|
||||
|
||||
"""
|
||||
1
demo/demoproject/path/models.py
Normal file
1
demo/demoproject/path/models.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Required to make a Django application."""
|
||||
28
demo/demoproject/path/tests.py
Normal file
28
demo/demoproject/path/tests.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
||||
class StaticPathTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'static_path' serves 'fixtures/hello-world.txt'."""
|
||||
url = reverse('path:static_path')
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class DynamicPathTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'dynamic_path' serves 'fixtures/{path}'."""
|
||||
url = reverse('path:dynamic_path', kwargs={'path': 'hello-world.txt'})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
14
demo/demoproject/path/urls.py
Normal file
14
demo/demoproject/path/urls.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
from demoproject.path import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^static-path/$',
|
||||
views.static_path,
|
||||
name='static_path'),
|
||||
url(r'^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
views.dynamic_path,
|
||||
name='dynamic_path'),
|
||||
)
|
||||
39
demo/demoproject/path/views.py
Normal file
39
demo/demoproject/path/views.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
|
||||
from django_downloadview import PathDownloadView
|
||||
|
||||
|
||||
# Let's initialize some fixtures.
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_dir = os.path.dirname(app_dir)
|
||||
fixtures_dir = os.path.join(project_dir, 'fixtures')
|
||||
#: Path to a text file that says 'Hello world!'.
|
||||
hello_world_path = os.path.join(fixtures_dir, 'hello-world.txt')
|
||||
|
||||
#: Serve ``fixtures/hello-world.txt`` file.
|
||||
static_path = PathDownloadView.as_view(path=hello_world_path)
|
||||
|
||||
|
||||
class DynamicPathDownloadView(PathDownloadView):
|
||||
"""Serve file in ``settings.MEDIA_ROOT``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Make sure to prevent "../" in path via URL patterns.
|
||||
|
||||
.. note::
|
||||
|
||||
This particular setup would be easier to perform with
|
||||
:class:`StorageDownloadView`
|
||||
|
||||
"""
|
||||
def get_path(self):
|
||||
"""Return path inside fixtures directory."""
|
||||
# Get path from URL resolvers or as_view kwarg.
|
||||
relative_path = super(DynamicPathDownloadView, self).get_path()
|
||||
# Make it absolute.
|
||||
absolute_path = os.path.join(fixtures_dir, relative_path)
|
||||
return absolute_path
|
||||
|
||||
|
||||
dynamic_path = DynamicPathDownloadView.as_view()
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Django settings for Django-DownloadView demo project."""
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ demoproject_dir = dirname(abspath(__file__))
|
|||
demo_dir = dirname(demoproject_dir)
|
||||
root_dir = dirname(demo_dir)
|
||||
data_dir = join(root_dir, 'var')
|
||||
cfg_dir = join(root_dir, 'etc')
|
||||
|
||||
|
||||
# Mandatory settings.
|
||||
|
|
@ -44,45 +46,41 @@ INSTALLED_APPS = (
|
|||
'django.contrib.staticfiles',
|
||||
# The actual django-downloadview demo.
|
||||
'demoproject',
|
||||
'demoproject.download', # Sample standard download views.
|
||||
'demoproject.nginx', # Sample optimizations for Nginx.
|
||||
'demoproject.object', # Demo around ObjectDownloadView
|
||||
'demoproject.storage', # Demo around StorageDownloadView
|
||||
'demoproject.path', # Demo around PathDownloadView
|
||||
'demoproject.http', # Demo around HTTPDownloadView
|
||||
'demoproject.virtual', # Demo around VirtualDownloadView
|
||||
'demoproject.nginx', # Sample optimizations for Nginx X-Accel.
|
||||
# For test purposes. The demo project is part of django-downloadview
|
||||
# test suite.
|
||||
'django_nose',
|
||||
)
|
||||
|
||||
|
||||
# Default middlewares. You may alter the list later.
|
||||
# Middlewares.
|
||||
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',
|
||||
'django_downloadview.DownloadDispatcherMiddleware'
|
||||
]
|
||||
|
||||
|
||||
# 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"
|
||||
DOWNLOADVIEW_MIDDLEWARES = (
|
||||
('default', 'django_downloadview.nginx.XAccelRedirectMiddleware',
|
||||
{'source_url': '/media/nginx/',
|
||||
'destination_url': '/nginx-optimized-by-middleware/'}),
|
||||
)
|
||||
|
||||
|
||||
# Development configuration.
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
NOSE_ARGS = ['--verbose',
|
||||
'--nocapture',
|
||||
'--rednose',
|
||||
'--with-id', # allows --failed which only reruns failed tests
|
||||
'--id-file=%s' % join(data_dir, 'test', 'noseids'),
|
||||
'--with-doctest',
|
||||
'--with-xunit',
|
||||
'--xunit-file=%s' % join(data_dir, 'test', 'nosetests.xml'),
|
||||
'--with-coverage',
|
||||
'--cover-erase',
|
||||
'--cover-package=django_downloadview',
|
||||
'--no-path-adjustment',
|
||||
'--all-modules',
|
||||
]
|
||||
nose_cfg_dir = join(cfg_dir, 'nose')
|
||||
NOSE_ARGS = ['--config={etc}/base.cfg'.format(etc=nose_cfg_dir),
|
||||
'--config={etc}/{package}.cfg'.format(etc=nose_cfg_dir,
|
||||
package=__package__)]
|
||||
|
|
|
|||
7
demo/demoproject/storage/__init__.py
Normal file
7
demo/demoproject/storage/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Demo for :class:`django_downloadview.StorageDownloadView`.
|
||||
|
||||
Code in this package is included in documentation's :doc:`/views/storage`.
|
||||
Make sure to maintain both together.
|
||||
|
||||
"""
|
||||
1
demo/demoproject/storage/models.py
Normal file
1
demo/demoproject/storage/models.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Required to make a Django application."""
|
||||
4
demo/demoproject/storage/storage.py
Normal file
4
demo/demoproject/storage/storage.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
||||
storage = FileSystemStorage()
|
||||
76
demo/demoproject/storage/tests.py
Normal file
76
demo/demoproject/storage/tests.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import unittest
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
from django_downloadview import assert_download_response, temporary_media_root
|
||||
from django_downloadview import setup_view
|
||||
|
||||
from demoproject.storage import views
|
||||
|
||||
|
||||
# Fixtures.
|
||||
file_content = 'Hello world!\n'
|
||||
|
||||
|
||||
def setup_file(path):
|
||||
views.storage.save(path, ContentFile(file_content))
|
||||
|
||||
|
||||
class StaticPathTestCase(django.test.TestCase):
|
||||
@temporary_media_root()
|
||||
def test_download_response(self):
|
||||
"""'storage:static_path' streams file by path."""
|
||||
setup_file('1.txt')
|
||||
url = reverse('storage:static_path', kwargs={'path': '1.txt'})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename='1.txt',
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class DynamicPathIntegrationTestCase(django.test.TestCase):
|
||||
"""Integration tests around ``storage:dynamic_path`` URL."""
|
||||
@temporary_media_root()
|
||||
def test_download_response(self):
|
||||
"""'dynamic_path' streams file by generated path.
|
||||
|
||||
As we use ``self.client``, this test involves the whole Django stack,
|
||||
including settings, middlewares, decorators... So we need to setup a
|
||||
file, the storage, and an URL.
|
||||
|
||||
This test actually asserts the URL ``storage:dynamic_path`` streams a
|
||||
file in storage.
|
||||
|
||||
"""
|
||||
setup_file('1.TXT')
|
||||
url = reverse('storage:dynamic_path', kwargs={'path': '1.txt'})
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename='1.TXT',
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class DynamicPathUnitTestCase(unittest.TestCase):
|
||||
"""Unit tests around ``views.DynamicStorageDownloadView``."""
|
||||
def test_get_path(self):
|
||||
"""DynamicStorageDownloadView.get_path() returns uppercase path.
|
||||
|
||||
Uses :func:`~django_downloadview.test.setup_view` to target only
|
||||
overriden methods.
|
||||
|
||||
This test does not involve URLconf, middlewares or decorators. It is
|
||||
fast. It has clear scope. It does not assert ``storage:dynamic_path``
|
||||
URL works. It targets only custom ``DynamicStorageDownloadView`` class.
|
||||
|
||||
"""
|
||||
view = setup_view(views.DynamicStorageDownloadView(),
|
||||
django.test.RequestFactory().get('/fake-url'),
|
||||
path='dummy path')
|
||||
path = view.get_path()
|
||||
self.assertEqual(path, 'DUMMY PATH')
|
||||
14
demo/demoproject/storage/urls.py
Normal file
14
demo/demoproject/storage/urls.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
from demoproject.storage import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
views.static_path,
|
||||
name='static_path'),
|
||||
url(r'^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
views.dynamic_path,
|
||||
name='dynamic_path'),
|
||||
)
|
||||
20
demo/demoproject/storage/views.py
Normal file
20
demo/demoproject/storage/views.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
from django_downloadview import StorageDownloadView
|
||||
|
||||
|
||||
storage = FileSystemStorage()
|
||||
|
||||
|
||||
#: Serve file using ``path`` argument.
|
||||
static_path = StorageDownloadView.as_view(storage=storage)
|
||||
|
||||
|
||||
class DynamicStorageDownloadView(StorageDownloadView):
|
||||
"""Serve file of storage by path.upper()."""
|
||||
def get_path(self):
|
||||
"""Return uppercase path."""
|
||||
return super(DynamicStorageDownloadView, self).get_path().upper()
|
||||
|
||||
|
||||
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)
|
||||
|
|
@ -10,19 +10,7 @@
|
|||
<h2>Serving files with Django</h2>
|
||||
<p>In the following views, Django streams the files, no optimization
|
||||
has been setup.</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'hello_world' %}">PathDownloadView</a></li>
|
||||
<li><a href="{% url 'fixture_from_path' 'hello-world.txt' %}">
|
||||
PathDownloadView + argument in URL
|
||||
</a></li>
|
||||
<li><a href="{% url 'fixture_from_storage' 'hello-world.txt' %}">
|
||||
StorageDownloadView + path in URL
|
||||
</a></li>
|
||||
<li><a href="{% url 'document' 'hello-world' %}">
|
||||
ObjectDownloadView
|
||||
</a></li>
|
||||
<li><a href="{% url 'http_hello_world' %}">
|
||||
HTTPDownloadView</a>, a simple HTTP proxy</li>
|
||||
<ul>
|
||||
</ul>
|
||||
|
||||
<h2>Optimized downloads</h2>
|
||||
|
|
@ -31,9 +19,6 @@
|
|||
<p>Since nginx and other servers aren't installed on the demo, you
|
||||
will get raw "X-Sendfile" responses. Look at the headers!</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">
|
||||
ObjectDownloadView (nginx)
|
||||
</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,30 @@ home = TemplateView.as_view(template_name='home.html')
|
|||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
# Standard download views.
|
||||
url(r'^download/', include('demoproject.download.urls')),
|
||||
# ObjectDownloadView.
|
||||
url(r'^object/', include('demoproject.object.urls',
|
||||
app_name='object',
|
||||
namespace='object')),
|
||||
# StorageDownloadView.
|
||||
url(r'^storage/', include('demoproject.storage.urls',
|
||||
app_name='storage',
|
||||
namespace='storage')),
|
||||
# PathDownloadView.
|
||||
url(r'^path/', include('demoproject.path.urls',
|
||||
app_name='path',
|
||||
namespace='path')),
|
||||
# HTTPDownloadView.
|
||||
url(r'^http/', include('demoproject.http.urls',
|
||||
app_name='http',
|
||||
namespace='http')),
|
||||
# VirtualDownloadView.
|
||||
url(r'^virtual/', include('demoproject.virtual.urls',
|
||||
app_name='virtual',
|
||||
namespace='virtual')),
|
||||
# Nginx optimizations.
|
||||
url(r'^nginx/', include('demoproject.nginx.urls')),
|
||||
url(r'^nginx/', include('demoproject.nginx.urls',
|
||||
app_name='nginx',
|
||||
namespace='nginx')),
|
||||
# An informative homepage.
|
||||
url(r'', home, name='home')
|
||||
)
|
||||
|
|
|
|||
7
demo/demoproject/virtual/__init__.py
Normal file
7
demo/demoproject/virtual/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Demo for :class:`django_downloadview.VirtualDownloadView`.
|
||||
|
||||
Code in this package is included in documentation's :doc:`/views/virtual`.
|
||||
Make sure to maintain both together.
|
||||
|
||||
"""
|
||||
1
demo/demoproject/virtual/models.py
Normal file
1
demo/demoproject/virtual/models.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Required to make a Django application."""
|
||||
40
demo/demoproject/virtual/tests.py
Normal file
40
demo/demoproject/virtual/tests.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
||||
class TextTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'virtual:text' serves 'hello-world.txt' from unicode."""
|
||||
url = reverse('virtual:text')
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class StringIOTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'virtual:stringio' serves 'hello-world.txt' from stringio."""
|
||||
url = reverse('virtual:stringio')
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
|
||||
|
||||
class GeneratedTestCase(django.test.TestCase):
|
||||
def test_download_response(self):
|
||||
"""'virtual:generated' serves 'hello-world.txt' from generator."""
|
||||
url = reverse('virtual:generated')
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
content='Hello world!\n',
|
||||
basename='hello-world.txt',
|
||||
mime_type='text/plain')
|
||||
17
demo/demoproject/virtual/urls.py
Normal file
17
demo/demoproject/virtual/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
from demoproject.virtual import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^text/$',
|
||||
views.TextDownloadView.as_view(),
|
||||
name='text'),
|
||||
url(r'^stringio/$',
|
||||
views.StringIODownloadView.as_view(),
|
||||
name='stringio'),
|
||||
url(r'^gerenated/$',
|
||||
views.GeneratedDownloadView.as_view(),
|
||||
name='generated'),
|
||||
)
|
||||
33
demo/demoproject/virtual/views.py
Normal file
33
demo/demoproject/virtual/views.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from StringIO import StringIO
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from django_downloadview import VirtualDownloadView
|
||||
from django_downloadview import VirtualFile
|
||||
from django_downloadview import StringIteratorIO
|
||||
|
||||
|
||||
class TextDownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return :class:`django.core.files.base.ContentFile` object."""
|
||||
return ContentFile(u"Hello world!\n", name='hello-world.txt')
|
||||
|
||||
|
||||
class StringIODownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return wrapper on ``StringIO`` object."""
|
||||
file_obj = StringIO(u"Hello world!\n".encode('utf-8'))
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
||||
|
||||
def generate_hello():
|
||||
yield u'Hello '
|
||||
yield u'world!'
|
||||
yield u'\n'
|
||||
|
||||
|
||||
class GeneratedDownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return wrapper on ``StringIteratorIO`` object."""
|
||||
file_obj = StringIteratorIO(generate_hello())
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
|
@ -1,46 +1,47 @@
|
|||
# coding=utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Python packaging."""
|
||||
import os
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
def read_relative_file(filename):
|
||||
"""Returns contents of the given file, which path is supposed relative
|
||||
to this module."""
|
||||
with open(os.path.join(os.path.dirname(__file__), filename)) as f:
|
||||
return f.read()
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
project_root = os.path.dirname(here)
|
||||
|
||||
|
||||
NAME = 'django-downloadview-demo'
|
||||
README = read_relative_file('README')
|
||||
VERSION = '0.1'
|
||||
DESCRIPTION = 'Serve files with Django and reverse-proxies.'
|
||||
README = open(os.path.join(here, 'README.rst')).read()
|
||||
VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
|
||||
AUTHOR = u'Benoît Bryon'
|
||||
EMAIL = u'benoit@marmelune.net'
|
||||
URL = 'https://{name}.readthedocs.org/'.format(name=NAME)
|
||||
CLASSIFIERS = ['Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Framework :: Django']
|
||||
KEYWORDS = []
|
||||
PACKAGES = ['demoproject']
|
||||
REQUIRES = ['django-downloadview',
|
||||
'django-nose']
|
||||
REQUIREMENTS = ['django-downloadview', 'django-nose']
|
||||
ENTRY_POINTS = {
|
||||
'console_scripts': ['demo = demoproject.manage:main']
|
||||
}
|
||||
|
||||
|
||||
setup(name=NAME,
|
||||
version=VERSION,
|
||||
description='Demo project for Django-DownloadView.',
|
||||
long_description=README,
|
||||
classifiers=['Development Status :: 1 - Planning',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Framework :: Django',
|
||||
],
|
||||
keywords='class-based view, generic view, download',
|
||||
author='Benoit Bryon',
|
||||
author_email='benoit@marmelune.net',
|
||||
url='https://github.com/benoitbryon/%s' % NAME,
|
||||
license='BSD',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIRES,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'demo = demoproject.manage:main',
|
||||
]
|
||||
},
|
||||
)
|
||||
if __name__ == '__main__': # Don't run setup() when we import this module.
|
||||
setup(name=NAME,
|
||||
version=VERSION,
|
||||
description=DESCRIPTION,
|
||||
long_description=README,
|
||||
classifiers=CLASSIFIERS,
|
||||
keywords=' '.join(KEYWORDS),
|
||||
author=AUTHOR,
|
||||
author_email=EMAIL,
|
||||
url=URL,
|
||||
license='BSD',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIREMENTS,
|
||||
entry_points=ENTRY_POINTS)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"""django-downloadview provides generic download views for Django."""
|
||||
# Shortcut import.
|
||||
from django_downloadview.views import (PathDownloadView,
|
||||
ObjectDownloadView,
|
||||
StorageDownloadView,
|
||||
VirtualDownloadView)
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Serve files with Django and reverse proxies."""
|
||||
import pkg_resources
|
||||
|
||||
|
||||
pkg_resources = __import__('pkg_resources')
|
||||
distribution = pkg_resources.get_distribution('django-downloadview')
|
||||
|
||||
#: Module version, as defined in PEP-0396.
|
||||
__version__ = distribution.version
|
||||
__version__ = pkg_resources.get_distribution(__package__.replace('-', '_')) \
|
||||
.version
|
||||
|
||||
|
||||
# API shortcuts.
|
||||
from django_downloadview.api import * # NoQA
|
||||
|
|
|
|||
22
django_downloadview/api.py
Normal file
22
django_downloadview/api.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Declaration of API shortcuts."""
|
||||
from django_downloadview.io import StringIteratorIO # NoQA
|
||||
from django_downloadview.files import (StorageFile, # NoQA
|
||||
VirtualFile,
|
||||
HTTPFile,
|
||||
File)
|
||||
from django_downloadview.response import (DownloadResponse, # NoQA
|
||||
ProxiedDownloadResponse)
|
||||
from django_downloadview.middlewares import (BaseDownloadMiddleware, # NoQA
|
||||
DownloadDispatcherMiddleware)
|
||||
from django_downloadview.views import (PathDownloadView, # NoQA
|
||||
ObjectDownloadView,
|
||||
StorageDownloadView,
|
||||
HTTPDownloadView,
|
||||
VirtualDownloadView,
|
||||
BaseDownloadView,
|
||||
DownloadMixin)
|
||||
from django_downloadview.sendfile import sendfile # NoQA
|
||||
from django_downloadview.test import (assert_download_response, # NoQA
|
||||
setup_view,
|
||||
temporary_media_root)
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
"""View decorators.
|
||||
|
||||
See also decorators provided by server-specific modules, such as
|
||||
:py:func:`django_downloadview.nginx.x_accel_redirect`.
|
||||
:func:`django_downloadview.nginx.x_accel_redirect`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class DownloadDecorator(object):
|
||||
"""View decorator factory to apply middleware to ``view_func`` response.
|
||||
"""View decorator factory to apply middleware to ``view_func``'s response.
|
||||
|
||||
Middleware instance is built from ``middleware_factory`` with ``*args`` and
|
||||
``**kwargs``. Middleware factory is typically a class, such as some
|
||||
:py:class:`django_downloadview.middlewares.XAccelMiddleware` subclass.
|
||||
:py:class:`django_downloadview.BaseDownloadMiddleware` subclass.
|
||||
|
||||
Response is built from view, then the middleware's ``process_response``
|
||||
method is applied on response.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""File wrappers for use as exchange data between views and responses."""
|
||||
from django.core.files import File
|
||||
from __future__ import absolute_import
|
||||
from io import BytesIO
|
||||
from urlparse import urlparse
|
||||
|
||||
import django.core.files
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class File(django.core.files.File):
|
||||
"""Patch Django's :meth:`__iter__` implementation.
|
||||
|
||||
See https://code.djangoproject.com/ticket/21321
|
||||
|
||||
"""
|
||||
def __iter__(self):
|
||||
# Iterate over this file-like object by newlines
|
||||
buffer_ = None
|
||||
for chunk in self.chunks():
|
||||
chunk_buffer = BytesIO(force_bytes(chunk))
|
||||
|
||||
for line in chunk_buffer:
|
||||
if buffer_:
|
||||
line = buffer_ + line
|
||||
buffer_ = None
|
||||
|
||||
# If this is the end of a line, yield
|
||||
# otherwise, wait for the next round
|
||||
if line[-1] in ('\n', '\r'):
|
||||
yield line
|
||||
else:
|
||||
buffer_ = line
|
||||
|
||||
if buffer_ is not None:
|
||||
yield buffer_
|
||||
|
||||
|
||||
class StorageFile(File):
|
||||
"""A file in a Django storage.
|
||||
|
||||
|
|
@ -185,7 +219,14 @@ class HTTPFile(File):
|
|||
**kwargs):
|
||||
self.request_factory = request_factory
|
||||
self.url = url
|
||||
self.name = name
|
||||
if name is None:
|
||||
parts = urlparse(url)
|
||||
if parts.path: # Name from path.
|
||||
self.name = parts.path.strip('/').rsplit('/', 1)[-1]
|
||||
else: # Name from domain.
|
||||
self.name = parts.netloc
|
||||
else:
|
||||
self.name = name
|
||||
kwargs['stream'] = True
|
||||
self.request_kwargs = kwargs
|
||||
|
||||
|
|
|
|||
65
django_downloadview/io.py
Normal file
65
django_downloadview/io.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Low-level IO operations, for use with file wrappers."""
|
||||
from __future__ import absolute_import
|
||||
import io
|
||||
|
||||
|
||||
class StringIteratorIO(io.TextIOBase):
|
||||
"""A dynamically generated StringIO-like object.
|
||||
|
||||
Original code by Matt Joiner <anacrolix@gmail.com> from:
|
||||
|
||||
* http://stackoverflow.com/questions/12593576/adapt-an-iterator-to-behave-like-a-file-like-object-in-python
|
||||
* https://gist.github.com/anacrolix/3788413
|
||||
|
||||
"""
|
||||
def __init__(self, iterator):
|
||||
self._iter = iterator
|
||||
self._left = ''
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def _read1(self, n=None):
|
||||
while not self._left:
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
break
|
||||
ret = self._left[:n]
|
||||
self._left = self._left[len(ret):]
|
||||
return ret
|
||||
|
||||
def read(self, n=None):
|
||||
l = []
|
||||
if n is None or n < 0:
|
||||
while True:
|
||||
m = self._read1()
|
||||
if not m:
|
||||
break
|
||||
l.append(m)
|
||||
else:
|
||||
while n > 0:
|
||||
m = self._read1(n)
|
||||
if not m:
|
||||
break
|
||||
n -= len(m)
|
||||
l.append(m)
|
||||
return ''.join(l)
|
||||
|
||||
def readline(self):
|
||||
l = []
|
||||
while True:
|
||||
i = self._left.find('\n')
|
||||
if i == -1:
|
||||
l.append(self._left)
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
self._left = ''
|
||||
break
|
||||
else:
|
||||
l.append(self._left[:i + 1])
|
||||
self._left = self._left[i + 1:]
|
||||
break
|
||||
return ''.join(l)
|
||||
|
|
@ -1,5 +1,25 @@
|
|||
"""Base material for download middlewares."""
|
||||
from django_downloadview.response import is_download_response
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Base material for download middlewares.
|
||||
|
||||
Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
|
||||
responses and may replace them with optimized download responses.
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from django_downloadview.response import DownloadResponse
|
||||
|
||||
|
||||
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.response.DownloadResponse`.
|
||||
|
||||
"""
|
||||
return isinstance(response, DownloadResponse)
|
||||
|
||||
|
||||
class BaseDownloadMiddleware(object):
|
||||
|
|
@ -12,14 +32,14 @@ class BaseDownloadMiddleware(object):
|
|||
"""Return True if ``response`` can be considered as a file download.
|
||||
|
||||
By default, this method uses
|
||||
:py:func:`django_downloadview.response.is_download_response`.
|
||||
:py:func:`django_downloadview.middlewares.is_download_response`.
|
||||
Override this method if you want a different behaviour.
|
||||
|
||||
"""
|
||||
return is_download_response(response)
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Call :py:meth:`process_download_response` if ``response`` is download."""
|
||||
"""Call `process_download_response()` if ``response`` is download."""
|
||||
if self.is_download_response(response):
|
||||
return self.process_download_response(request, response)
|
||||
return response
|
||||
|
|
@ -27,3 +47,106 @@ class BaseDownloadMiddleware(object):
|
|||
def process_download_response(self, request, response):
|
||||
"""Handle file download response."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RealDownloadMiddleware(BaseDownloadMiddleware):
|
||||
"""Download middleware that cannot handle virtual files."""
|
||||
def is_download_response(self, response):
|
||||
"""Return True for DownloadResponse, except for "virtual" files.
|
||||
|
||||
This implementation cannot handle files that live in memory or which
|
||||
are to be dynamically iterated over. So, we capture only responses
|
||||
whose file attribute have either an URL or a file name.
|
||||
|
||||
"""
|
||||
if super(RealDownloadMiddleware, self).is_download_response(response):
|
||||
try:
|
||||
return response.file.url or response.file.name
|
||||
except AttributeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
||||
"""Download middleware that dispatches job to several middlewares.
|
||||
|
||||
The list of Children middlewares is read in `DOWNLOADVIEW_MIDDLEWARES`
|
||||
setting.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
#: List of children middlewares.
|
||||
self.middlewares = []
|
||||
self.load_middlewares_from_settings()
|
||||
|
||||
def load_middlewares_from_settings(self):
|
||||
for (key, import_string, kwargs) in getattr(settings,
|
||||
'DOWNLOADVIEW_MIDDLEWARES',
|
||||
[]):
|
||||
if ':' in import_string:
|
||||
module_string, attr_string = import_string.split(':', 1)
|
||||
else:
|
||||
module_string, attr_string = import_string.rsplit('.', 1)
|
||||
module = __import__(module_string, globals(), locals(),
|
||||
[attr_string], -1)
|
||||
factory = getattr(module, attr_string)
|
||||
middleware = factory(**kwargs)
|
||||
self.middlewares.append((key, middleware))
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Dispatches job to children middlewares."""
|
||||
for (key, middleware) in self.middlewares:
|
||||
response = middleware.process_response(request, response)
|
||||
return response
|
||||
|
||||
|
||||
class NoRedirectionMatch(Exception):
|
||||
"""Response object does not match redirection rules."""
|
||||
|
||||
|
||||
class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
||||
"""Base class for middlewares that use optimizations of reverse proxies."""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None):
|
||||
"""Constructor."""
|
||||
self.source_dir = source_dir
|
||||
self.source_url = source_url
|
||||
self.destination_url = destination_url
|
||||
|
||||
def get_redirect_url(self, response):
|
||||
"""Return redirect URL for file wrapped into response."""
|
||||
url = None
|
||||
file_url = ''
|
||||
if self.source_url:
|
||||
try:
|
||||
file_url = response.file.url
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if file_url.startswith(self.source_url):
|
||||
file_url = file_url[len(self.source_url):]
|
||||
url = file_url
|
||||
file_name = ''
|
||||
if url is None and self.source_dir:
|
||||
try:
|
||||
file_name = response.file.name
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if file_name.startswith(self.source_dir):
|
||||
file_name = os.path.relpath(file_name, self.source_dir)
|
||||
url = file_name.replace(os.path.sep, '/')
|
||||
if url is None:
|
||||
message = ("""Couldn't capture/convert file attributes into a """
|
||||
"""redirection. """
|
||||
"""``source_url`` is "%(source_url)s", """
|
||||
"""file's URL is "%(file_url)s". """
|
||||
"""``source_dir`` is "%(source_dir)s", """
|
||||
"""file's name is "%(file_name)s". """
|
||||
% {'source_url': self.source_url,
|
||||
'file_url': file_url,
|
||||
'source_dir': self.source_dir,
|
||||
'file_name': file_name})
|
||||
raise NoRedirectionMatch(message)
|
||||
return '/'.join((self.destination_url.rstrip('/'), url.lstrip('/')))
|
||||
|
|
|
|||
|
|
@ -1,415 +0,0 @@
|
|||
"""Optimizations for Nginx.
|
||||
|
||||
See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||
:doc:`narrative documentation about Nginx optimizations
|
||||
</optimizations/nginx>`.
|
||||
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponse
|
||||
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
from django_downloadview.middlewares import BaseDownloadMiddleware
|
||||
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',
|
||||
DEFAULT_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-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)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR.
|
||||
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
|
||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT'):
|
||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is "
|
||||
"deprecated, use "
|
||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR instead.",
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR',
|
||||
DEFAULT_SOURCE_DIR)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL.
|
||||
DEFAULT_SOURCE_URL = settings.MEDIA_URL
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL',
|
||||
DEFAULT_SOURCE_URL)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL.
|
||||
DEFAULT_DESTINATION_URL = None
|
||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL'):
|
||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is "
|
||||
"deprecated, use "
|
||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL "
|
||||
"instead.",
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL',
|
||||
DEFAULT_DESTINATION_URL)
|
||||
|
||||
|
||||
class XAccelRedirectResponse(HttpResponse):
|
||||
"""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, attachment=True):
|
||||
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
||||
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
|
||||
if attachment:
|
||||
self.basename = basename or redirect_url.split('/')[-1]
|
||||
self['Content-Disposition'] = 'attachment; filename={name}'.format(
|
||||
name=self.basename)
|
||||
self['X-Accel-Redirect'] = redirect_url
|
||||
self['X-Accel-Charset'] = content_type_to_charset(content_type)
|
||||
if with_buffering is not None:
|
||||
self['X-Accel-Buffering'] = with_buffering and 'yes' or 'no'
|
||||
if expires:
|
||||
expire_seconds = timedelta(expires - datetime.now()).seconds
|
||||
self['X-Accel-Expires'] = expire_seconds
|
||||
elif expires is not None: # We explicitely want it off.
|
||||
self['X-Accel-Expires'] = 'off'
|
||||
if limit_rate is not None:
|
||||
self['X-Accel-Limit-Rate'] = (limit_rate
|
||||
and '%d' % limit_rate
|
||||
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_attachment(self, test_case, response, value):
|
||||
header = 'Content-Disposition'
|
||||
if value:
|
||||
test_case.assertTrue(response[header].startswith('attachment'))
|
||||
else:
|
||||
test_case.assertFalse(header in response)
|
||||
|
||||
|
||||
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):
|
||||
"""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
|
||||
use this class as the factory in
|
||||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None,
|
||||
expires=None, with_buffering=None, limit_rate=None,
|
||||
media_root=None, media_url=None):
|
||||
"""Constructor."""
|
||||
if media_url is not None:
|
||||
warnings.warn("%s ``media_url`` is deprecated. Use "
|
||||
"``destination_url`` instead."
|
||||
% self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if destination_url is None:
|
||||
self.destination_url = media_url
|
||||
else:
|
||||
self.destination_url = destination_url
|
||||
else:
|
||||
self.destination_url = destination_url
|
||||
if media_root is not None:
|
||||
warnings.warn("%s ``media_root`` is deprecated. Use "
|
||||
"``source_dir`` instead." % self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if source_dir is None:
|
||||
self.source_dir = media_root
|
||||
else:
|
||||
self.source_dir = source_dir
|
||||
else:
|
||||
self.source_dir = source_dir
|
||||
self.source_url = source_url
|
||||
self.expires = expires
|
||||
self.with_buffering = with_buffering
|
||||
self.limit_rate = limit_rate
|
||||
|
||||
def is_download_response(self, response):
|
||||
"""Return True for DownloadResponse, except for "virtual" files.
|
||||
|
||||
This implementation can't handle files that live in memory or which are
|
||||
to be dynamically iterated over. So, we capture only responses whose
|
||||
file attribute have either an URL or a file name.
|
||||
|
||||
"""
|
||||
if super(BaseXAccelRedirectMiddleware,
|
||||
self).is_download_response(response):
|
||||
try:
|
||||
response.file.url
|
||||
except AttributeError:
|
||||
try:
|
||||
response.file.name
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_redirect_url(self, response):
|
||||
"""Return redirect URL for file wrapped into response."""
|
||||
url = None
|
||||
file_url = ''
|
||||
if self.source_url is not None:
|
||||
try:
|
||||
file_url = response.file.url
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if file_url.startswith(self.source_url):
|
||||
file_url = file_url[len(self.source_url):]
|
||||
url = file_url
|
||||
file_name = ''
|
||||
if url is None and self.source_dir is not None:
|
||||
try:
|
||||
file_name = response.file.name
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if file_name.startswith(self.source_dir):
|
||||
file_name = os.path.relpath(file_name, self.source_dir)
|
||||
url = file_name.replace(os.path.sep, '/')
|
||||
if url is None:
|
||||
message = ("""Couldn't capture/convert file attributes into a """
|
||||
"""redirection. """
|
||||
"""``source_url`` is "%(source_url)s", """
|
||||
"""file's URL is "%(file_url)s". """
|
||||
"""``source_dir`` is "%(source_dir)s", """
|
||||
"""file's name is "%(file_name)s". """
|
||||
% {'source_url': self.source_url,
|
||||
'file_url': file_url,
|
||||
'source_dir': self.source_dir,
|
||||
'file_name': file_name})
|
||||
raise ImproperlyConfigured(message)
|
||||
return '/'.join((self.destination_url.rstrip('/'), url.lstrip('/')))
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Replace DownloadResponse instances by NginxDownloadResponse ones."""
|
||||
redirect_url = self.get_redirect_url(response)
|
||||
if self.expires:
|
||||
expires = self.expires
|
||||
else:
|
||||
try:
|
||||
expires = response.expires
|
||||
except AttributeError:
|
||||
expires = None
|
||||
return XAccelRedirectResponse(redirect_url=redirect_url,
|
||||
content_type=response['Content-Type'],
|
||||
basename=response.basename,
|
||||
expires=expires,
|
||||
with_buffering=self.with_buffering,
|
||||
limit_rate=self.limit_rate,
|
||||
attachment=response.attachment)
|
||||
|
||||
|
||||
class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware):
|
||||
"""Apply X-Accel-Redirect globally, via Django settings.
|
||||
|
||||
Available settings are:
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL:
|
||||
The string at the beginning of URLs to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then URLs aren't captured.
|
||||
Defaults to ``settings.MEDIA_URL``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR:
|
||||
The string at the beginning of filenames (path) to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then filenames aren't captured.
|
||||
Defaults to ``settings.MEDIA_ROOT``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL:
|
||||
The base URL where requests are proxied to.
|
||||
If ``None`` an ImproperlyConfigured exception is raised.
|
||||
|
||||
.. note::
|
||||
|
||||
The following settings are deprecated since version 1.1.
|
||||
URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT"
|
||||
and "MEDIA_URL" became too confuse.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
"""Use Django settings as configuration."""
|
||||
if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None:
|
||||
raise ImproperlyConfigured(
|
||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is '
|
||||
'required by %s middleware' % self.__class__.__name__)
|
||||
super(XAccelRedirectMiddleware, self).__init__(
|
||||
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
||||
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
||||
destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL,
|
||||
expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES,
|
||||
with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING,
|
||||
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE)
|
||||
|
||||
|
||||
#: Apply BaseXAccelRedirectMiddleware to ``view_func`` response.
|
||||
#:
|
||||
#: Proxies additional arguments (``*args``, ``**kwargs``) to
|
||||
#: :py:class:`BaseXAccelRedirectMiddleware` constructor (``expires``,
|
||||
#: ``with_buffering``, and ``limit_rate``).
|
||||
x_accel_redirect = DownloadDecorator(BaseXAccelRedirectMiddleware)
|
||||
14
django_downloadview/nginx/__init__.py
Normal file
14
django_downloadview/nginx/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Optimizations for Nginx.
|
||||
|
||||
See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||
:doc:`narrative documentation about Nginx optimizations
|
||||
</optimizations/nginx>`.
|
||||
|
||||
"""
|
||||
# API shortcuts.
|
||||
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
||||
from django_downloadview.nginx.response import XAccelRedirectResponse # NoQA
|
||||
from django_downloadview.nginx.tests import assert_x_accel_redirect # NoQA
|
||||
from django_downloadview.nginx.middlewares import ( # NoQA
|
||||
XAccelRedirectMiddleware)
|
||||
16
django_downloadview/nginx/decorators.py
Normal file
16
django_downloadview/nginx/decorators.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Decorators to apply Nginx X-Accel on a specific view."""
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
||||
|
||||
|
||||
def x_accel_redirect(view_func, *args, **kwargs):
|
||||
"""Apply
|
||||
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware` to
|
||||
``view_func``.
|
||||
|
||||
Proxies (``*args``, ``**kwargs``) to middleware constructor.
|
||||
|
||||
"""
|
||||
decorator = DownloadDecorator(XAccelRedirectMiddleware)
|
||||
return decorator(view_func, *args, **kwargs)
|
||||
120
django_downloadview/nginx/middlewares.py
Normal file
120
django_downloadview/nginx/middlewares.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from django_downloadview.middlewares import (ProxiedDownloadMiddleware,
|
||||
NoRedirectionMatch)
|
||||
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||
|
||||
|
||||
class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
||||
"""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
|
||||
use this class as the factory in
|
||||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None,
|
||||
expires=None, with_buffering=None, limit_rate=None,
|
||||
media_root=None, media_url=None):
|
||||
"""Constructor."""
|
||||
if media_url is not None:
|
||||
warnings.warn("%s ``media_url`` is deprecated. Use "
|
||||
"``destination_url`` instead."
|
||||
% self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if destination_url is None:
|
||||
destination_url = media_url
|
||||
else:
|
||||
destination_url = destination_url
|
||||
else:
|
||||
destination_url = destination_url
|
||||
if media_root is not None:
|
||||
warnings.warn("%s ``media_root`` is deprecated. Use "
|
||||
"``source_dir`` instead." % self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if source_dir is None:
|
||||
source_dir = media_root
|
||||
else:
|
||||
source_dir = source_dir
|
||||
else:
|
||||
source_dir = source_dir
|
||||
super(XAccelRedirectMiddleware, self).__init__(source_dir,
|
||||
source_url,
|
||||
destination_url)
|
||||
self.expires = expires
|
||||
self.with_buffering = with_buffering
|
||||
self.limit_rate = limit_rate
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Replace DownloadResponse instances by NginxDownloadResponse ones."""
|
||||
try:
|
||||
redirect_url = self.get_redirect_url(response)
|
||||
except NoRedirectionMatch:
|
||||
return response
|
||||
if self.expires:
|
||||
expires = self.expires
|
||||
else:
|
||||
try:
|
||||
expires = response.expires
|
||||
except AttributeError:
|
||||
expires = None
|
||||
return XAccelRedirectResponse(redirect_url=redirect_url,
|
||||
content_type=response['Content-Type'],
|
||||
basename=response.basename,
|
||||
expires=expires,
|
||||
with_buffering=self.with_buffering,
|
||||
limit_rate=self.limit_rate,
|
||||
attachment=response.attachment)
|
||||
|
||||
|
||||
class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware):
|
||||
"""Apply X-Accel-Redirect globally, via Django settings.
|
||||
|
||||
Available settings are:
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL:
|
||||
The string at the beginning of URLs to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then URLs aren't captured.
|
||||
Defaults to ``settings.MEDIA_URL``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR:
|
||||
The string at the beginning of filenames (path) to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then filenames aren't captured.
|
||||
Defaults to ``settings.MEDIA_ROOT``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL:
|
||||
The base URL where requests are proxied to.
|
||||
If ``None`` an ImproperlyConfigured exception is raised.
|
||||
|
||||
.. note::
|
||||
|
||||
The following settings are deprecated since version 1.1.
|
||||
URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT"
|
||||
and "MEDIA_URL" became too confuse.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
"""Use Django settings as configuration."""
|
||||
if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None:
|
||||
raise ImproperlyConfigured(
|
||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is '
|
||||
'required by %s middleware' % self.__class__.__name__)
|
||||
super(SingleXAccelRedirectMiddleware, self).__init__(
|
||||
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
||||
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
||||
destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL,
|
||||
expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES,
|
||||
with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING,
|
||||
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE)
|
||||
34
django_downloadview/nginx/response.py
Normal file
34
django_downloadview/nginx/response.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Nginx's specific responses."""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from django_downloadview.response import ProxiedDownloadResponse
|
||||
from django_downloadview.utils import content_type_to_charset, url_basename
|
||||
|
||||
|
||||
class XAccelRedirectResponse(ProxiedDownloadResponse):
|
||||
"Http response that delegates serving file to Nginx via X-Accel headers."
|
||||
def __init__(self, redirect_url, content_type, basename=None, expires=None,
|
||||
with_buffering=None, limit_rate=None, attachment=True):
|
||||
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
||||
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
|
||||
if attachment:
|
||||
self.basename = basename or url_basename(redirect_url,
|
||||
content_type)
|
||||
self['Content-Disposition'] = 'attachment; filename={name}'.format(
|
||||
name=self.basename)
|
||||
self['X-Accel-Redirect'] = redirect_url
|
||||
self['X-Accel-Charset'] = content_type_to_charset(content_type)
|
||||
if with_buffering is not None:
|
||||
self['X-Accel-Buffering'] = with_buffering and 'yes' or 'no'
|
||||
if expires:
|
||||
expire_seconds = timedelta(expires - now()).seconds
|
||||
self['X-Accel-Expires'] = expire_seconds
|
||||
elif expires is not None: # We explicitely want it off.
|
||||
self['X-Accel-Expires'] = 'off'
|
||||
if limit_rate is not None:
|
||||
self['X-Accel-Limit-Rate'] = (limit_rate
|
||||
and '%d' % limit_rate
|
||||
or 'off')
|
||||
147
django_downloadview/nginx/settings.py
Normal file
147
django_downloadview/nginx/settings.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Django settings around Nginx X-Accel.
|
||||
|
||||
.. warning::
|
||||
|
||||
These settings are deprecated since version 1.3. You can now provide custom
|
||||
configuration via `DOWNLOADVIEW_MIDDLEWARES` setting. See :doc:`/settings`
|
||||
for details.
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
# In version 1.3, former XAccelRedirectMiddleware has been renamed to
|
||||
# SingleXAccelRedirectMiddleware. So tell the users.
|
||||
middleware = 'django_downloadview.nginx.XAccelRedirectMiddleware'
|
||||
if middleware in settings.MIDDLEWARE_CLASSES:
|
||||
raise ImproperlyConfigured(
|
||||
'{middleware} middleware has been renamed as of django-downloadview '
|
||||
'version 1.3. You may use '
|
||||
'"django_downloadview.nginx.SingleXAccelRedirectMiddleware" instead, '
|
||||
'or upgrade to "django_downloadview.DownloadDispatcherMiddleware". ')
|
||||
|
||||
|
||||
#: 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
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_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
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_LIMIT_RATE)
|
||||
|
||||
|
||||
#: 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
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_EXPIRES)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR.
|
||||
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_SOURCE_DIR)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL.
|
||||
DEFAULT_SOURCE_URL = settings.MEDIA_URL
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_SOURCE_URL)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL.
|
||||
DEFAULT_DESTINATION_URL = None
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
||||
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL'
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn('settings.{deprecated} is deprecated. You should combine '
|
||||
'"django_downloadview.DownloadDispatcherMiddleware" with '
|
||||
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
|
||||
deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_DESTINATION_URL)
|
||||
119
django_downloadview/nginx/tests.py
Normal file
119
django_downloadview/nginx/tests.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||
|
||||
|
||||
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_attachment(self, test_case, response, value):
|
||||
header = 'Content-Disposition'
|
||||
if value:
|
||||
test_case.assertTrue(response[header].startswith('attachment'))
|
||||
else:
|
||||
test_case.assertFalse(header in response)
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -1,48 +1,71 @@
|
|||
"""HttpResponse subclasses."""
|
||||
# -*- coding: utf-8 -*-
|
||||
""":py:class:`django.http.HttpResponse` subclasses."""
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
|
||||
|
||||
class DownloadResponse(StreamingHttpResponse):
|
||||
"""File download response.
|
||||
"""File download response (Django serves file, client downloads it).
|
||||
|
||||
``content`` attribute is supposed to be a file object wrapper, which makes
|
||||
this response "lazy".
|
||||
This is a specialization of :class:`django.http.StreamingHttpResponse`
|
||||
where :attr:`~django.http.StreamingHttpResponse.streaming_content` is a
|
||||
file wrapper.
|
||||
|
||||
Constructor differs a bit from :class:`~django.http.response.HttpResponse`:
|
||||
|
||||
``file_instance``
|
||||
A :doc:`file wrapper instance </files>`, such as
|
||||
:class:`~django.core.files.base.File`.
|
||||
|
||||
``attachement``
|
||||
Boolean. Whether to return the file as attachment or not.
|
||||
Affects ``Content-Disposition`` header.
|
||||
|
||||
``basename``
|
||||
Unicode. Client-side name of the file to stream.
|
||||
Only used if ``attachment`` is ``True``.
|
||||
Affects ``Content-Disposition`` header.
|
||||
|
||||
``status``
|
||||
HTTP status code.
|
||||
|
||||
``content_type``
|
||||
Value for ``Content-Type`` header.
|
||||
If ``None``, then mime-type and encoding will be populated by the
|
||||
response (default implementation uses mimetypes, based on file
|
||||
name).
|
||||
|
||||
|
||||
Here are some highlights to understand internal mechanisms and motivations:
|
||||
|
||||
* Let's start by quoting :pep:`3333` (WSGI specification):
|
||||
|
||||
For large files, or for specialized uses of HTTP streaming,
|
||||
applications will usually return an iterator (often a
|
||||
generator-iterator) that produces the output in a block-by-block
|
||||
fashion.
|
||||
|
||||
* `Django WSGI handler (application implementation) return response object
|
||||
<https://github.com/django/django/blob/fd1279a44df3b9a837453cd79fd0fbcf81bae39d/django/core/handlers/wsgi.py#L268>`_.
|
||||
|
||||
* :class:`django.http.HttpResponse` and subclasses are iterators.
|
||||
|
||||
* In :class:`~django.http.StreamingHttpResponse`, the
|
||||
:meth:`~container.__iter__` implementation proxies to
|
||||
:attr:`~django.http.StreamingHttpResponse.streaming_content`.
|
||||
|
||||
* In :class:`DownloadResponse` and subclasses, :attr:`streaming_content`
|
||||
is a :doc:`file wrapper </files>`. File wrapper is itself an iterator
|
||||
over actual file content, and it also encapsulates access to file
|
||||
attributes (size, name, ...).
|
||||
|
||||
"""
|
||||
def __init__(self, file_instance, attachment=True, basename=None,
|
||||
status=200, content_type=None):
|
||||
"""Constructor.
|
||||
|
||||
It differs a bit from HttpResponse constructor.
|
||||
|
||||
file_instance:
|
||||
A file wrapper object. Could be a FieldFile.
|
||||
|
||||
attachement:
|
||||
Boolean, whether to return the file as attachment or not. Affects
|
||||
"Content-Disposition" header.
|
||||
Defaults to ``True``.
|
||||
|
||||
basename:
|
||||
Unicode. Only used if ``attachment`` is ``True``. Client-side name
|
||||
of the file to stream. Affects "Content-Disposition" header.
|
||||
Defaults to basename(``file_instance.name``).
|
||||
|
||||
status:
|
||||
HTTP status code.
|
||||
Defaults to 200.
|
||||
|
||||
content_type:
|
||||
Value for "Content-Type" header.
|
||||
If ``None``, then mime-type and encoding will be populated by the
|
||||
response (default implementation uses mimetypes, based on file name).
|
||||
Defaults is ``None``.
|
||||
|
||||
"""
|
||||
"""Constructor."""
|
||||
self.file = file_instance
|
||||
super(DownloadResponse, self).__init__(streaming_content=self.file,
|
||||
status=status,
|
||||
|
|
@ -69,7 +92,10 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
except AttributeError:
|
||||
headers = {}
|
||||
headers['Content-Type'] = self.get_content_type()
|
||||
headers['Content-Length'] = self.file.size
|
||||
try:
|
||||
headers['Content-Length'] = self.file.size
|
||||
except (AttributeError, NotImplementedError):
|
||||
pass # Generated files.
|
||||
if self.attachment:
|
||||
headers['Content-Disposition'] = 'attachment; filename=%s' \
|
||||
% self.get_basename()
|
||||
|
|
@ -120,11 +146,11 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
return settings.DEFAULT_CHARSET
|
||||
|
||||
|
||||
def is_download_response(response):
|
||||
"""Return ``True`` if ``response`` is a download response.
|
||||
class ProxiedDownloadResponse(HttpResponse):
|
||||
"""Base class for internal redirect download responses.
|
||||
|
||||
Current implementation returns True if ``response`` is an instance of
|
||||
:py:class:`django_downloadview.response.DownloadResponse`.
|
||||
This base class makes it possible to identify several types of specific
|
||||
responses such as
|
||||
:py:class:`~django_downloadview.nginx.response.XAccelRedirectResponse`.
|
||||
|
||||
"""
|
||||
return isinstance(response, DownloadResponse)
|
||||
|
|
|
|||
23
django_downloadview/sendfile.py
Normal file
23
django_downloadview/sendfile.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Port of django-sendfile in django-downloadview."""
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
from django_downloadview.views.storage import StorageDownloadView
|
||||
|
||||
|
||||
def sendfile(request, filename, attachment=False, attachment_filename=None,
|
||||
mimetype=None, encoding=None):
|
||||
"""Port of django-sendfile's API in django-downloadview.
|
||||
|
||||
Instantiates a :class:`~django.core.files.storage.FileSystemStorage` with
|
||||
``settings.SENDFILE_ROOT`` as root folder. Then uses
|
||||
:class:`StorageDownloadView` to stream the file by ``filename``.
|
||||
|
||||
"""
|
||||
storage = FileSystemStorage(location=settings.SENDFILE_ROOT)
|
||||
view = StorageDownloadView().as_view(storage=storage,
|
||||
path=filename,
|
||||
attachment=attachment,
|
||||
basename=attachment_filename)
|
||||
return view(request)
|
||||
|
|
@ -5,11 +5,43 @@ import tempfile
|
|||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from django_downloadview.response import is_download_response
|
||||
from django_downloadview.middlewares import is_download_response
|
||||
|
||||
|
||||
def setup_view(view, request, *args, **kwargs):
|
||||
"""Mimic ``as_view()``, but returns view instance.
|
||||
|
||||
Use this function to get view instances on which you can run unit tests,
|
||||
by testing specific methods.
|
||||
|
||||
This is an early implementation of
|
||||
https://code.djangoproject.com/ticket/20456
|
||||
|
||||
``view``
|
||||
A view instance, such as ``TemplateView(template_name='dummy.html')``.
|
||||
Initialization arguments are the same you would pass to ``as_view()``.
|
||||
|
||||
``request``
|
||||
A request object, typically built with
|
||||
:class:`~django.test.client.RequestFactory`.
|
||||
|
||||
``args`` and ``kwargs``
|
||||
"URLconf" positional and keyword arguments, the same you would pass to
|
||||
:func:`~django.core.urlresolvers.reverse`.
|
||||
|
||||
"""
|
||||
view.request = request
|
||||
view.args = args
|
||||
view.kwargs = kwargs
|
||||
return view
|
||||
|
||||
|
||||
class temporary_media_root(override_settings):
|
||||
"""Context manager or decorator to override settings.MEDIA_ROOT.
|
||||
"""Temporarily override settings.MEDIA_ROOT with a temporary directory.
|
||||
|
||||
The temporary directory is automatically created and destroyed.
|
||||
|
||||
Use this function as a context manager:
|
||||
|
||||
>>> from django_downloadview.test import temporary_media_root
|
||||
>>> from django.conf import settings
|
||||
|
|
@ -20,6 +52,8 @@ class temporary_media_root(override_settings):
|
|||
>>> global_media_root == settings.MEDIA_ROOT
|
||||
True
|
||||
|
||||
Or as a decorator:
|
||||
|
||||
>>> @temporary_media_root()
|
||||
... def use_temporary_media_root():
|
||||
... return settings.MEDIA_ROOT
|
||||
|
|
@ -73,9 +107,10 @@ class DownloadResponseValidator(object):
|
|||
test_case.assertTrue(is_download_response(response))
|
||||
|
||||
def assert_basename(self, test_case, response, value):
|
||||
test_case.assertEqual(response.basename, value)
|
||||
test_case.assertTrue('filename={name}'.format(name=response.basename),
|
||||
value)
|
||||
"""Implies ``attachement is True``."""
|
||||
test_case.assertTrue(
|
||||
response['Content-Disposition'].endswith(
|
||||
'filename={name}'.format(name=value)))
|
||||
|
||||
def assert_content_type(self, test_case, response, value):
|
||||
test_case.assertEqual(response['Content-Type'], value)
|
||||
|
|
@ -84,7 +119,6 @@ class DownloadResponseValidator(object):
|
|||
test_case.assertTrue(response['Content-Type'].startswith(value))
|
||||
|
||||
def assert_content(self, test_case, response, value):
|
||||
test_case.assertEqual(response.file.read(), value)
|
||||
test_case.assertEqual(''.join(response.streaming_content), value)
|
||||
|
||||
def assert_attachment(self, test_case, response, value):
|
||||
|
|
@ -93,7 +127,7 @@ class DownloadResponseValidator(object):
|
|||
|
||||
|
||||
def assert_download_response(test_case, response, **assertions):
|
||||
"""Make ``test_case`` assert that ``response`` is a DownloadResponse.
|
||||
"""Make ``test_case`` assert that ``response`` meets ``assertions``.
|
||||
|
||||
Optional ``assertions`` dictionary can be used to check additional items:
|
||||
|
||||
|
|
@ -101,9 +135,12 @@ def assert_download_response(test_case, response, **assertions):
|
|||
|
||||
* ``content_type``: the value of "Content-Type" header.
|
||||
|
||||
* ``charset``: the value of ``X-Accel-Charset`` header.
|
||||
* ``mime_type``: the MIME type part of "Content-Type" header (without
|
||||
charset).
|
||||
|
||||
* ``content``: the content of the file to be downloaded.
|
||||
* ``content``: the contents of the file.
|
||||
|
||||
* ``attachment``: whether the file is returned as attachment or not.
|
||||
|
||||
"""
|
||||
validator = DownloadResponseValidator()
|
||||
|
|
|
|||
2
django_downloadview/tests/__init__.py
Normal file
2
django_downloadview/tests/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests."""
|
||||
117
django_downloadview/tests/api.py
Normal file
117
django_downloadview/tests/api.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Test suite around :mod:`django_downloadview.api` and deprecation plan."""
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import django.test
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
|
||||
class APITestCase(unittest.TestCase):
|
||||
"""Make sure django_downloadview exposes API."""
|
||||
def assert_module_attributes(self, module_path, attribute_names):
|
||||
"""Assert imported ``module_path`` has ``attribute_names``."""
|
||||
module = import_module(module_path)
|
||||
missing_attributes = []
|
||||
for attribute_name in attribute_names:
|
||||
if not hasattr(module, attribute_name):
|
||||
missing_attributes.append(attribute_name)
|
||||
if missing_attributes:
|
||||
self.fail('Missing attributes in "{module}": {attributes}'.format(
|
||||
module=module_path, attributes=', '.join(missing_attributes)))
|
||||
|
||||
def test_root_attributes(self):
|
||||
"""API is exposed in django_downloadview root package.
|
||||
|
||||
The goal of this test is to make sure that main items of project's API
|
||||
are easy to import... and prevent refactoring from breaking main API.
|
||||
|
||||
If this test is broken by refactoring, a :class:`DeprecationWarning` or
|
||||
simimar should be raised.
|
||||
|
||||
"""
|
||||
api = [
|
||||
# Views:
|
||||
'ObjectDownloadView',
|
||||
'StorageDownloadView',
|
||||
'PathDownloadView',
|
||||
'HTTPDownloadView',
|
||||
'VirtualDownloadView',
|
||||
'BaseDownloadView',
|
||||
'DownloadMixin',
|
||||
# File wrappers:
|
||||
'File',
|
||||
'StorageFile',
|
||||
'HTTPFile',
|
||||
'VirtualFile',
|
||||
# Responses:
|
||||
'DownloadResponse',
|
||||
'ProxiedDownloadResponse',
|
||||
# Middlewares:
|
||||
'BaseDownloadMiddleware',
|
||||
'DownloadDispatcherMiddleware',
|
||||
# Testing:
|
||||
'assert_download_response',
|
||||
'setup_view',
|
||||
'temporary_media_root',
|
||||
# Utilities:
|
||||
'StringIteratorIO',
|
||||
'sendfile']
|
||||
self.assert_module_attributes('django_downloadview', api)
|
||||
|
||||
def test_nginx_attributes(self):
|
||||
"""Nginx-related API is exposed in django_downloadview.nginx."""
|
||||
api = [
|
||||
'XAccelRedirectResponse',
|
||||
'XAccelRedirectMiddleware',
|
||||
'x_accel_redirect',
|
||||
'assert_x_accel_redirect']
|
||||
self.assert_module_attributes('django_downloadview.nginx', api)
|
||||
|
||||
|
||||
class DeprecatedAPITestCase(django.test.SimpleTestCase):
|
||||
"""Make sure using deprecated items raise DeprecationWarning."""
|
||||
def test_nginx_x_accel_redirect_middleware(self):
|
||||
"XAccelRedirectMiddleware in settings triggers ImproperlyConfigured."
|
||||
with override_settings(
|
||||
MIDDLEWARE_CLASSES=[
|
||||
'django_downloadview.nginx.XAccelRedirectMiddleware']):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
import django_downloadview.nginx.settings
|
||||
reload(django_downloadview.nginx.settings)
|
||||
|
||||
def test_nginx_x_accel_redirect_global_settings(self):
|
||||
"""Global settings for Nginx middleware are deprecated."""
|
||||
settings_overrides = {
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING': True,
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE': 32,
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES': 3600,
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT': '/',
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR': '/',
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL': '/',
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL': '/',
|
||||
'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL': '/',
|
||||
}
|
||||
import django_downloadview.nginx.settings
|
||||
missed_warnings = []
|
||||
for setting_name, setting_value in settings_overrides.items():
|
||||
warnings.resetwarnings()
|
||||
warnings.simplefilter("always")
|
||||
with warnings.catch_warnings(record=True) as warning_list:
|
||||
with override_settings(**{setting_name: setting_value}):
|
||||
reload(django_downloadview.nginx.settings)
|
||||
caught = False
|
||||
for warning_item in warning_list:
|
||||
if warning_item.category == DeprecationWarning:
|
||||
if 'deprecated' in str(warning_item.message):
|
||||
if setting_name in str(warning_item.message):
|
||||
caught = True
|
||||
break
|
||||
if not caught:
|
||||
missed_warnings.append(setting_name)
|
||||
if missed_warnings:
|
||||
self.fail(
|
||||
'No DeprecationWarning raised about following settings: '
|
||||
'{settings}.'.format(settings=', '.join(missed_warnings)))
|
||||
179
django_downloadview/tests/views.py
Normal file
179
django_downloadview/tests/views.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests around views."""
|
||||
import unittest
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from django.http.response import HttpResponseNotModified
|
||||
import django.test
|
||||
|
||||
from django_downloadview.test import setup_view
|
||||
from django_downloadview.views import base
|
||||
|
||||
|
||||
class DownloadMixinTestCase(unittest.TestCase):
|
||||
"""Tests around :class:`django_downloadviews.views.base.DownloadMixin`."""
|
||||
def test_get_file(self):
|
||||
"""DownloadMixin.get_file() raise NotImplementedError.
|
||||
|
||||
Subclasses must implement it!
|
||||
|
||||
"""
|
||||
mixin = base.DownloadMixin()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
mixin.get_file()
|
||||
|
||||
def test_get_basename(self):
|
||||
"""DownloadMixin.get_basename() returns basename attribute."""
|
||||
mixin = base.DownloadMixin()
|
||||
self.assertEqual(mixin.get_basename(), None)
|
||||
mixin.basename = 'fake'
|
||||
self.assertEqual(mixin.get_basename(), 'fake')
|
||||
|
||||
def test_was_modified_since_file(self):
|
||||
"""DownloadMixin.was_modified_since() tries (1) file's implementation.
|
||||
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
first tries to delegate computations to file wrapper's implementation.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = base.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
file_wrapper.was_modified_since.assertCalledOnceWith(
|
||||
mock.sentinel.since)
|
||||
|
||||
def test_was_modified_since_django(self):
|
||||
"""DownloadMixin.was_modified_since() tries (2) files attributes.
|
||||
|
||||
When calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``,
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
tries to pass file wrapper's ``size`` and ``modified_time`` to
|
||||
:func:`django.views.static import was_modified_since`.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=AttributeError)
|
||||
file_wrapper.size = mock.sentinel.size
|
||||
file_wrapper.modified_time = mock.sentinel.modified_time
|
||||
was_modified_since_mock = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = base.DownloadMixin()
|
||||
with mock.patch('django_downloadview.views.base.was_modified_since',
|
||||
new=was_modified_since_mock):
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
was_modified_since_mock.assertCalledOnceWith(
|
||||
mock.sentinel.size,
|
||||
mock.sentinel.modified_time)
|
||||
|
||||
def test_was_modified_since_fallback(self):
|
||||
"""DownloadMixin.was_modified_since() fallbacks to `True`.
|
||||
|
||||
When:
|
||||
|
||||
* calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``;
|
||||
|
||||
* and accessing ``size`` and ``modified_time`` from file wrapper raises
|
||||
``NotImplementedError`` or ``AttributeError``...
|
||||
|
||||
... then
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
returns ``True``.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=NotImplementedError)
|
||||
type(file_wrapper).modified_time = mock.PropertyMock(
|
||||
side_effect=NotImplementedError)
|
||||
mixin = base.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, 'fake since'),
|
||||
True)
|
||||
|
||||
def test_not_modified_response(self):
|
||||
"DownloadMixin.not_modified_response returns HttpResponseNotModified."
|
||||
mixin = base.DownloadMixin()
|
||||
response = mixin.not_modified_response()
|
||||
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||
|
||||
def test_download_response(self):
|
||||
"DownloadMixin.download_response() returns download response instance."
|
||||
mixin = base.DownloadMixin()
|
||||
mixin.file_instance = mock.sentinel.file_wrapper
|
||||
response_factory = mock.Mock(return_value=mock.sentinel.response)
|
||||
mixin.response_class = response_factory
|
||||
response_kwargs = {'dummy': 'value',
|
||||
'file_instance': mock.sentinel.file_wrapper,
|
||||
'attachment': True,
|
||||
'basename': None}
|
||||
response = mixin.download_response(**response_kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
response_factory.assert_called_once_with(**response_kwargs) # Not args
|
||||
|
||||
def test_render_to_response_not_modified(self):
|
||||
"""DownloadMixin.render_to_response() respects HTTP_IF_MODIFIED_SINCE
|
||||
header (calls ``not_modified_response()``)."""
|
||||
# Setup.
|
||||
mixin = base.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=mock.sentinel.http_if_modified_since)
|
||||
mixin.was_modified_since = mock.Mock(return_value=False)
|
||||
mixin.not_modified_response = mock.Mock(
|
||||
return_value=mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
mixin.was_modified_since.assert_called_once_with(
|
||||
mock.sentinel.file_wrapper,
|
||||
mock.sentinel.http_if_modified_since)
|
||||
mixin.not_modified_response.assert_called_once_with()
|
||||
|
||||
def test_render_to_response_modified(self):
|
||||
"""DownloadMixin.render_to_response() calls download_response()."""
|
||||
# Setup.
|
||||
mixin = base.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=None)
|
||||
mixin.was_modified_since = mock.Mock()
|
||||
mixin.download_response = mock.Mock(
|
||||
return_value=mock.sentinel.download_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.download_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
self.assertEqual(mixin.was_modified_since.call_count, 0)
|
||||
mixin.download_response.assert_called_once_with()
|
||||
|
||||
|
||||
class BaseDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests around :class:`django_downloadviews.views.base.BaseDownloadView`."
|
||||
def test_get(self):
|
||||
"""BaseDownloadView.get() calls render_to_response()."""
|
||||
request = django.test.RequestFactory().get('/dummy-url')
|
||||
args = ['dummy-arg']
|
||||
kwargs = {'dummy': 'kwarg'}
|
||||
view = setup_view(base.BaseDownloadView(), request, *args, **kwargs)
|
||||
view.render_to_response = mock.Mock(
|
||||
return_value=mock.sentinel.response)
|
||||
response = view.get(request, *args, **kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
view.render_to_response.assert_called_once_with()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
"""Utility functions."""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Utility functions that may be implemented in external packages."""
|
||||
import re
|
||||
|
||||
|
||||
|
|
@ -16,3 +17,17 @@ def content_type_to_charset(content_type):
|
|||
match = re.search(charset_pattern, content_type)
|
||||
if match:
|
||||
return match.group('charset')
|
||||
|
||||
|
||||
def url_basename(url, content_type):
|
||||
"""Return best-guess basename from URL and content-type.
|
||||
|
||||
>>> from django_downloadview.utils import url_basename
|
||||
|
||||
If URL contains extension, it is kept as-is.
|
||||
|
||||
>>> url_basename(u'/path/to/somefile.rst', 'text/plain')
|
||||
u'somefile.rst'
|
||||
|
||||
"""
|
||||
return url.split('/')[-1]
|
||||
|
|
|
|||
|
|
@ -1,286 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""Views."""
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import DefaultStorage
|
||||
from django.http import HttpResponseNotModified
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.detail import BaseDetailView
|
||||
from django.views.static import was_modified_since
|
||||
|
||||
import requests
|
||||
|
||||
from django_downloadview import files
|
||||
from django_downloadview.response import DownloadResponse
|
||||
|
||||
|
||||
class DownloadMixin(object):
|
||||
"""Placeholders and base implementation to create file download views.
|
||||
|
||||
.. note::
|
||||
|
||||
This class does not inherit from
|
||||
:py:class:`django.views.generic.base.View`.
|
||||
|
||||
The :py:meth:`get_file` method is a placeholder subclasses must implement.
|
||||
Base implementation raises ``NotImplementedError``.
|
||||
|
||||
Other methods provide a base implementation that use the file wrapper
|
||||
returned by :py:meth:`get_file`.
|
||||
|
||||
"""
|
||||
#: Response class, to be used in :py:meth:`render_to_response`.
|
||||
response_class = DownloadResponse
|
||||
|
||||
#: Whether to return the response as attachment or not.
|
||||
attachment = True
|
||||
|
||||
#: Client-side filename, if only file is returned as attachment.
|
||||
basename = None
|
||||
|
||||
def get_file(self):
|
||||
"""Return a file wrapper instance."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_basename(self):
|
||||
return self.basename
|
||||
|
||||
def was_modified_since(self, file_instance, since):
|
||||
"""Return True if ``file_instance`` was modified after ``since``.
|
||||
|
||||
Uses file wrapper's ``was_modified_since`` if available, with value of
|
||||
``since`` as positional argument.
|
||||
|
||||
Else, fallbacks to default implementation, which uses
|
||||
:py:func:`django.views.static.was_modified_since`.
|
||||
|
||||
Django's ``was_modified_since`` function needs a datetime and a size.
|
||||
It is passed ``modified_time`` and ``size`` attributes from file
|
||||
wrapper. If file wrapper does not support these attributes
|
||||
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
||||
the file is considered as modified and ``True`` is returned.
|
||||
|
||||
"""
|
||||
try:
|
||||
return file_instance.was_modified_since(since)
|
||||
except (AttributeError, NotImplementedError):
|
||||
try:
|
||||
modification_time = file_instance.modified_time
|
||||
size = file_instance.size
|
||||
except (AttributeError, NotImplementedError):
|
||||
return True
|
||||
else:
|
||||
return was_modified_since(since, modification_time, size)
|
||||
|
||||
def not_modified_response(self, *args, **kwargs):
|
||||
"""Return :py:class:`django.http.HttpResponseNotModified` instance."""
|
||||
content_type = self.file_instance.content_type
|
||||
return HttpResponseNotModified(content_type=content_type)
|
||||
|
||||
def download_response(self, *args, **kwargs):
|
||||
"""Return :py:class:`DownloadResponse` instance."""
|
||||
response_kwargs = {'file_instance': self.file_instance,
|
||||
'attachment': self.attachment,
|
||||
'basename': self.get_basename()}
|
||||
response_kwargs.update(kwargs)
|
||||
response = self.response_class(**response_kwargs)
|
||||
return response
|
||||
|
||||
def render_to_response(self, *args, **kwargs):
|
||||
"""Return a download response.
|
||||
|
||||
Respects the "HTTP_IF_MODIFIED_SINCE" header if any. In that case, uses
|
||||
:py:meth:`was_modified_since` and :py:meth:`not_modified_response`.
|
||||
|
||||
Else, uses :py:meth:`download_response` to return a download response.
|
||||
|
||||
"""
|
||||
self.file_instance = self.get_file()
|
||||
# Respect the If-Modified-Since header.
|
||||
since = self.request.META.get('HTTP_IF_MODIFIED_SINCE', None)
|
||||
if since is not None:
|
||||
if not self.was_modified_since(self.file_instance, since):
|
||||
return self.not_modified_response(*args, **kwargs)
|
||||
# Return download response.
|
||||
return self.download_response(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseDownloadView(DownloadMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
|
||||
|
||||
class PathDownloadView(BaseDownloadView):
|
||||
"""Serve a file using filename."""
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
#: Filename is supposed to be an absolute filename of a file located on the
|
||||
#: local filesystem.
|
||||
path = None
|
||||
|
||||
#: Name of the URL argument that contains path.
|
||||
path_url_kwarg = 'path'
|
||||
|
||||
def get_path(self):
|
||||
"""Return actual path of the file to serve.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path`.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
As an example, :py:attr:`path` could be relative and your custom
|
||||
:py:meth:`get_path` implementation makes it absolute.
|
||||
|
||||
"""
|
||||
return self.kwargs.get(self.path_url_kwarg, self.path)
|
||||
|
||||
def get_file(self):
|
||||
"""Use path to return wrapper around file to serve."""
|
||||
return File(open(self.get_path()))
|
||||
|
||||
|
||||
class StorageDownloadView(PathDownloadView):
|
||||
"""Serve a file using storage and filename."""
|
||||
#: Storage the file to serve belongs to.
|
||||
storage = DefaultStorage()
|
||||
|
||||
#: Path to the file to serve relative to storage.
|
||||
path = None # Override docstring.
|
||||
|
||||
def get_path(self):
|
||||
"""Return path of the file to serve, relative to storage.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path`.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
|
||||
"""
|
||||
return super(StorageDownloadView, self).get_path()
|
||||
|
||||
def get_file(self):
|
||||
"""Use path and storage to return wrapper around file to serve."""
|
||||
return files.StorageFile(self.storage, self.get_path())
|
||||
|
||||
|
||||
class VirtualDownloadView(BaseDownloadView):
|
||||
"""Serve not-on-disk or generated-on-the-fly file.
|
||||
|
||||
Use this class to serve :py:class:`StringIO` files.
|
||||
|
||||
Override the :py:meth:`get_file` method to customize file wrapper.
|
||||
|
||||
"""
|
||||
def get_file(self):
|
||||
"""Return wrapper."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def was_modified_since(self, file_instance, since):
|
||||
"""Delegate to file wrapper's was_modified_since, or return True.
|
||||
|
||||
This is the implementation of an edge case: when files are generated
|
||||
on the fly, we cannot guess whether they have been modified or not.
|
||||
If the file wrapper implements ``was_modified_since()`` method, then we
|
||||
trust it. Otherwise it is safer to suppose that the file has been
|
||||
modified.
|
||||
|
||||
This behaviour prevents file size to be computed on the Django side.
|
||||
Because computing file size means iterating over all the file contents,
|
||||
and we want to avoid that whenever possible. As an example, it could
|
||||
reduce all the benefits of working with dynamic file generators...
|
||||
which is a major feature of virtual files.
|
||||
|
||||
"""
|
||||
try:
|
||||
return file_instance.was_modified_since(since)
|
||||
except (AttributeError, NotImplementedError):
|
||||
return True
|
||||
|
||||
|
||||
class HTTPDownloadView(BaseDownloadView):
|
||||
"""Proxy files that live on remote servers."""
|
||||
#: URL to download (the one we are proxying).
|
||||
url = u''
|
||||
|
||||
#: Additional keyword arguments for request handler.
|
||||
request_kwargs = {}
|
||||
|
||||
def get_request_factory(self):
|
||||
"""Return request factory to perform actual HTTP request."""
|
||||
return requests.get
|
||||
|
||||
def get_request_kwargs(self):
|
||||
"""Return keyword arguments for use with request factory."""
|
||||
return self.request_kwargs
|
||||
|
||||
def get_url(self):
|
||||
"""Return remote file URL (the one we are proxying)."""
|
||||
return self.url
|
||||
|
||||
def get_file(self):
|
||||
"""Return wrapper which has an ``url`` attribute."""
|
||||
return files.HTTPFile(request_factory=self.get_request_factory(),
|
||||
name=self.get_basename(),
|
||||
url=self.get_url(),
|
||||
**self.get_request_kwargs())
|
||||
|
||||
|
||||
class ObjectDownloadView(DownloadMixin, BaseDetailView):
|
||||
"""Download view for models which contain a FileField.
|
||||
|
||||
This class extends BaseDetailView, so you can use its arguments to target
|
||||
the instance to operate on: slug, slug_kwarg, model, queryset...
|
||||
See Django's DetailView reference for details.
|
||||
|
||||
In addition to BaseDetailView arguments, you can set arguments related to
|
||||
the file to be downloaded.
|
||||
|
||||
The main one is ``file_field``.
|
||||
|
||||
The other arguments are provided for convenience, in case your model holds
|
||||
some (deserialized) metadata about the file, such as its basename, its
|
||||
modification time, its MIME type... These fields may be particularly handy
|
||||
if your file storage is not the local filesystem.
|
||||
|
||||
"""
|
||||
#: Name of the model's attribute which contains the file to be streamed.
|
||||
#: Typically the name of a FileField.
|
||||
file_field = 'file'
|
||||
|
||||
#: Optional name of the model's attribute which contains the basename.
|
||||
basename_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the encoding.
|
||||
encoding_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the MIME type.
|
||||
mime_type_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the charset.
|
||||
charset_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the modification
|
||||
# time.
|
||||
modification_time_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the size.
|
||||
size_field = None
|
||||
|
||||
def get_file(self):
|
||||
"""Return FieldFile instance."""
|
||||
file_instance = getattr(self.object, self.file_field)
|
||||
for field in ('encoding', 'mime_type', 'charset', 'modification_time',
|
||||
'size'):
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
value = getattr(self.object, model_field)
|
||||
setattr(file_instance, field, value)
|
||||
return file_instance
|
||||
|
||||
def get_basename(self):
|
||||
"""Return client-side filename."""
|
||||
basename = super(ObjectDownloadView, self).get_basename()
|
||||
if basename is None:
|
||||
field = 'basename'
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
basename = getattr(self.object, model_field)
|
||||
return basename
|
||||
12
django_downloadview/views/__init__.py
Normal file
12
django_downloadview/views/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# coding=utf-8
|
||||
"""Views."""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Views to stream files."""
|
||||
# API shortcuts.
|
||||
from django_downloadview.views.base import (DownloadMixin, # NoQA
|
||||
BaseDownloadView)
|
||||
from django_downloadview.views.path import PathDownloadView # NoQA
|
||||
from django_downloadview.views.storage import StorageDownloadView # NoQA
|
||||
from django_downloadview.views.object import ObjectDownloadView # NoQA
|
||||
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
||||
from django_downloadview.views.virtual import VirtualDownloadView # NoQA
|
||||
104
django_downloadview/views/base.py
Normal file
104
django_downloadview/views/base.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Base material for download views: :class:`DownloadMixin` and
|
||||
:class:`BaseDownloadView`"""
|
||||
from django.http import HttpResponseNotModified
|
||||
from django.views.generic.base import View
|
||||
from django.views.static import was_modified_since
|
||||
|
||||
from django_downloadview.response import DownloadResponse
|
||||
|
||||
|
||||
class DownloadMixin(object):
|
||||
"""Placeholders and base implementation to create file download views.
|
||||
|
||||
.. note::
|
||||
|
||||
This class does not inherit from
|
||||
:py:class:`django.views.generic.base.View`.
|
||||
|
||||
The :py:meth:`get_file` method is a placeholder subclasses must implement.
|
||||
Base implementation raises ``NotImplementedError``.
|
||||
|
||||
Other methods provide a base implementation that use the file wrapper
|
||||
returned by :py:meth:`get_file`.
|
||||
|
||||
"""
|
||||
#: Response class, to be used in :py:meth:`render_to_response`.
|
||||
response_class = DownloadResponse
|
||||
|
||||
#: Whether to return the response as attachment or not.
|
||||
attachment = True
|
||||
|
||||
#: Client-side filename, if only file is returned as attachment.
|
||||
basename = None
|
||||
|
||||
def get_file(self):
|
||||
"""Return a file wrapper instance."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_basename(self):
|
||||
return self.basename
|
||||
|
||||
def was_modified_since(self, file_instance, since):
|
||||
"""Return True if ``file_instance`` was modified after ``since``.
|
||||
|
||||
Uses file wrapper's ``was_modified_since`` if available, with value of
|
||||
``since`` as positional argument.
|
||||
|
||||
Else, fallbacks to default implementation, which uses
|
||||
:py:func:`django.views.static.was_modified_since`.
|
||||
|
||||
Django's ``was_modified_since`` function needs a datetime and a size.
|
||||
It is passed ``modified_time`` and ``size`` attributes from file
|
||||
wrapper. If file wrapper does not support these attributes
|
||||
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
||||
the file is considered as modified and ``True`` is returned.
|
||||
|
||||
"""
|
||||
try:
|
||||
return file_instance.was_modified_since(since)
|
||||
except (AttributeError, NotImplementedError):
|
||||
try:
|
||||
modification_time = file_instance.modified_time
|
||||
size = file_instance.size
|
||||
except (AttributeError, NotImplementedError):
|
||||
return True
|
||||
else:
|
||||
return was_modified_since(since, modification_time, size)
|
||||
|
||||
def not_modified_response(self, *response_args, **response_kwargs):
|
||||
"""Return :class:`django.http.HttpResponseNotModified` instance."""
|
||||
return HttpResponseNotModified(*response_args, **response_kwargs)
|
||||
|
||||
def download_response(self, *response_args, **response_kwargs):
|
||||
"""Return :class:`~django_downloadview.response.DownloadResponse`."""
|
||||
response_kwargs.setdefault('file_instance', self.file_instance)
|
||||
response_kwargs.setdefault('attachment', self.attachment)
|
||||
response_kwargs.setdefault('basename', self.get_basename())
|
||||
response = self.response_class(*response_args, **response_kwargs)
|
||||
return response
|
||||
|
||||
def render_to_response(self, *response_args, **response_kwargs):
|
||||
"""Return "download" response.
|
||||
|
||||
Respects the "HTTP_IF_MODIFIED_SINCE" header if any. In that case, uses
|
||||
:py:meth:`was_modified_since` and :py:meth:`not_modified_response`.
|
||||
|
||||
Else, uses :py:meth:`download_response` to return a download response.
|
||||
|
||||
"""
|
||||
self.file_instance = self.get_file()
|
||||
# Respect the If-Modified-Since header.
|
||||
since = self.request.META.get('HTTP_IF_MODIFIED_SINCE', None)
|
||||
if since is not None:
|
||||
if not self.was_modified_since(self.file_instance, since):
|
||||
return self.not_modified_response(**response_kwargs)
|
||||
# Return download response.
|
||||
return self.download_response(*response_args, **response_kwargs)
|
||||
|
||||
|
||||
class BaseDownloadView(DownloadMixin, View):
|
||||
"""A base :class:`DownloadMixin` that implements :meth:`get`."""
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
46
django_downloadview/views/http.py
Normal file
46
django_downloadview/views/http.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files given an URL, i.e. files you want to proxy."""
|
||||
import requests
|
||||
|
||||
from django_downloadview.files import HTTPFile
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
|
||||
class HTTPDownloadView(BaseDownloadView):
|
||||
"""Proxy files that live on remote servers."""
|
||||
#: URL to download (the one we are proxying).
|
||||
url = u''
|
||||
|
||||
#: Additional keyword arguments for request handler.
|
||||
request_kwargs = {}
|
||||
|
||||
def get_request_factory(self):
|
||||
"""Return request factory to perform actual HTTP request.
|
||||
|
||||
Default implementation returns :func:`requests.get` callable.
|
||||
|
||||
"""
|
||||
return requests.get
|
||||
|
||||
def get_request_kwargs(self):
|
||||
"""Return keyword arguments for use with :meth:`get_request_factory`.
|
||||
|
||||
Default implementation returns :attr:`request_kwargs`.
|
||||
|
||||
"""
|
||||
return self.request_kwargs
|
||||
|
||||
def get_url(self):
|
||||
"""Return remote file URL (the one we are proxying).
|
||||
|
||||
Default implementation returns :attr:`url`.
|
||||
|
||||
"""
|
||||
return self.url
|
||||
|
||||
def get_file(self):
|
||||
"""Return wrapper which has an ``url`` attribute."""
|
||||
return HTTPFile(request_factory=self.get_request_factory(),
|
||||
name=self.get_basename(),
|
||||
url=self.get_url(),
|
||||
**self.get_request_kwargs())
|
||||
82
django_downloadview/views/object.py
Normal file
82
django_downloadview/views/object.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files that live in models."""
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
|
||||
class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
||||
"""Serve file fields from models.
|
||||
|
||||
This class extends BaseDetailView, so you can use its arguments to target
|
||||
the instance to operate on: slug, slug_kwarg, model, queryset...
|
||||
See Django's DetailView reference for details.
|
||||
|
||||
In addition to BaseDetailView arguments, you can set arguments related to
|
||||
the file to be downloaded.
|
||||
|
||||
The main one is ``file_field``.
|
||||
|
||||
The other arguments are provided for convenience, in case your model holds
|
||||
some (deserialized) metadata about the file, such as its basename, its
|
||||
modification time, its MIME type... These fields may be particularly handy
|
||||
if your file storage is not the local filesystem.
|
||||
|
||||
"""
|
||||
#: Name of the model's attribute which contains the file to be streamed.
|
||||
#: Typically the name of a FileField.
|
||||
file_field = 'file'
|
||||
|
||||
#: Optional name of the model's attribute which contains the basename.
|
||||
basename_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the encoding.
|
||||
encoding_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the MIME type.
|
||||
mime_type_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the charset.
|
||||
charset_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the modification
|
||||
# time.
|
||||
modification_time_field = None
|
||||
|
||||
#: Optional name of the model's attribute which contains the size.
|
||||
size_field = None
|
||||
|
||||
def get_file(self):
|
||||
"""Return :class:`~django.db.models.fields.files.FieldFile` instance.
|
||||
|
||||
The file wrapper is model's field specified as :attr:`file_field`. It
|
||||
is typically a :class:`~django.db.models.fields.files.FieldFile` or
|
||||
subclass.
|
||||
|
||||
Additional attributes are set on the file wrapper if :attr:`encoding`,
|
||||
:attr:`mime_type`, :attr:`charset`, :attr:`modification_time` or
|
||||
:attr:`size` are configured.
|
||||
|
||||
"""
|
||||
file_instance = getattr(self.object, self.file_field)
|
||||
for field in ('encoding', 'mime_type', 'charset', 'modification_time',
|
||||
'size'):
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
value = getattr(self.object, model_field)
|
||||
setattr(file_instance, field, value)
|
||||
return file_instance
|
||||
|
||||
def get_basename(self):
|
||||
"""Return client-side filename."""
|
||||
basename = super(ObjectDownloadView, self).get_basename()
|
||||
if basename is None:
|
||||
field = 'basename'
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
basename = getattr(self.object, model_field)
|
||||
return basename
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super(ObjectDownloadView, self).get(request, *args, **kwargs)
|
||||
33
django_downloadview/views/path.py
Normal file
33
django_downloadview/views/path.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
""":class:`PathDownloadView`."""
|
||||
from django.core.files import File
|
||||
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
|
||||
class PathDownloadView(BaseDownloadView):
|
||||
"""Serve a file using filename."""
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
#: Filename is supposed to be an absolute filename of a file located on the
|
||||
#: local filesystem.
|
||||
path = None
|
||||
|
||||
#: Name of the URL argument that contains path.
|
||||
path_url_kwarg = 'path'
|
||||
|
||||
def get_path(self):
|
||||
"""Return actual path of the file to serve.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path`.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
As an example, :py:attr:`path` could be relative and your custom
|
||||
:py:meth:`get_path` implementation makes it absolute.
|
||||
|
||||
"""
|
||||
return self.kwargs.get(self.path_url_kwarg, self.path)
|
||||
|
||||
def get_file(self):
|
||||
"""Use path to return wrapper around file to serve."""
|
||||
return File(open(self.get_path()))
|
||||
29
django_downloadview/views/storage.py
Normal file
29
django_downloadview/views/storage.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files from storage."""
|
||||
from django.core.files.storage import DefaultStorage
|
||||
|
||||
from django_downloadview.files import StorageFile
|
||||
from django_downloadview.views.path import PathDownloadView
|
||||
|
||||
|
||||
class StorageDownloadView(PathDownloadView):
|
||||
"""Serve a file using storage and filename."""
|
||||
#: Storage the file to serve belongs to.
|
||||
storage = DefaultStorage()
|
||||
|
||||
#: Path to the file to serve relative to storage.
|
||||
path = None # Override docstring.
|
||||
|
||||
def get_path(self):
|
||||
"""Return path of the file to serve, relative to storage.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path` attribute.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
|
||||
"""
|
||||
return super(StorageDownloadView, self).get_path()
|
||||
|
||||
def get_file(self):
|
||||
"""Return :class:`~django_downloadview.files.StorageFile` instance."""
|
||||
return StorageFile(self.storage, self.get_path())
|
||||
31
django_downloadview/views/virtual.py
Normal file
31
django_downloadview/views/virtual.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files that you generate or that live in memory."""
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
|
||||
class VirtualDownloadView(BaseDownloadView):
|
||||
"""Serve not-on-disk or generated-on-the-fly file.
|
||||
|
||||
Override the :py:meth:`get_file` method to customize file wrapper.
|
||||
|
||||
"""
|
||||
def was_modified_since(self, file_instance, since):
|
||||
"""Delegate to file wrapper's was_modified_since, or return True.
|
||||
|
||||
This is the implementation of an edge case: when files are generated
|
||||
on the fly, we cannot guess whether they have been modified or not.
|
||||
If the file wrapper implements ``was_modified_since()`` method, then we
|
||||
trust it. Otherwise it is safer to suppose that the file has been
|
||||
modified.
|
||||
|
||||
This behaviour prevents file size to be computed on the Django side.
|
||||
Because computing file size means iterating over all the file contents,
|
||||
and we want to avoid that whenever possible. As an example, it could
|
||||
reduce all the benefits of working with dynamic file generators...
|
||||
which is a major feature of virtual files.
|
||||
|
||||
"""
|
||||
try:
|
||||
return file_instance.was_modified_since(since)
|
||||
except (AttributeError, NotImplementedError):
|
||||
return True
|
||||
|
|
@ -30,30 +30,45 @@ django-sendfile
|
|||
`django-sendfile`_ is a wrapper around web-server specific methods for sending
|
||||
files to web clients.
|
||||
|
||||
API is made of a single ``sendfile()`` function, which returns a download
|
||||
response. The download response type depends on the chosen backend, which could
|
||||
be Django, Lighttpd's X-Sendfile, Nginx's X-Accel...
|
||||
``django-senfile``'s main focus is simplicity: API is made of a single
|
||||
``sendfile()`` function you call inside your views:
|
||||
|
||||
It seems that django-senfile main focus is simplicity: you call the
|
||||
``sendfile()`` method inside your views.
|
||||
.. code:: python
|
||||
|
||||
Django-downloadview main focus is reusability: you configure (or override)
|
||||
class-based views depending on the use case.
|
||||
from sendfile import sendfile
|
||||
|
||||
def hello_world(request):
|
||||
"""Send 'hello-world.pdf' file as a response."""
|
||||
return sendfile(request, '/path/to/hello-world.pdf')
|
||||
|
||||
The download response type depends on the chosen backend, which could
|
||||
be Django, Lighttpd's X-Sendfile, Nginx's X-Accel... depending your settings:
|
||||
|
||||
.. code:: python
|
||||
|
||||
SENDFILE_BACKEND = 'sendfile.backends.nginx' # sendfile() will return
|
||||
# X-Accel responses.
|
||||
# Additional settings for sendfile's nginx backend.
|
||||
SENDFILE_ROOT = '/path/to'
|
||||
SENDFILE_URL = '/proxied-download'
|
||||
|
||||
Here are main differences between the two projects:
|
||||
|
||||
* ``django-sendfile`` supports only files that live on local filesystem (i.e.
|
||||
where ``os.path.exists`` returns ``True``). Whereas ``django-downloadview``
|
||||
allows you to serve or proxy files stored in various locations, including
|
||||
remote ones.
|
||||
|
||||
* ``django-sendfile`` uses a single global configuration (i.e.
|
||||
``settings.SENDFILE_ROOT``), thus optimizations are limited to a single
|
||||
root folder. Whereas ``django-downloadview``'s
|
||||
``DownloadDispatcherMiddleware`` supports multiple configurations.
|
||||
|
||||
As of 2012-04-11, ``django-sendfile`` (version 0.3.2) seems quite popular and
|
||||
may be a good alternative **provided you serve files that live in local
|
||||
filesystem**, because the ``sendfile()`` method only accepts filenames relative
|
||||
to local filesystem (i.e. using ``os.path.exists``).
|
||||
may be a good alternative **provided you serve files that live in a single
|
||||
directory of local filesystem**.
|
||||
|
||||
Django-downloadview (since version 1.1) handles file wrappers, and thus allows
|
||||
you to serve files from more locations:
|
||||
|
||||
* models,
|
||||
* storages,
|
||||
* local filesystem,
|
||||
* remote URL (using `requests`_),
|
||||
* in-memory (or generated) files (such as StringIO),
|
||||
* ... and your custom ones with little efforts.
|
||||
:func:`django_downloadview.sendfile` is a port of django-sendfile's main function.
|
||||
|
||||
|
||||
********************
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ About django-downloadview
|
|||
|
||||
.. toctree::
|
||||
|
||||
vision
|
||||
alternatives
|
||||
license
|
||||
authors
|
||||
|
|
|
|||
26
docs/about/vision.txt
Normal file
26
docs/about/vision.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
######
|
||||
Vision
|
||||
######
|
||||
|
||||
`django-downloadview` tries to simplify the development of "download" views
|
||||
using `Django`_ framework. It provides generic views that cover most common
|
||||
patterns.
|
||||
|
||||
Django is not the best solution to serve files: reverse proxies are far more
|
||||
efficient. `django-downloadview` makes it easy to implement this best-practice.
|
||||
|
||||
Tests matter: `django-downloadview` provides tools to test download views and
|
||||
optimizations.
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :doc:`/about/alternatives`
|
||||
* `roadmap
|
||||
<https://github.com/benoitbryon/django-downloadview/issues/milestones>`_
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Django`: https://django-project.com
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
django_downloadview Package
|
||||
===========================
|
||||
|
||||
:mod:`django_downloadview` Package
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: django_downloadview.__init__
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`decorators` Module
|
||||
------------------------
|
||||
|
||||
.. automodule:: django_downloadview.decorators
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`files` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: django_downloadview.files
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`middlewares` Module
|
||||
-------------------------
|
||||
|
||||
.. automodule:: django_downloadview.middlewares
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`nginx` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: django_downloadview.nginx
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`response` Module
|
||||
----------------------
|
||||
|
||||
.. automodule:: django_downloadview.response
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`test` Module
|
||||
------------------
|
||||
|
||||
.. automodule:: django_downloadview.test
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`utils` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: django_downloadview.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`views` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: django_downloadview.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
###
|
||||
API
|
||||
###
|
||||
|
||||
Here is API documentation, generated from code.
|
||||
|
||||
.. toctree::
|
||||
|
||||
modules
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
django_downloadview
|
||||
===================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
django_downloadview
|
||||
19
docs/conf.py
19
docs/conf.py
|
|
@ -10,8 +10,7 @@
|
|||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import os
|
||||
|
||||
|
||||
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
|
||||
|
|
@ -41,8 +40,11 @@ version_filename = os.path.join(project_dir, 'VERSION')
|
|||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest', 'sphinx.ext.coverage']
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.intersphinx']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
|
@ -189,6 +191,15 @@ html_sidebars = {
|
|||
htmlhelp_basename = 'django-downloadviewdoc'
|
||||
|
||||
|
||||
# -- Options for sphinx.ext.intersphinx ---------------------------------------
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/2.7', None),
|
||||
'django': ('http://docs.djangoproject.com/en/1.5/',
|
||||
'http://docs.djangoproject.com/en/1.5/_objects/'),
|
||||
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
||||
}
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
.. include:: ../demo/README
|
||||
.. include:: ../demo/README.rst
|
||||
|
|
|
|||
17
docs/dev.txt
17
docs/dev.txt
|
|
@ -44,7 +44,7 @@ Setup a development environment
|
|||
|
||||
System requirements:
|
||||
|
||||
* `Python`_ version 2.6 or 2.7, available as ``python`` command.
|
||||
* `Python`_ version 2.7, available as ``python`` command.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
@ -80,21 +80,6 @@ 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`_.
|
||||
|
||||
|
||||
*********************
|
||||
Demo project included
|
||||
*********************
|
||||
|
|
|
|||
89
docs/files.txt
Normal file
89
docs/files.txt
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#############
|
||||
File wrappers
|
||||
#############
|
||||
|
||||
.. py:module:: django_downloadview.files
|
||||
|
||||
A view return :class:`~django_downloadview.response.DownloadResponse` which
|
||||
itself carries a file wrapper. Here are file wrappers distributed by Django
|
||||
and django-downloadview.
|
||||
|
||||
|
||||
*****************
|
||||
Django's builtins
|
||||
*****************
|
||||
|
||||
`Django itself provides some file wrappers`_ you can use within
|
||||
``django-downloadview``:
|
||||
|
||||
* :py:class:`django.core.files.File` wraps a file that live on local
|
||||
filesystem, initialized with a path. ``django-downloadview`` uses this
|
||||
wrapper in :doc:`/views/path`.
|
||||
|
||||
* :py:class:`django.db.models.fields.files.FieldFile` wraps a file that is
|
||||
managed in a model. ``django-downloadview`` uses this wrapper in
|
||||
:doc:`/views/object`.
|
||||
|
||||
* :py:class:`django.core.files.base.ContentFile` wraps a bytes, string or
|
||||
unicode object. You may use it with :doc:`VirtualDownloadView
|
||||
</views/virtual>`.
|
||||
|
||||
|
||||
****************************
|
||||
django-downloadview builtins
|
||||
****************************
|
||||
|
||||
``django-downloadview`` implements additional file wrappers:
|
||||
|
||||
* :class:`StorageFile` wraps a file that is
|
||||
managed via a storage (but not necessarily via a model).
|
||||
:doc:`/views/storage` uses this wrapper.
|
||||
|
||||
* :class:`HTTPFile` wraps a file that lives at
|
||||
some (remote) location, initialized with an URL.
|
||||
:doc:`/views/http` uses this wrapper.
|
||||
|
||||
* :class:`VirtualFile` wraps a file that lives in
|
||||
memory, i.e. built as a string.
|
||||
This is a convenient wrapper to use in :doc:`/views/virtual` subclasses.
|
||||
|
||||
|
||||
*************
|
||||
API reference
|
||||
*************
|
||||
|
||||
StorageFile
|
||||
===========
|
||||
|
||||
.. autoclass:: StorageFile
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
HTTPFile
|
||||
========
|
||||
|
||||
.. autoclass:: HTTPFile
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
VirtualFile
|
||||
===========
|
||||
|
||||
.. autoclass:: VirtualFile
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Django itself provides some file wrappers`:
|
||||
https://docs.djangoproject.com/en/1.5/ref/files/file/
|
||||
106
docs/healthchecks.txt
Normal file
106
docs/healthchecks.txt
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
##################
|
||||
Write healthchecks
|
||||
##################
|
||||
|
||||
In the previous :doc:`testing </testing>` topic, you made sure the views and
|
||||
middlewares work as expected... within a test environment.
|
||||
|
||||
One common issue when deploying in production is that the reverse-proxy's
|
||||
configuration does not fit. You cannot check that within test environment.
|
||||
|
||||
**Healthchecks are made to diagnose issues in live (production) environments**.
|
||||
|
||||
|
||||
************************
|
||||
Introducing healthchecks
|
||||
************************
|
||||
|
||||
Healthchecks (sometimes called "smoke tests" or "diagnosis") are assertions you
|
||||
run on a live (typically production) service, as opposed to fake/mock service
|
||||
used during tests (unit, integration, functional).
|
||||
|
||||
See `hospital`_ and `django-doctor`_ projects about writing healthchecks for
|
||||
Python and Django.
|
||||
|
||||
|
||||
********************
|
||||
Typical healthchecks
|
||||
********************
|
||||
|
||||
Here is a typical healthcheck setup for download views with reverse-proxy
|
||||
optimizations.
|
||||
|
||||
When you run this healthcheck suite, you get a good overview if a problem
|
||||
occurs: you can compare expected results and learn which part (Django,
|
||||
reverse-proxy or remote storage) is guilty.
|
||||
|
||||
.. note::
|
||||
|
||||
In the examples below, we use "localhost" and ports "80" (reverse-proxy) or
|
||||
"8000" (Django). Adapt them to your configuration.
|
||||
|
||||
Check storage
|
||||
=============
|
||||
|
||||
Put a dummy file on the storage Django uses.
|
||||
|
||||
The write a healthcheck that asserts you can read the dummy file from storage.
|
||||
|
||||
**On success, you know remote storage is ok.**
|
||||
|
||||
Issues may involve permissions or communications (remote storage).
|
||||
|
||||
.. note::
|
||||
|
||||
This healthcheck may be outside Django.
|
||||
|
||||
Check Django VS storage
|
||||
=======================
|
||||
|
||||
Implement a download view dedicated to healthchecks. It is typically a public
|
||||
(but not referenced) view that streams a dummy file from real storage.
|
||||
Let's say you register it as ``/healthcheck-utils/download/`` URL.
|
||||
|
||||
Write a healthcheck that asserts ``GET
|
||||
http://localhost:8000/healtcheck-utils/download/`` (notice the `8000` port:
|
||||
local Django server) returns the expected reverse-proxy response (X-Accel,
|
||||
X-Sendfile...).
|
||||
|
||||
**On success, you know there is no configuration issue on the Django side.**
|
||||
|
||||
Check reverse proxy VS storage
|
||||
==============================
|
||||
|
||||
Write a location in your reverse-proxy's configuration that proxy-pass to a
|
||||
dummy file on storage.
|
||||
|
||||
Write a healthcheck that asserts this location returns the expected dummy file.
|
||||
|
||||
**On success, you know the reverse proxy can serve files from storage.**
|
||||
|
||||
Check them all together
|
||||
=======================
|
||||
|
||||
We just checked all parts separately, so let's make sure they can work
|
||||
together.
|
||||
Configure the reverse-proxy so that `/healthcheck-utils/download/` is proxied
|
||||
to Django. Then write a healthcheck that asserts ``GET
|
||||
http://localhost:80/healthcheck-utils/download`` (notice the `80` port:
|
||||
reverse-proxy server) returns the expected dummy file.
|
||||
|
||||
**On success, you know everything is ok.**
|
||||
|
||||
On failure, there is an issue in the X-Accel/X-Sendfile configuration.
|
||||
|
||||
.. note::
|
||||
|
||||
This last healthcheck should be the first one to run, i.e. if it passes,
|
||||
others should pass too. The others are useful when this one fails.
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`hospital`: https://pypi.python.org/pypi/hospital
|
||||
.. _`django-doctor`: https://pypi.python.org/pypi/django-doctor
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.. include:: ../README
|
||||
.. include:: ../README.rst
|
||||
|
||||
|
||||
********
|
||||
|
|
@ -9,11 +9,15 @@ Contents
|
|||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
demo
|
||||
overview
|
||||
install
|
||||
views
|
||||
settings
|
||||
views/index
|
||||
optimizations/index
|
||||
testing
|
||||
api/index
|
||||
healthchecks
|
||||
files
|
||||
responses
|
||||
demo
|
||||
about/index
|
||||
dev
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#############
|
||||
Optimizations
|
||||
#############
|
||||
##################
|
||||
Optimize streaming
|
||||
##################
|
||||
|
||||
Some reverse proxies allow applications to delegate actual download to the
|
||||
proxy:
|
||||
|
|
@ -11,40 +11,41 @@ proxy:
|
|||
As a result, you get increased performance: reverse proxies are more efficient
|
||||
than Django at serving static files.
|
||||
|
||||
The setup depends on the reverse proxy:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
nginx
|
||||
|
||||
Currently, only `nginx's X-Accel`_ is supported, but `contributions are
|
||||
welcome`_!
|
||||
.. note::
|
||||
|
||||
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.
|
||||
View return some :class:`~django_downloadview.response.DownloadResponse`
|
||||
instance, which itself carries a :doc:`file wrapper </files>`.
|
||||
|
||||
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.
|
||||
`django-downloadview` provides response middlewares and decorators that are
|
||||
able to capture :class:`~django_downloadview.response.DownloadResponse`
|
||||
instances and convert them to
|
||||
:class:`~django_downloadview.response.ProxiedDownloadResponse`.
|
||||
|
||||
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`.
|
||||
.. note::
|
||||
|
||||
The feature is inspired by :mod:`Django's TemplateResponse
|
||||
<django.template.response>`
|
||||
|
||||
|
||||
.. rubric:: References
|
||||
.. rubric:: Notes & 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.5/ref/template-response/
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
Nginx
|
||||
#####
|
||||
|
||||
If you serve Django behind Nginx, then you can delegate the file download
|
||||
service to Nginx and get increased performance:
|
||||
If you serve Django behind Nginx, then you can delegate the file streaming
|
||||
to Nginx and get increased performance:
|
||||
|
||||
* lower resources used by Python/Django workers ;
|
||||
* faster download.
|
||||
|
|
@ -11,103 +11,94 @@ service to Nginx and get increased performance:
|
|||
See `Nginx X-accel documentation`_ for details.
|
||||
|
||||
|
||||
****************************
|
||||
Configure some download view
|
||||
****************************
|
||||
************
|
||||
Given a view
|
||||
************
|
||||
|
||||
Let's start in the situation described in the :doc:`demo application </demo>`:
|
||||
Let's consider the following view:
|
||||
|
||||
* a project "demoproject"
|
||||
* an application "demoproject.download"
|
||||
* a :py:class:`django_downloadview.views.ObjectDownloadView` view serves files
|
||||
of a "Document" model.
|
||||
.. literalinclude:: /../demo/demoproject/nginx/views.py
|
||||
:language: python
|
||||
:lines: 1-6, 8-16
|
||||
|
||||
We are to make it more efficient with Nginx.
|
||||
What is important here is that the files will have an ``url`` property
|
||||
implemented by storage. Let's setup an optimization rule based on that URL.
|
||||
|
||||
.. note::
|
||||
|
||||
Examples below are taken from the :doc:`demo project </demo>`.
|
||||
It is generally easier to setup rules based on URL rather than based on
|
||||
name in filesystem. This is because path is generally relative to storage,
|
||||
whereas URL usually contains some storage identifier, i.e. it is easier to
|
||||
target a specific location by URL rather than by filesystem name.
|
||||
|
||||
|
||||
***********
|
||||
Write tests
|
||||
***********
|
||||
********************************
|
||||
Setup XAccelRedirect middlewares
|
||||
********************************
|
||||
|
||||
Use :py:func:`django_downloadview.nginx.assert_x_accel_redirect` function as
|
||||
a shortcut in your tests.
|
||||
Make sure ``django_downloadview.DownloadDispatcherMiddleware`` is in
|
||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
||||
|
||||
:file:`demo/demoproject/nginx/tests.py`:
|
||||
Example:
|
||||
|
||||
.. literalinclude:: ../../demo/demoproject/nginx/tests.py
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:emphasize-lines: 5, 25-34
|
||||
:lines: 61-68
|
||||
|
||||
Right now, this test should fail, since you haven't implemented the view yet.
|
||||
Then register as many
|
||||
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware`
|
||||
instances as you wish in ``DOWNLOADVIEW_MIDDLEWARES``.
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 72-76
|
||||
|
||||
The first item is an identifier.
|
||||
|
||||
The second item is the import path of
|
||||
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware` class.
|
||||
|
||||
The third item is a dictionary of keyword arguments passed to the middleware
|
||||
factory. In the example above, we capture responses by ``source_url`` and
|
||||
convert them to internal redirects to ``destination_url``.
|
||||
|
||||
.. autoclass:: django_downloadview.nginx.middlewares.XAccelRedirectMiddleware
|
||||
:members:
|
||||
:inherited-members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
************
|
||||
Setup Django
|
||||
************
|
||||
**********************************************
|
||||
Per-view setup with x_accel_redirect decorator
|
||||
**********************************************
|
||||
|
||||
At the end of this setup, the test should pass, but you still have to `setup
|
||||
Nginx`_!
|
||||
Middlewares should be enough for most use cases, but you may want per-view
|
||||
configuration. For `nginx`, there is ``x_accel_redirect``:
|
||||
|
||||
You have two options: global setup with a middleware, or per-view setup with
|
||||
decorators.
|
||||
.. autofunction:: django_downloadview.nginx.decorators.x_accel_redirect
|
||||
|
||||
Global delegation, with XAccelRedirectMiddleware
|
||||
================================================
|
||||
As an example:
|
||||
|
||||
If you want to delegate all file downloads to Nginx, then use
|
||||
:py:class:`django_downloadview.nginx.XAccelRedirectMiddleware`.
|
||||
.. literalinclude:: /../demo/demoproject/nginx/views.py
|
||||
:language: python
|
||||
:lines: 1-7, 17-
|
||||
|
||||
Register it in your settings:
|
||||
|
||||
.. code-block:: python
|
||||
*******************************************
|
||||
Test responses with assert_x_accel_redirect
|
||||
*******************************************
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
# ...
|
||||
'django_downloadview.nginx.XAccelRedirectMiddleware',
|
||||
# ...
|
||||
)
|
||||
Use :func:`~django_downloadview.nginx.decorators.assert_x_accel_redirect`
|
||||
function as a shortcut in your tests.
|
||||
|
||||
Setup the middleware:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT # Could be elsewhere.
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = '/proxied-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.
|
||||
|
||||
:file:`demo/demoproject/nginx/views.py`:
|
||||
|
||||
.. literalinclude:: ../../demo/demoproject/nginx/views.py
|
||||
.. literalinclude:: /../demo/demoproject/nginx/tests.py
|
||||
:language: python
|
||||
|
||||
And use it in som URL conf, as an example in
|
||||
:file:`demo/demoproject/nginx/urls.py`:
|
||||
.. autofunction:: django_downloadview.nginx.tests.assert_x_accel_redirect
|
||||
|
||||
.. 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.
|
||||
The tests above assert the `Django` part is OK. Now let's configure `nginx`.
|
||||
|
||||
|
||||
***********
|
||||
|
|
@ -136,11 +127,11 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
|
|||
# like /optimized-download/myfile.tar.gz
|
||||
#
|
||||
# See http://wiki.nginx.org/X-accel
|
||||
# and https://github.com/benoitbryon/django-downloadview
|
||||
# and https://django-downloadview.readthedocs.org
|
||||
#
|
||||
location /proxied-download {
|
||||
internal;
|
||||
# Location to files on disk.
|
||||
# See Django's settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||
alias /var/www/files/;
|
||||
}
|
||||
|
||||
|
|
@ -158,12 +149,17 @@ section.
|
|||
|
||||
.. note::
|
||||
|
||||
``/proxied-download`` is not available for the client, i.e. users
|
||||
won't be able to download files via ``/optimized-download/<filename>``.
|
||||
``/proxied-download`` has the ``internal`` flag, so this location is not
|
||||
available for the client, i.e. users are not able to download files via
|
||||
``/optimized-download/<filename>``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Make sure Nginx can read the files to download! Check permissions.
|
||||
*********************************************
|
||||
Assert everything goes fine with healthchecks
|
||||
*********************************************
|
||||
|
||||
:doc:`Healthchecks </healthchecks>` are the best way to check the complete
|
||||
setup.
|
||||
|
||||
|
||||
*************
|
||||
|
|
|
|||
109
docs/overview.txt
Normal file
109
docs/overview.txt
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
##################
|
||||
Overview, concepts
|
||||
##################
|
||||
|
||||
Given:
|
||||
|
||||
* you manage files with Django (permissions, search, generation, ...)
|
||||
|
||||
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||
storage, memory...)
|
||||
|
||||
As a developer, you want to serve files quick and efficiently.
|
||||
|
||||
Here is an overview of ``django-downloadview``'s answer...
|
||||
|
||||
|
||||
************************************
|
||||
Generic views cover commons patterns
|
||||
************************************
|
||||
|
||||
Choose the generic view depending on the file you want to serve:
|
||||
|
||||
* :doc:`/views/object`: file field in a model;
|
||||
* :doc:`/views/storage`: file in a storage;
|
||||
* :doc:`/views/path`: absolute filename on local filesystem;
|
||||
* :doc:`/views/http`: URL (the resource is proxied);
|
||||
* :doc:`/views/virtual`: bytes, text, :class:`~StringIO.StringIO`, generated
|
||||
file...
|
||||
|
||||
|
||||
*************************************************
|
||||
Generic views and mixins allow easy customization
|
||||
*************************************************
|
||||
|
||||
If your use case is a bit specific, you can easily extend the views above or
|
||||
:doc:`create your own based on mixins </views/custom>`.
|
||||
|
||||
|
||||
*****************************
|
||||
Views return DownloadResponse
|
||||
*****************************
|
||||
|
||||
Views return :py:class:`~django_downloadview.response.DownloadResponse`. It is
|
||||
a special :py:class:`django.http.StreamingHttpResponse` where content is
|
||||
encapsulated in a file wrapper.
|
||||
|
||||
Learn more in :doc:`responses`.
|
||||
|
||||
|
||||
***********************************
|
||||
DownloadResponse carry file wrapper
|
||||
***********************************
|
||||
|
||||
Views instanciate a :doc:`file wrapper </files>` and use it to initialize
|
||||
responses.
|
||||
|
||||
**File wrappers describe files**: they carry files properties such as name,
|
||||
size, encoding...
|
||||
|
||||
**File wrappers implement loading and iterating over file content**. Whenever
|
||||
possible, file wrappers do not embed file data, in order to save memory.
|
||||
|
||||
Learn more about available file wrappers in :doc:`files`.
|
||||
|
||||
|
||||
*****************************************************************
|
||||
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
||||
*****************************************************************
|
||||
|
||||
Before WSGI application use file wrapper to load file contents, middlewares
|
||||
(or decorators) are given the opportunity to capture
|
||||
:class:`~django_downloadview.response.DownloadResponse` instances.
|
||||
|
||||
Let's take this opportunity to optimize file loading and streaming!
|
||||
|
||||
A good optimization it to delegate streaming to a reverse proxy, such as
|
||||
`nginx`_ via `X-Accel`_ internal redirects.
|
||||
|
||||
`django_downloadview` provides middlewares that convert
|
||||
:class:`~django_downloadview.response.DownloadResponse` into
|
||||
:class:`~django_downloadview.response.ProxiedDownloadResponse`.
|
||||
|
||||
Learn more in :doc:`optimizations/index`.
|
||||
|
||||
|
||||
***************
|
||||
Testing matters
|
||||
***************
|
||||
|
||||
`django-downloadview` also helps you :doc:`test the views you customized
|
||||
<testing>`.
|
||||
|
||||
You may also :doc:`write healthchecks </healthchecks>` to make sure everything
|
||||
goes fine in live environments.
|
||||
|
||||
|
||||
************
|
||||
What's next?
|
||||
************
|
||||
|
||||
Let's :doc:`install django-downloadview <install>`.
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`nginx`: http://nginx.org
|
||||
.. _`X-Accel`: http://wiki.nginx.org/X-accel
|
||||
32
docs/responses.txt
Normal file
32
docs/responses.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#########
|
||||
Responses
|
||||
#########
|
||||
|
||||
.. currentmodule:: django_downloadview.response
|
||||
|
||||
Views return :class:`DownloadResponse`.
|
||||
|
||||
Middlewares (and decorators) are given the opportunity to capture responses and
|
||||
convert them to :class:`ProxiedDownloadResponse`.
|
||||
|
||||
|
||||
****************
|
||||
DownloadResponse
|
||||
****************
|
||||
|
||||
.. autoclass:: DownloadResponse
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
***********************
|
||||
ProxiedDownloadResponse
|
||||
***********************
|
||||
|
||||
.. autoclass:: ProxiedDownloadResponse
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
65
docs/settings.txt
Normal file
65
docs/settings.txt
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#########
|
||||
Configure
|
||||
#########
|
||||
|
||||
Here is the list of settings used by `django-downloadview`.
|
||||
|
||||
|
||||
**************
|
||||
INSTALLED_APPS
|
||||
**************
|
||||
|
||||
There is no need to register this application in your Django's
|
||||
``INSTALLED_APPS`` setting.
|
||||
|
||||
|
||||
******************
|
||||
MIDDLEWARE_CLASSES
|
||||
******************
|
||||
|
||||
If you plan to setup reverse-proxy optimizations, add
|
||||
``django_downloadview.DownloadDispatcherMiddleware`` to ``MIDDLEWARE_CLASSES``.
|
||||
It is a response middleware. Move it after middlewares that compute the
|
||||
response content such as gzip middleware.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 61-68
|
||||
|
||||
|
||||
************************
|
||||
DOWNLOADVIEW_MIDDLEWARES
|
||||
************************
|
||||
|
||||
:default: []
|
||||
|
||||
If you plan to setup reverse-proxy :doc:`optimizations </optimizations/index>`,
|
||||
setup ``DOWNLOADVIEW_MIDDLEWARES`` value. This setting is used by
|
||||
:py:class:`~django_downloadview.middlewares.DownloadDispatcherMiddleware`.
|
||||
It is the list of handlers that will be given the opportunity to capture
|
||||
download responses and convert them to internal redirects for use with
|
||||
reverse-proxies.
|
||||
|
||||
The list expects items ``(id, path, options)`` such as:
|
||||
|
||||
* ``id`` is an identifier
|
||||
* ``path`` is the import path of some download middleware factory (typically a
|
||||
class).
|
||||
* ``options`` is a dictionary of keyword arguments passed to the middleware
|
||||
factory.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 72-76
|
||||
|
||||
See :doc:`/optimizations/index` for details about middlewares and their
|
||||
options.
|
||||
|
||||
.. note::
|
||||
|
||||
You can register several middlewares. It allows you to setup several
|
||||
conversion rules with distinct source/destination patterns.
|
||||
|
|
@ -1,7 +1,44 @@
|
|||
######################
|
||||
Testing download views
|
||||
######################
|
||||
###########
|
||||
Write tests
|
||||
###########
|
||||
|
||||
This project includes shortcuts to simplify testing.
|
||||
`django_downloadview` embeds test utilities:
|
||||
|
||||
* :func:`~django_downloadview.test.temporary_media_root`
|
||||
* :func:`~django_downloadview.test.assert_download_response`
|
||||
* :func:`~django_downloadview.test.setup_view`
|
||||
* :func:`~django_downloadview.nginx.tests.assert_x_accel_redirect`
|
||||
|
||||
|
||||
********************
|
||||
temporary_media_root
|
||||
********************
|
||||
|
||||
.. autofunction:: django_downloadview.test.temporary_media_root
|
||||
|
||||
|
||||
************************
|
||||
assert_download_response
|
||||
************************
|
||||
|
||||
.. autofunction:: django_downloadview.test.assert_download_response
|
||||
|
||||
Examples, related to :doc:`StorageDownloadView demo </views/storage>`:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/storage/tests.py
|
||||
:language: python
|
||||
:lines: 3-7, 9-57
|
||||
|
||||
|
||||
**********
|
||||
setup_view
|
||||
**********
|
||||
|
||||
.. autofunction:: django_downloadview.test.setup_view
|
||||
|
||||
Example, related to :doc:`StorageDownloadView demo </views/storage>`:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/storage/tests.py
|
||||
:language: python
|
||||
:lines: 1-2, 8-12, 59-
|
||||
|
||||
See :py:mod:`django_downloadview.test` for details.
|
||||
|
|
|
|||
113
docs/views.txt
113
docs/views.txt
|
|
@ -1,113 +0,0 @@
|
|||
##############
|
||||
Download views
|
||||
##############
|
||||
|
||||
This section contains narrative overview about class-based views provided by
|
||||
django-downloadview.
|
||||
|
||||
By default, all of those views would stream the file to the client.
|
||||
But keep in mind that you can setup :doc:`/optimizations/index` to delegate
|
||||
actual streaming to a reverse proxy.
|
||||
|
||||
|
||||
*************
|
||||
DownloadMixin
|
||||
*************
|
||||
|
||||
The :py:class:`django_downloadview.views.DownloadMixin` class is not a view. It
|
||||
is a base class which you can inherit of to create custom download views.
|
||||
|
||||
``DownloadMixin`` is a base of `BaseDownloadView`_, which itself is a base of
|
||||
all other django_downloadview's builtin views.
|
||||
|
||||
|
||||
****************
|
||||
BaseDownloadView
|
||||
****************
|
||||
|
||||
The :py:class:`django_downloadview.views.BaseDownloadView` class is a base
|
||||
class to create download views. It inherits `DownloadMixin`_ and
|
||||
:py:class:`django.views.generic.base.View`.
|
||||
|
||||
The only thing it does is to implement
|
||||
:py:meth:`get <django_downloadview.views.BaseDownloadView.get>`: it triggers
|
||||
:py:meth:`DownloadMixin's render_to_response
|
||||
<django_downloadview.views.DownloadMixin.render_to_response>`.
|
||||
|
||||
|
||||
******************
|
||||
ObjectDownloadView
|
||||
******************
|
||||
|
||||
The :py:class:`django_downloadview.views.ObjectDownloadView` class-based view
|
||||
allows you to **serve files given a model with some file fields** such as
|
||||
FileField or ImageField.
|
||||
|
||||
Use this view anywhere you could use Django's builtin ObjectDetailView.
|
||||
|
||||
Some options allow you to store file metadata (size, content-type, ...) in the
|
||||
model, as deserialized fields.
|
||||
|
||||
|
||||
*******************
|
||||
StorageDownloadView
|
||||
*******************
|
||||
|
||||
The :py:class:`django_downloadview.views.StorageDownloadView` class-based view
|
||||
allows you to **serve files given a storage and a path**.
|
||||
|
||||
Use this view when you manage files in a storage (which is a good practice),
|
||||
unrelated to a model.
|
||||
|
||||
|
||||
****************
|
||||
PathDownloadView
|
||||
****************
|
||||
|
||||
The :py:class:`django_downloadview.views.PathDownloadView` class-based view
|
||||
allows you to **serve files given an absolute path on local filesystem**.
|
||||
|
||||
Two main use cases:
|
||||
|
||||
* as a shortcut. This dead-simple view is straight to call, so you can use it
|
||||
to simplify code in more complex views, provided you have an absolute path to
|
||||
a local file.
|
||||
|
||||
* override. Extend :py:class:`django_downloadview.views.PathDownloadView` and
|
||||
override :py:meth:`django_downloadview.views.PathDownloadView:get_path`.
|
||||
|
||||
|
||||
****************
|
||||
HTTPDownloadView
|
||||
****************
|
||||
|
||||
The :py:class:`django_downloadview.views.HTTPDownloadView` class-based view
|
||||
allows you to **serve files given an URL**. That URL is supposed to be
|
||||
downloadable from the Django server.
|
||||
|
||||
Use it when you want to setup a proxy to remote files:
|
||||
|
||||
* the Django view filters input and computes target URL.
|
||||
* if you setup optimizations, Django itself doesn't proxies the file,
|
||||
* but, as a fallback, Django uses `requests`_ to proxy the file.
|
||||
|
||||
Extend :py:class:`django_downloadview.views.HTTPDownloadView` then
|
||||
override :py:meth:`django_downloadview.views.HTTPDownloadView:get_url`.
|
||||
|
||||
|
||||
*******************
|
||||
VirtualDownloadView
|
||||
*******************
|
||||
|
||||
The :py:class:`django_downloadview.views.VirtualDownloadView` class-based view
|
||||
allows you to **serve files that don't live on disk**.
|
||||
|
||||
Use it when you want to stream a file which content is dynamically generated
|
||||
or which lives in memory.
|
||||
|
||||
|
||||
.. rubric:: References
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`requests`: https://pypi.python.org/pypi/requests
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue