django-downloadview¶
django-downloadview makes it easy to serve files with Django:
- you manage files with Django (permissions, filters, 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 or Apache’s X-Sendfile.
Example¶
Let’s serve a file stored in a file field of some model:
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'),
)
Resources¶
- Documentation: https://django-downloadview.readthedocs.io
- PyPI page: http://pypi.python.org/pypi/django-downloadview
- Code repository: https://github.com/jazzband/django-downloadview
- Bugtracker: https://github.com/jazzband/django-downloadview/issues
- Continuous integration: https://github.com/jazzband/django-downloadview/actions
- Roadmap: https://github.com/jazzband/django-downloadview/milestones
Contents¶
Overview, concepts¶
Given:
- you manage files with Django (permissions, filters, 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:
- ObjectDownloadView: file field in a model;
- StorageDownloadView: file in a storage;
- PathDownloadView: absolute filename on local filesystem;
- HTTPDownloadView: file at URL (the resource is proxied);
- VirtualDownloadView: bytes, text, file-like objects, generated files…
Generic views and mixins allow easy customization¶
If your use case is a bit specific, you can easily extend the views above or create your own based on mixins.
Views return DownloadResponse¶
Views return DownloadResponse
. It is
a special django.http.StreamingHttpResponse
where content is
encapsulated in a file wrapper.
Learn more in Responses.
DownloadResponse carry file wrapper¶
Views instanciate a file wrapper 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 File wrappers.
Middlewares convert DownloadResponse into ProxiedDownloadResponse¶
Before WSGI application use file wrapper and actually use file contents,
middlewares or decorators) are given the opportunity to capture
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 [1] via X-Accel [2] internal redirects. This way, Django doesn’t load file content in memory.
django_downloadview provides middlewares that convert
DownloadResponse
into
ProxiedDownloadResponse
.
Learn more in Optimize streaming.
Testing matters¶
django-downloadview also helps you test the views you customized.
You may also write healthchecks to make sure everything goes fine in live environments.
What’s next?¶
Let’s install django-downloadview.
Notes & references
[1] | http://nginx.org |
[2] | http://wiki.nginx.org/X-accel |
Install¶
Note
If you want to install a development environment, please see Contributing.
Requirements¶
django-downloadview has been tested with Python [1] 3.6, 3.7 and 3.8. Other versions may work, but they are not part of the test suite at the moment.
Installing django-downloadview will automatically trigger the installation of the following requirements:
"Django>=2.2",
"requests",
As a library¶
In most cases, you will use django-downloadview as a dependency of another
project. In such a case, you should add django-downloadview in your main
project’s requirements. Typically in setup.py
:
from setuptools import setup
setup(
install_requires=[
'django-downloadview',
#...
]
# ...
)
Then when you install your main project with your favorite package manager (like pip [2]), django-downloadview and its recursive dependencies will automatically be installed.
Standalone¶
You can install django-downloadview with your favorite Python package manager. As an example with pip [2]:
pip install django-downloadview
Check¶
Check django-downloadview has been installed:
python -c "import django_downloadview;print(django_downloadview.__version__)"
You should get installed django-downloadview’s version.
Notes & references
[1] | https://www.python.org/ |
[2] | (1, 2) https://pip.pypa.io/ |
Configure¶
Here is the list of Django settings for django-downloadview.
INSTALLED_APPS¶
There is no need to register this application in INSTALLED_APPS
.
MIDDLEWARE_CLASSES¶
If you plan to setup reverse-proxy optimizations,
add django_downloadview.SmartDownloadMiddleware
to MIDDLEWARE_CLASSES
.
It is a response middleware. Move it after middlewares that compute the
response content such as gzip middleware.
Example:
MIDDLEWARE = [
"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.SmartDownloadMiddleware",
]
DEFAULT_FILE_STORAGE¶
django-downloadview offers a built-in signed file storage, which cryptographically signs requested file URLs with the Django’s built-in TimeStampSigner.
To utilize the signed storage views you can configure
DEFAULT_FILE_STORAGE='django_downloadview.storage.SignedStorage'
The signed file storage system inserts a X-Signature
header to the requested file
URLs, and they can then be verified with the supplied signature_required
wrapper function:
from django.conf.urls import url, url_patterns
from django_downloadview import ObjectDownloadView
from django_downloadview.decorators import signature_required
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')
urlpatterns = [
path('download/<str:slug>/', signature_required(download),
]
Make sure to test the desired functionality after configuration.
DOWNLOADVIEW_URL_EXPIRATION¶
Number of seconds signed download URLs are valid before expiring.
Default value for this flag is None and URLs never expire.
DOWNLOADVIEW_BACKEND¶
This setting is used by
SmartDownloadMiddleware
.
It is the import string of a callable (typically a class) of an optimization
backend (typically a BaseDownloadMiddleware
subclass).
Example:
DOWNLOADVIEW_BACKEND = "django_downloadview.nginx.XAccelRedirectMiddleware"
See Optimize streaming for a list of available backends (middlewares).
When django_downloadview.SmartDownloadMiddleware
is in your
MIDDLEWARE_CLASSES
, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.
DOWNLOADVIEW_RULES¶
This setting is used by
SmartDownloadMiddleware
.
It is a list of positional arguments or keyword arguments that will be used to
instanciate class mentioned as DOWNLOADVIEW_BACKEND
.
Each item in the list can be either a list of positional arguments, or a dictionary of keyword arguments. One item cannot contain both positional and keyword arguments.
Here is an example containing one rule using keyword arguments:
DOWNLOADVIEW_RULES = [
{
"source_url": "/media/nginx/",
"destination_url": "/nginx-optimized-by-middleware/",
},
]
See Optimize streaming for details about builtin backends (middlewares) and their options.
When django_downloadview.SmartDownloadMiddleware
is in your
MIDDLEWARE_CLASSES
, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.
Setup views¶
Setup views depending on your needs:
- 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 generate a file dynamically;
- bases and mixins to make your own.
ObjectDownloadView¶
ObjectDownloadView
serves files managed in models with file fields
such as FileField
or
ImageField
.
Use this view like Django’s builtin
DetailView
.
Additional options allow you to store file metadata (size, content-type, …) in the model, as deserialized fields.
Simple example¶
Given a model with a FileField
:
from django.db import models
class Document(models.Model):
slug = models.SlugField()
file = models.FileField(upload_to="object")
Setup a view to stream the file
attribute:
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)
ObjectDownloadView
inherits from
BaseDetailView
, i.e. it expects either
slug
or pk
:
from django.urls import re_path
from demoproject.object import views
app_name = "object"
urlpatterns = [
re_path(
r"^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
views.default_file_view,
name="default_file",
),
]
Base options¶
ObjectDownloadView
inherits from
DownloadMixin
, which has various
options such as basename
or attachment
.
Serving specific file field¶
If your model holds several file fields, or if the file field name is not
“file”, you can use ObjectDownloadView.file_field
to specify the field
to use.
Here is a model where there are two file fields:
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")
Then here is the code to serve “another_file” instead of the default “file”:
from django_downloadview import ObjectDownloadView
from demoproject.object.models import Document
#: Serve ``another_file`` attribute of ``Document`` model.
another_file_view = ObjectDownloadView.as_view(
model=Document, file_field="another_file"
)
Mapping file attributes to model’s¶
Sometimes, you use Django model to store file’s metadata. Some of this metadata can be used when you serve the file.
As an example, let’s consider the client-side basename lives in model and not in storage:
from django.db import models
class Document(models.Model):
slug = models.SlugField()
file = models.FileField(upload_to="object")
basename = models.CharField(max_length=100)
Then you can configure the ObjectDownloadView.basename_field
option:
from django_downloadview import ObjectDownloadView
from demoproject.object.models import Document
#: Serve ``file`` attribute of ``Document`` model, using client-side filename
#: from model.
deserialized_basename_view = ObjectDownloadView.as_view(
model=Document, basename_field="basename"
)
Note
basename
could have been a model’s property instead of a CharField
.
See details below for a full list of options.
API reference¶
StorageDownloadView¶
StorageDownloadView
serves 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.
Simple example¶
Given a storage:
from django.core.files.storage import FileSystemStorage
storage = FileSystemStorage()
Setup a view to stream files in storage:
from django_downloadview import StorageDownloadView
storage = FileSystemStorage()
#: Serve file using ``path`` argument.
static_path = StorageDownloadView.as_view(storage=storage)
The view accepts a path
argument you can setup either in as_view
or
via URLconfs:
from django.urls import re_path
from demoproject.storage import views
app_name = "storage"
urlpatterns = [
re_path(
r"^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
views.static_path,
name="static_path",
),
]
Base options¶
StorageDownloadView
inherits from
DownloadMixin
, which has various
options such as basename
or attachment
.
Computing path dynamically¶
Override the StorageDownloadView.get_path()
method to adapt path
resolution to your needs.
As an example, here is the same view as above, but the path is converted to uppercase:
from django_downloadview import StorageDownloadView
storage = FileSystemStorage()
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)
API reference¶
PathDownloadView¶
PathDownloadView
serves file given a path on local filesystem.
Use this view whenever you just have a path, outside storage or model.
Warning
Take care of path validation, especially if you compute paths from user input: an attacker may be able to download files from arbitrary locations. In most cases, you should consider managing files in storages, because they implement default security mechanisms.
Simple example¶
Setup a view to stream files given path:
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)
Base options¶
PathDownloadView
inherits from
DownloadMixin
, which has various
options such as basename
or attachment
.
Computing path dynamically¶
Override the PathDownloadView.get_path()
method to adapt path
resolution to your needs:
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!'.
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()
The view accepts a path
argument you can setup either in as_view
or
via URLconfs:
from django.urls import path, re_path
from demoproject.path import views
app_name = "path"
urlpatterns = [
path("static-path/", views.static_path, name="static_path"),
re_path(
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
views.dynamic_path,
name="dynamic_path",
),
]
API reference¶
HTTPDownloadView¶
HTTPDownloadView
serves a file given an URL., i.e. it acts like
a proxy.
This view is particularly handy when:
- the client does not have access to the file resource, while your Django server does.
- the client does trust your server, your server trusts a third-party, you do not want to bother the client with the third-party.
Simple example¶
Setup a view to stream files given URL:
from django_downloadview import HTTPDownloadView
class SimpleURLDownloadView(HTTPDownloadView):
def get_url(self):
"""Return URL of hello-world.txt file on GitHub."""
return (
"https://raw.githubusercontent.com"
"/jazzband/django-downloadview"
"/b7f660c5e3f37d918b106b02c5af7a887acc0111"
"/demo/demoproject/download/fixtures/hello-world.txt"
)
class GithubAvatarDownloadView(HTTPDownloadView):
def get_url(self):
return "https://avatars0.githubusercontent.com/u/235204"
simple_url = SimpleURLDownloadView.as_view()
avatar_url = GithubAvatarDownloadView.as_view()
Base options¶
HTTPDownloadView
inherits from
DownloadMixin
, which has various
options such as basename
or attachment
.
API reference¶
VirtualDownloadView¶
VirtualDownloadView
serves files that do not live on disk.
Use it when you want to stream a file which content is dynamically generated
or which lives in memory.
It is all about overriding VirtualDownloadView.get_file()
method so that
it returns a suitable file wrapper…
Note
Current implementation does not support reverse-proxy optimizations, because content is actually generated within Django, not stored in some third-party place.
Base options¶
VirtualDownloadView
inherits from
DownloadMixin
, which has various
options such as basename
or attachment
.
Serve text (string or unicode) or bytes¶
Let’s consider you build text dynamically, as a bytes or string or unicode
object. Serve it with Django’s builtin
ContentFile
wrapper:
from io import StringIO
from django.core.files.base import ContentFile
class TextDownloadView(VirtualDownloadView):
def get_file(self):
"""Return :class:`django.core.files.base.ContentFile` object."""
return ContentFile(b"Hello world!\n", name="hello-world.txt")
Serve StringIO¶
StringIO
object lives in memory. Let’s wrap it in some
download view via VirtualFile
:
from io import StringIO
from django.core.files.base import ContentFile
from django_downloadview import TextIteratorIO, VirtualDownloadView, VirtualFile
class StringIODownloadView(VirtualDownloadView):
def get_file(self):
"""Return wrapper on ``six.StringIO`` object."""
file_obj = StringIO("Hello world!\n")
Stream generated content¶
Let’s consider you have a generator function (yield
) or an iterator object
(__iter__()
):
def generate_hello():
yield "Hello "
yield "world!"
Stream generated content using VirtualDownloadView
,
VirtualFile
and
BytesIteratorIO
:
from django.core.files.base import ContentFile
class GeneratedDownloadView(VirtualDownloadView):
def get_file(self):
"""Return wrapper on ``StringIteratorIO`` object."""
file_obj = TextIteratorIO(generate_hello())
API reference¶
Make your own view¶
DownloadMixin¶
The 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 django_downloadview.views.BaseDownloadView
class is a base
class to create download views. It inherits DownloadMixin and
django.views.generic.base.View
.
The only thing it does is to implement
get
: it triggers
DownloadMixin's render_to_response
.
Serving a file inline rather than as attachment¶
Use attachment
to make a view serve a file inline rather
than as attachment, i.e. to display the file as if it was an internal part of a
page rather than triggering “Save file as…” prompt.
See details in attachment API documentation
.
from django_downloadview import ObjectDownloadView
#: Serve ``file`` attribute of ``Document`` model, inline (not as attachment).
Handling http not modified responses¶
Sometimes, you know the latest date and time the content was generated at, and
you know a new request would generate exactly the same content. In such a case,
you should implement was_modified_since()
in your
view.
Note
Default was_modified_since()
implementation
trusts file wrapper’s was_modified_since
if any. Else (if calling
was_modified_since()
raises NotImplementedError
or
AttributeError
) it returns True
, i.e. it assumes the file was
modified.
As an example, the download views above always generate “Hello world!”… so, if the client already downloaded it, we can safely return some HTTP “304 Not Modified” response:
from django.core.files.base import ContentFile
from django_downloadview import VirtualDownloadView
class TextDownloadView(VirtualDownloadView):
def get_file(self):
"""Return :class:`django.core.files.base.ContentFile` object."""
return ContentFile("Hello world!", name='hello-world.txt')
def was_modified_since(self, file_instance, since):
return False # Never modified, always "Hello world!".
Optimize streaming¶
Some reverse proxies allow applications to delegate actual download to the proxy:
- with Django, manage permissions, generate files…
- let the reverse proxy serve the file.
As a result, you get increased performance: reverse proxies are more efficient than Django at serving static files.
Supported features grid¶
Supported features depend on backend. Given the file you want to stream, the backend may or may not be able to handle it:
View / File | Nginx | Apache | Lighttpd |
---|---|---|---|
PathDownloadView | Yes, local filesystem. | Yes, local filesystem. | Yes, local filesystem. |
StorageDownloadView | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. |
ObjectDownloadView | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. |
HTTPDownloadView | Yes. | No. | No. |
VirtualDownloadView | No. | No. | No. |
As an example, Nginx X-Accel handles URL for
internal redirects, so it can manage
HTTPFile
; whereas Apache X-Sendfile handles absolute path, so it can only deal with files
on local filesystem.
There are currently no optimizations to stream in-memory files, since they only live on Django side, i.e. they do not persist after Django returned a response. Note: there is a feature request about “local cache” for streamed files [2].
How does it work?¶
View return some DownloadResponse
instance, which itself carries a file wrapper.
django-downloadview provides response middlewares and decorators that are
able to capture DownloadResponse
instances and convert them to
ProxiedDownloadResponse
.
The ProxiedDownloadResponse
is specific
to the reverse-proxy (backend): it tells the reverse proxy to stream some
resource.
Note
The feature is inspired by Django's TemplateResponse
Available optimizations¶
Here are optimizations builtin django_downloadview:
Nginx¶
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.
See Nginx X-accel documentation [1] for details.
Known limitations¶
- Nginx needs access to the resource by URL (proxy) or path (location).
- Thus
VirtualFile
and any generated files cannot be streamed by Nginx.
Given a view¶
Let’s consider the following view:
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
storage_dir = os.path.join(settings.MEDIA_ROOT, "nginx")
storage = FileSystemStorage(
location=storage_dir, base_url="".join([settings.MEDIA_URL, "nginx/"])
)
optimized_by_middleware = StorageDownloadView.as_view(
storage=storage, path="hello-world.txt"
)
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
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.
Setup XAccelRedirect middlewares¶
Make sure django_downloadview.SmartDownloadMiddleware
is in
MIDDLEWARE
of your Django settings.
Example:
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django_downloadview.SmartDownloadMiddleware",
]
# END middlewares
Then set django_downloadview.nginx.XAccelRedirectMiddleware
as
DOWNLOADVIEW_BACKEND
:
"""Could also be:
Then register as many DOWNLOADVIEW_RULES
as you wish:
"source_url": "/media/nginx/",
"destination_url": "/nginx-optimized-by-middleware/",
},
]
# END rules
DOWNLOADVIEW_RULES += [
Each item in DOWNLOADVIEW_RULES
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
.
Per-view setup with x_accel_redirect decorator¶
Middlewares should be enough for most use cases, but you may want per-view
configuration. For nginx, there is x_accel_redirect
:
As an example:
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
)
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/",
)
Test responses with assert_x_accel_redirect¶
Use assert_x_accel_redirect()
function as a shortcut in your tests.
import os
from django.core.files.base import ContentFile
import django.test
from django.urls import reverse
from django_downloadview.nginx import assert_x_accel_redirect
from demoproject.nginx.views import storage, storage_dir
def setup_file():
if not os.path.exists(storage_dir):
os.makedirs(storage_dir)
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
class OptimizedByMiddlewareTestCase(django.test.TestCase):
def test_response(self):
"""'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="/nginx-optimized-by-middleware/hello-world.txt",
expires=None,
with_buffering=None,
limit_rate=None,
)
class OptimizedByDecoratorTestCase(django.test.TestCase):
def test_response(self):
"""'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",
basename="hello-world.txt",
redirect_url="/nginx-optimized-by-decorator/hello-world.txt",
expires=None,
with_buffering=None,
limit_rate=None,
)
The tests above assert the Django part is OK. Now let’s configure nginx.
Setup Nginx¶
See Nginx X-accel documentation [1] for details.
Here is what you could have in /etc/nginx/sites-available/default
:
charset utf-8;
# Django-powered service.
upstream frontend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80 default;
# File-download proxy.
#
# Will serve /var/www/files/myfile.tar.gz when passed URI
# like /optimized-download/myfile.tar.gz
#
# See http://wiki.nginx.org/X-accel
# and https://django-downloadview.readthedocs.io
#
location /proxied-download {
internal;
# Location to files on disk.
alias /var/www/files/;
}
# Proxy to Django-powered frontend.
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://frontend;
}
}
… where specific configuration is the location /optimized-download
section.
Note
/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>
.
Assert everything goes fine with healthchecks¶
Healthchecks are the best way to check the complete setup.
Common issues¶
Unknown charset "utf-8" to override
¶Add charset utf-8;
in your nginx configuration file.
open() "path/to/something" failed (2: No such file or directory)
¶Check your settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR
in Django
configuration VS alias
in nginx configuration: in a standard configuration,
they should be equal.
References
[1] | (1, 2) http://wiki.nginx.org/X-accel |
Apache¶
If you serve Django behind Apache, then you can delegate the file streaming to Apache and get increased performance:
- lower resources used by Python/Django workers ;
- faster download.
See Apache mod_xsendfile documentation [1] for details.
Known limitations¶
- Apache needs access to the resource by path on local filesystem.
- Thus only files that live on local filesystem can be streamed by Apache.
Given a view¶
Let’s consider the following view:
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
storage_dir = os.path.join(settings.MEDIA_ROOT, "apache")
storage = FileSystemStorage(
location=storage_dir, base_url="".join([settings.MEDIA_URL, "apache/"])
)
optimized_by_middleware = StorageDownloadView.as_view(
storage=storage, path="hello-world.txt"
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
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.
Setup XSendfile middlewares¶
Make sure django_downloadview.SmartDownloadMiddleware
is in
MIDDLEWARE_CLASSES
of your Django settings.
Example:
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django_downloadview.SmartDownloadMiddleware",
]
# END middlewares
Then set django_downloadview.apache.XSendfileMiddleware
as
DOWNLOADVIEW_BACKEND
:
Then register as many DOWNLOADVIEW_RULES
as you wish:
"destination_url": "/nginx-optimized-by-middleware/",
# Bypass global default backend with additional argument "backend".
# Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be
# enough. Here, the django_downloadview demo project needs to
# demonstrate usage of several backends.
"backend": "django_downloadview.apache.XSendfileMiddleware",
},
{
"source_url": "/media/lighttpd/",
"destination_dir": "/lighttpd-optimized-by-middleware/",
# Test/development settings.
Each item in DOWNLOADVIEW_RULES
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_dir
.
Per-view setup with x_sendfile decorator¶
Middlewares should be enough for most use cases, but you may want per-view
configuration. For Apache, there is x_sendfile
:
As an example:
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
from django_downloadview.apache import x_sendfile
)
optimized_by_decorator = x_sendfile(
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
source_url=storage.base_url,
destination_dir="/apache-optimized-by-decorator/",
)
Test responses with assert_x_sendfile¶
Use assert_x_sendfile()
function as a shortcut in your tests.
import os
from django.core.files.base import ContentFile
import django.test
from django.urls import reverse
from django_downloadview.apache import assert_x_sendfile
from demoproject.apache.views import storage, storage_dir
def setup_file():
if not os.path.exists(storage_dir):
os.makedirs(storage_dir)
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
class OptimizedByMiddlewareTestCase(django.test.TestCase):
def test_response(self):
"""'apache:optimized_by_middleware' returns X-Sendfile response."""
setup_file()
url = reverse("apache:optimized_by_middleware")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/apache-optimized-by-middleware/hello-world.txt",
)
class OptimizedByDecoratorTestCase(django.test.TestCase):
def test_response(self):
"""'apache:optimized_by_decorator' returns X-Sendfile response."""
setup_file()
url = reverse("apache:optimized_by_decorator")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/apache-optimized-by-decorator/hello-world.txt",
)
The tests above assert the Django part is OK. Now let’s configure Apache.
Setup Apache¶
See Apache mod_xsendfile documentation [1] for details.
Assert everything goes fine with healthchecks¶
Healthchecks are the best way to check the complete setup.
References
[1] | (1, 2) https://tn123.org/mod_xsendfile/ |
Lighttpd¶
If you serve Django behind Lighttpd, then you can delegate the file streaming to Lighttpd and get increased performance:
- lower resources used by Python/Django workers ;
- faster download.
See Lighttpd X-Sendfile documentation [1] for details.
Note
Currently, django_downloadview supports X-Sendfile
, but not
X-Sendfile2
. If you need X-Sendfile2
or know how to handle it,
check X-Sendfile2 feature request on django_downloadview’s bugtracker [2].
Known limitations¶
- Lighttpd needs access to the resource by path on local filesystem.
- Thus only files that live on local filesystem can be streamed by Lighttpd.
Given a view¶
Let’s consider the following view:
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
storage_dir = os.path.join(settings.MEDIA_ROOT, "lighttpd")
storage = FileSystemStorage(
location=storage_dir, base_url="".join([settings.MEDIA_URL, "lighttpd/"])
)
optimized_by_middleware = StorageDownloadView.as_view(
storage=storage, path="hello-world.txt"
)
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
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.
Setup XSendfile middlewares¶
Make sure django_downloadview.SmartDownloadMiddleware
is in
MIDDLEWARE_CLASSES
of your Django settings.
Example:
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django_downloadview.SmartDownloadMiddleware",
]
# END middlewares
Then set django_downloadview.lighttpd.XSendfileMiddleware
as
DOWNLOADVIEW_BACKEND
:
# BEGIN rules
Then register as many DOWNLOADVIEW_RULES
as you wish:
"destination_url": "/nginx-optimized-by-middleware/",
# Bypass global default backend with additional argument "backend".
# Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be
# enough. Here, the django_downloadview demo project needs to
# demonstrate usage of several backends.
"backend": "django_downloadview.lighttpd.XSendfileMiddleware",
},
]
# Test/development settings.
Each item in DOWNLOADVIEW_RULES
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_dir
.
Per-view setup with x_sendfile decorator¶
Middlewares should be enough for most use cases, but you may want per-view
configuration. For Lighttpd, there is x_sendfile
:
As an example:
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
from django_downloadview.lighttpd import x_sendfile
optimized_by_decorator = x_sendfile(
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
source_url=storage.base_url,
destination_dir="/lighttpd-optimized-by-decorator/",
)
Test responses with assert_x_sendfile¶
Use assert_x_sendfile()
function as a shortcut in your tests.
import os
from django.core.files.base import ContentFile
import django.test
from django.urls import reverse
from django_downloadview.lighttpd import assert_x_sendfile
from demoproject.lighttpd.views import storage, storage_dir
def setup_file():
if not os.path.exists(storage_dir):
os.makedirs(storage_dir)
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
class OptimizedByMiddlewareTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:optimized_by_middleware' returns X-Sendfile response."""
setup_file()
url = reverse("lighttpd:optimized_by_middleware")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-optimized-by-middleware/hello-world.txt",
)
class OptimizedByDecoratorTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:optimized_by_decorator' returns X-Sendfile response."""
setup_file()
url = reverse("lighttpd:optimized_by_decorator")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-optimized-by-decorator/hello-world.txt",
)
The tests above assert the Django part is OK. Now let’s configure Lighttpd.
Setup Lighttpd¶
See Lighttpd X-Sendfile documentation [1] for details.
Assert everything goes fine with healthchecks¶
Healthchecks are the best way to check the complete setup.
References
[1] | (1, 2) http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file |
[2] | https://github.com/jazzband/django-downloadview/issues/67 |
Notes & references
[1] | https://github.com/jazzband/django-downloadview/issues?labels=optimizations |
[2] | https://github.com/jazzband/django-downloadview/issues/70 |
Write tests¶
django_downloadview embeds test utilities:
temporary_media_root()
assert_download_response()
setup_view()
assert_x_accel_redirect()
temporary_media_root¶
assert_download_response¶
Examples, related to StorageDownloadView demo:
from django.core.files.base import ContentFile
from django.http.response import HttpResponseNotModified
import django.test
from django.urls import reverse
from django_downloadview import (
assert_download_response,
setup_view,
temporary_media_root,
)
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",
)
@temporary_media_root()
def test_not_modified_download_response(self):
"""'storage:static_path' sends not modified response if unmodified."""
setup_file("1.txt")
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
year = datetime.date.today().year + 4
response = self.client.get(
url,
HTTP_IF_MODIFIED_SINCE=f"Sat, 29 Oct {year} 19:43:31 GMT",
)
self.assertTrue(isinstance(response, HttpResponseNotModified))
@temporary_media_root()
def test_modified_since_download_response(self):
"""'storage:static_path' streams file if modified."""
setup_file("1.txt")
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
response = self.client.get(
setup_view¶
Example, related to StorageDownloadView demo:
import datetime
import unittest
from django_downloadview import (
assert_download_response,
setup_view,
temporary_media_root,
)
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")
Write healthchecks¶
In the previous 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 [1] and django-doctor [2] 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.
Notes & references
[1] | https://pypi.python.org/pypi/hospital |
[2] | https://pypi.python.org/pypi/django-doctor |
File wrappers¶
A view return 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 [1] you can use within
django-downloadview
:
django.core.files.File
wraps a file that live on local filesystem, initialized with a path.django-downloadview
uses this wrapper in PathDownloadView.django.db.models.fields.files.FieldFile
wraps a file that is managed in a model.django-downloadview
uses this wrapper in ObjectDownloadView.django.core.files.base.ContentFile
wraps a bytes, string or unicode object. You may use it with VirtualDownloadView.
django-downloadview builtins¶
django-downloadview
implements additional file wrappers:
StorageFile
wraps a file that is managed via a storage (but not necessarily via a model). StorageDownloadView uses this wrapper.HTTPFile
wraps a file that lives at some (remote) location, initialized with an URL. HTTPDownloadView uses this wrapper.VirtualFile
wraps a file that lives in memory, i.e. built as a string. This is a convenient wrapper to use in VirtualDownloadView subclasses.
Low-level IO utilities¶
django-downloadview provides two classes to implement file-like objects whose content is dynamically generated:
TextIteratorIO
for generated text;BytesIteratorIO
for generated bytes.
These classes may be handy to serve dynamically generated files. See VirtualDownloadView for details.
Tip
Text or bytes? (formerly “unicode or str?”) As django-downloadview is meant to serve files, as opposed to read or parse files, what matters is file contents is preserved. django-downloadview tends to handle files in binary mode and as bytes.
Responses¶
Views return DownloadResponse
.
Middlewares (and decorators) are given the opportunity to capture responses and
convert them to ProxiedDownloadResponse
.
DownloadResponse¶
ProxiedDownloadResponse¶
Migrating from django-sendfile¶
django-sendfile [1] is a wrapper around web-server specific methods for sending files to web clients. See Alternatives and related projects for details about this project.
django-downloadview provides a port of django-sendfile's main function
.
Warning
django-downloadview can replace the following django-sendfile’s
backends: nginx
, xsendfile
, simple
. But it currently cannot
replace mod_wsgi
backend.
Here are tips to migrate from django-sendfile to django-downloadview…
- In your project’s and apps dependencies, replace
django-sendfile
bydjango-downloadview
. - In your Python scripts, replace
import sendfile
andfrom sendfile
byimport django_downloadview
andfrom django_downloadview
. You get something likefrom django_downloadview import sendfile
- Adapt your settings as explained in Configure. Pay attention to:
- replace
sendfile
bydjango_downloadview
inINSTALLED_APPS
. - replace
SENDFILE_BACKEND
byDOWNLOADVIEW_BACKEND
- setup
DOWNLOADVIEW_RULES
. It replacesSENDFILE_ROOT
and can do more. - register
django_downloadview.SmartDownloadMiddleware
inMIDDLEWARE_CLASSES
.
- replace
- Change your tests if any. You can no longer use django-senfile’s
development
backend. See Write tests for django-downloadview’s toolkit. - Here you are! … or please report your story/bug at django-downloadview’s bugtracker [2] ;)
API reference¶
References
[1] | http://pypi.python.org/pypi/django-sendfile |
[2] | https://github.com/jazzband/django-downloadview/issues |
Demo project¶
Demo folder in project’s repository [1] 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¶
Deploy the demo¶
System requirements:
Python [2] version 3.6+, available as
python
command.Note
You may use Virtualenv [3] to make sure the active
python
is the right one.make
andwget
to use the providedMakefile
.
Execute:
git clone git@github.com:jazzband/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 demo/demoproject/
as a sandbox.
About django-downloadview¶
Vision¶
django-downloadview tries to simplify the development of “download” views using Django [1] 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.
Notes & references
See also
[1] | https://www.djangoproject.com |
License¶
Copyright (c) 2012-2014, Benoît Bryon. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of django-downloadview nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Authors & contributors¶
Maintainer: Benoît Bryon <benoit@marmelune.net>
Original code by PeopleDoc team:
- Nicolas Tobo <https://github.com/nicolastobo>
- Lauréline Guérin <https://github.com/zebuline>
- Gregory Tappero <https://github.com/gregtap>
- Rémy Hubscher <https://github.com/natim>
- Benoît Bryon <benoit@marmelune.net>
- Aleksi Häkli <https://github.com/aleksihakli>
- Johnt Hagen <johnthagen@gmail.com>
- Fabre Florian <ffabre@hybird.org>
- Peter Marheine <peter@taricorp.net>
- Hasan Ramezani <hasan.r67@gmail.com>
- Jannis Leidel <jannis@leidel.info>
- Erik Dykema <dykema@gmail.com>
- Nikhil Benesch <nikhil.benesch@gmail.com>
- Omer Katz <omer.drow@gmail.com>
- René Leonhardt <rene.leonhardt@gmail.com>
- Adam Chainz <adam@adamj.eu>
- Martin Bächtold <martin@baechtold.me>
- Tim Gates <tim.gates@iress.com>
- zero13cool <zero13cool@yandex.ru>
Changelog¶
This document describes changes between past releases. For information about future releases, check milestones [1] and Vision.
2.3 (unreleased)¶
- Drop Django 3.0 support
- Add Django 3.2 support
- Add support for Python 3.10
- Add support for Django 4.0
2.2 (unreleased)¶
- Remove support for Python 3.5 and Django 1.11
- Add support for Python 3.9 and Django 3.1
- Remove old urls syntax and adopt the new one
- Move the project to the jazzband organization
- Adopt black automatic formatting rules
2.1.1 (2020-01-14)¶
- Fix missing function parameter. (#152)
2.1 (2020-01-13)¶
- Add a SignedFileSystemStorage that signs file URLs for clients. (#151)
2.0 (2020-01-07)¶
- Drop support for Python 2.7.
- Add black and isort.
1.10 (2020-01-07)¶
- Introduced support from Django 1.11, 2.2 and 3.0.
- Drop support of Django 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 2.0 and 2.1
1.9 (2016-03-15)¶
- Feature #112 - Introduced support of Django 1.9.
- Feature #113 - Introduced support of Python 3.5.
- Feature #116 -
HTTPFile
hascontent_type
property. It makesHTTPDownloadView
proxyContent-Type
header from remote location.
1.8 (2015-07-20)¶
Bugfixes.
- Bugfix #103 -
PathDownloadView.get_file()
makes a single call toPathDownloadView.get_file()
(was doing it twice). - Bugfix #104 - Pass numeric timestamp to Django’s
was_modified_since()
(was passing a datetime).
1.7 (2015-06-13)¶
Bugfixes.
- Bugfix #87 - Filenames with commas are now supported. In download responses, filename is now surrounded by double quotes.
- Bugfix #97 -
HTTPFile
proxies bytes asBytesIteratorIO
(was undecoded urllib3 file object).StringIteratorIO
has been split intoTextIteratorIO
andBytesIteratorIO
.StringIteratorIO
is deprecated but kept for backward compatibility as an alias forTextIteratorIO
. - Bugfix #92 - Run demo using
make demo runserver
(was broken). - Feature #99 - Tox runs project’s tests with Python 2.7, 3.3 and 3.4, and with Django 1.5 to 1.8.
- Refactoring #98 - Refreshed development environment: packaging, Tox and Sphinx.
1.6 (2014-03-03)¶
Python 3 support, development environment refactoring.
- Feature #46: introduced support for Python>=3.3.
- Feature #80: added documentation about “how to serve a file inline VS how to
serve a file as attachment”. Improved documentation of views’ base options
inherited from
DownloadMixin
. - Feature #74: the Makefile in project’s repository no longer creates a virtualenv. Developers setup the environment as they like, i.e. using virtualenv, virtualenvwrapper or whatever. Tests are run with tox.
1.5 (2013-11-29)¶
X-Sendfile support and helpers to migrate for django-sendfile.
- Feature #2 - Introduced support of Lighttpd’s x-Sendfile.
- Feature #36 - Introduced support of Apache’s mod_xsendfile.
- Feature #41 -
django_downloadview.sendfile
is a port of django-sendfile’ssendfile
function. The documentation contains notes about migrating from django-sendfile to django-downloadview.
1.4 (2013-11-24)¶
Bugfixes and documentation features.
- Bugfix #43 -
ObjectDownloadView
returns HTTP 404 if model instance’s file field is empty (was HTTP 500). - Bugfix #7 - Special characters in file names (
Content-Disposition
header) are urlencoded. An US-ASCII fallback is also provided. - Feature #10 - django-downloadview is registered on djangopackages.com.
- Feature #65 - INSTALL documentation shows “known good set” (KGS) of versions, i.e. versions that have been used in test environment.
1.3 (2013-11-08)¶
Big refactoring around middleware configuration, API readability and documentation.
Bugfix #57 -
PathDownloadView
opens files in binary mode (was text mode).Bugfix #48 - Fixed
basename
assertion inassert_download_response
: checksContent-Disposition
header.Bugfix #49 - Fixed
content
assertion inassert_download_response
: checks only response’sstreaming_content
attribute.Bugfix #60 -
VirtualFile.__iter__
usesforce_bytes()
to support both “text-mode” and “binary-mode” content. See https://code.djangoproject.com/ticket/21321Feature #50 - Introduced
django_downloadview.DownloadDispatcherMiddleware
that iterates over a list of configurable download middlewares. Allows to plug several download middlewares with different configurations.This middleware is mostly dedicated to internal usage. It is used by
SmartDownloadMiddleware
described below.Feature #42 - Documentation shows how to stream generated content (yield). Introduced
django_downloadview.StringIteratorIO
.Refactoring #51 - Dropped support of Python 2.6
Refactoring #25 - Introduced
django_downloadview.SmartDownloadMiddleware
which allows to setup multiple optimization rules for one backend.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
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 asProxiedDownloadMiddleware
.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)¶
Bugfixes and documentation improvements.
- Bugfix #26 - Prevented computation of virtual file’s size, unless the file wrapper implements was_modified_since() method.
- 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)¶
Various improvements. Contains backward incompatible changes.
- Added HTTPDownloadView to proxy to arbitrary URL.
- Added VirtualDownloadView to support files living in memory.
- Using StreamingHttpResponse introduced with Django 1.5. Makes Django 1.5 a requirement!
- Added
django_downloadview.test.assert_download_response
utility. - Download views and response now use file wrappers. Most logic around file attributes, formerly in views, moved to wrappers.
- Replaced DownloadView by PathDownloadView and StorageDownloadView. Use the right one depending on the use case.
1.0 (2012-12-04)¶
- Introduced optimizations for Nginx X-Accel: a middleware and a decorator
- Introduced generic views: DownloadView and ObjectDownloadView
- Initialized project
Notes & references
[1] | https://github.com/jazzband/django-downloadview/milestones |
Contributing¶
This is a Jazzband project. By contributing you agree to abide by the Contributor Code of Conduct and follow the guidelines.
This document provides guidelines for people who want to contribute to django-downloadview.
Create tickets¶
Please use the bugtracker [1] before starting some work:
- check if the bug or feature request has already been filed. It may have been answered too!
- else create a new ticket.
- if you plan to contribute, tell us, so that we are given an opportunity to give feedback as soon as possible.
- Then, in your commit messages, reference the ticket with some
refs #TICKET-ID
syntax.
Use topic branches¶
- Work in branches.
- Prefix your branch with the ticket ID corresponding to the issue. As an
example, if you are working on ticket #23 which is about contribute
documentation, name your branch like
23-contribute-doc
. - If you work in a development branch and want to refresh it with changes from master, please rebase [2] or merge-based rebase [3], i.e. do not merge master.
Fork, clone¶
Clone django-downloadview repository (adapt to use your own fork):
git clone git@github.com:jazzband/django-downloadview.git
cd django-downloadview/
Usual actions¶
The Makefile is the reference card for usual actions in development environment:
- Install development toolkit with pip [4]:
make develop
. - Run tests with tox [5]:
make test
. - Build documentation:
make documentation
. It builds Sphinx [6] documentation in var/docs/html/index.html. - Release project with zest.releaser [7]:
make release
. - Cleanup local repository:
make clean
,make distclean
andmake maintainer-clean
.
See also make help
.
Demo project included¶
The demo included in project’s repository is part of the tests and documentation. Maintain it along with code and documentation.
Notes & references
[1] | https://github.com/jazzband/django-downloadview/issues |
[2] | http://git-scm.com/book/en/Git-Branching-Rebasing |
[3] | https://tech.people-doc.com/psycho-rebasing.html |
[4] | https://pypi.python.org/pypi/pip/ |
[5] | https://tox.readthedocs.io/ |
[6] | https://pypi.python.org/pypi/Sphinx/ |
[7] | https://pypi.python.org/pypi/zest.releaser/ |