Compare commits

..

No commits in common. "master" and "1.0" have entirely different histories.
master ... 1.0

153 changed files with 1711 additions and 7004 deletions

View file

@ -1,8 +0,0 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View file

@ -1,40 +0,0 @@
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-downloadview'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-downloadview/upload

View file

@ -1,61 +0,0 @@
name: Test
on: [push, pull_request]
jobs:
build:
name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['4.2', '5.0', '5.1', '5.2', 'main']
exclude:
# Django 5.0 dropped support for Python <3.10
- django-version: '5.0'
python-version: '3.8'
- django-version: '5.0'
python-version: '3.9'
- django-version: 'main'
python-version: '3.8'
- django-version: 'main'
python-version: '3.9'
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
- name: Tox tests
run: |
tox -v
env:
DJANGO: ${{ matrix.django-version }}
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

29
.gitignore vendored
View file

@ -1,32 +1,19 @@
# Local binaries.
# Local binaries (they are generated).
/bin/
# Libraries.
/lib/
# Data files.
/var/
coverage.xml
.coverage/
# Python files.
*.pyc
*.pyo
*.egg-info
# Tox files.
/.tox/
.eggs
*.egg-info
# Libraries.
/lib/
# Virtualenv files (created by tox).
/build/
/dist/
# Data files.
/var/
# Virtual environments (created by user).
/venv/
# Coverage output.
/.coverage
# Editors' temporary buffers.
# Editor(s) temporary files.
.*.swp
*~
.idea

View file

@ -1,16 +0,0 @@
[settings]
# # Needed for black compatibility
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
line_length=88
combine_as_imports=True
# List sections with django and
known_django=django
known_downloadview=django_downloadview
sections=FUTURE,STDLIB,DJANGO,DOWNLOADVIEW,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
# If set, imports will be sorted within their section independent to the import_type.
force_sort_within_sections=True

View file

@ -1,59 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
- id: file-contents-sorter
files: docs/spelling_wordlist.txt
- repo: https://github.com/pycqa/doc8
rev: v2.0.0
hooks:
- id: doc8
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.29.1
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- id: rst-directive-colons
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
entry: env PRETTIER_LEGACY_CLI=1 prettier
types_or: [javascript, css]
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.39.1
hooks:
- id: eslint
additional_dependencies:
- "eslint@v9.0.0-beta.1"
- "@eslint/js@v9.0.0-beta.1"
- "globals"
files: \.js?$
types: [file]
args:
- --fix
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.14.5'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.11.1
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
hooks:
- id: validate-pyproject

View file

@ -1,18 +0,0 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .

26
AUTHORS
View file

@ -2,27 +2,9 @@
Authors & contributors
######################
Maintainer: Benoît Bryon <benoit@marmelune.net>
Original code by `Novapost <http://www.novapost.fr>`_ team:
Original code by `PeopleDoc <https://www.people-doc.com/>`_ team:
* Adam Chainz <adam@adamj.eu>
* Aleksi Häkli <aleksi.hakli@iki.fi>
* Nicolas Tobo <https://github.com/nicolastobo>
* Lauréline Guérin <https://github.com/zebuline>
* Gregory Tappero <https://github.com/coulix>
* Benoît Bryon <benoit@marmelune.net>
* CJ <cjdreiss@users.noreply.github.com>
* David Wolf <68775926+devidw@users.noreply.github.com>
* Davide Setti <setti.davide89@gmail.com>
* Erik Dykema <dykema@gmail.com>
* Fabre Florian <ffabre@hybird.org>
* Hasan Ramezani <hasan.r67@gmail.com>
* Jannis Leidel <jannis@leidel.info>
* John Hagen <johnthagen@gmail.com>
* Mariusz Felisiak <felisiak.mariusz@gmail.com>
* Martin Bächtold <martin@baechtold.me>
* Nikhil Benesch <nikhil.benesch@gmail.com>
* Omer Katz <omer.drow@gmail.com>
* Peter Marheine <peter@taricorp.net>
* René Leonhardt <rene.leonhardt@gmail.com>
* Rémy HUBSCHER <hubscher.remy@gmail.com>
* Tim Gates <tim.gates@iress.com>
* zero13cool <zero13cool@yandex.ru>

260
CHANGELOG
View file

@ -1,269 +1,9 @@
Changelog
=========
This document describes changes between past releases. For information about
future releases, check `milestones`_ and :doc:`/about/vision`.
2.6.0 (unreleased)
----------------
- No changes yet
2.5.0 (2025-10-28)
----------------
- Upgrade to Django 5.2 and Python 3.14
2.4.0 (2024-08-05)
------------------
- Drop support for Python 3.6
- Escape malicious filenames
- Handle headers in XAccel responses
2.3.0 (2022-01-11)
------------------
- Drop Django 3.0 support
- Add Django 3.2 support
- Add support for Python 3.10
- Add support for Django 4.0
- 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`` has ``content_type`` property. It makes
``HTTPDownloadView`` proxy ``Content-Type`` header from remote location.
1.8 (2015-07-20)
----------------
Bugfixes.
- Bugfix #103 - ``PathDownloadView.get_file()`` makes a single call to
``PathDownloadView.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 as ``BytesIteratorIO`` (was undecoded
urllib3 file object). ``StringIteratorIO`` has been split into
``TextIteratorIO`` and ``BytesIteratorIO``. ``StringIteratorIO`` is
deprecated but kept for backward compatibility as an alias for
``TextIteratorIO``.
- 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`'s ``sendfile`` 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 in ``assert_download_response``:
checks ``Content-Disposition`` header.
- Bugfix #49 - Fixed ``content`` assertion in ``assert_download_response``:
checks only response's ``streaming_content`` attribute.
- Bugfix #60 - ``VirtualFile.__iter__`` uses ``force_bytes()`` to support both
"text-mode" and "binary-mode" content.
See https://code.djangoproject.com/ticket/21321
- Feature #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 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)
----------------
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
.. rubric:: Notes & references
.. target-notes::
.. _`milestones`: https://github.com/jazzband/django-downloadview/milestones

View file

@ -1,46 +0,0 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,102 +0,0 @@
############
Contributing
############
.. image:: https://jazzband.co/static/img/jazzband.svg
:target: https://jazzband.co/
:alt: Jazzband
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_ and follow the `guidelines <https://jazzband.co/about/guidelines>`_.
This document provides guidelines for people who want to contribute to
``django-downloadview``.
**************
Create tickets
**************
Please use the `bugtracker`_ **before** starting some work:
* check if the bug or feature request has already been filed. It may have been
answered too!
* else create a new ticket.
* if you plan to contribute, tell us, so that we are given an opportunity to
give feedback as soon as possible.
* Then, in your commit messages, reference the ticket with some
``refs #TICKET-ID`` syntax.
******************
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`_ or `merge-based rebase`_, i.e. do not merge master.
***********
Fork, clone
***********
Clone ``django-downloadview`` repository (adapt to use your own fork):
.. code:: sh
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`_: ``make develop``.
* Run tests with `tox`_: ``make test``.
* Build documentation: ``make documentation``. It builds `Sphinx`_
documentation in ``var/docs/html/index.html``.
* Release project with `zest.releaser`_: ``make release``.
* Cleanup local repository: ``make clean``, ``make distclean`` and
``make 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.
.. rubric:: Notes & references
.. target-notes::
.. _`bugtracker`:
https://github.com/jazzband/django-downloadview/issues
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html
.. _`pip`: https://pypi.python.org/pypi/pip/
.. _`tox`: https://tox.readthedocs.io/
.. _`Sphinx`: https://pypi.python.org/pypi/Sphinx/
.. _`zest.releaser`: https://pypi.python.org/pypi/zest.releaser/

95
INSTALL
View file

@ -1,88 +1,25 @@
#######
Install
#######
############
Installation
############
.. note::
This project is open-source, published under BSD license.
See :doc:`/about/license` for details.
If you want to install a development environment, please see
:doc:`/contributing`.
If you want to install a development environment, you should go to :doc:`/dev`
documentation.
Install the package with your favorite Python installer. As an example, with
pip:
************
Requirements
************
`django-downloadview` has been tested with `Python`_ 3.7, 3.8, 3.9 and 3.10.
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:
.. literalinclude:: /../setup.py
:language: python
:start-after: BEGIN requirements
:end-before: END requirements
************
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 :file:`setup.py`:
.. code:: python
from setuptools import setup
setup(
install_requires=[
'django-downloadview',
#...
]
# ...
)
Then when you install your main project with your favorite package manager
(like `pip`_), `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`_:
.. code:: sh
.. code-block:: sh
pip install django-downloadview
There is no need to register this application in your Django's
``INSTALLED_APPS`` setting.
*****
Check
*****
Next, you'll have to setup some download view(s). See :doc:`demo project
<demo>` for examples, and :doc:`API documentation <api/django_downloadview>`.
Check `django-downloadview` has been installed:
.. code:: sh
python -c "import django_downloadview;print(django_downloadview.__version__)"
You should get installed `django-downloadview`'s version.
.. rubric:: Notes & references
.. seealso::
* :doc:`/settings`
* :doc:`/about/changelog`
* :doc:`/about/license`
.. target-notes::
.. _`Python`: https://www.python.org/
.. _`pip`: https://pip.pypa.io/
Optionally, you may setup additional :doc:`server optimizations
<optimizations/index>`.

View file

@ -2,7 +2,7 @@
License
#######
Copyright (c) 2012-2014, Benoît Bryon.
Copyright (c) 2012, Benoît Bryon.
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -15,7 +15,7 @@ modification, are permitted provided that the following conditions are met:
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
* Neither the name of wardrobe nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.

View file

@ -1,8 +1,4 @@
recursive-include django_downloadview *
global-exclude *.pyc
include AUTHORS
include CHANGELOG
include CONTRIBUTING.rst
include INSTALL
include LICENSE
include README.rst
global-exclude *.pyc .*.swp
include *.txt
include AUTHORS CHANGELOG INSTALL LICENSE README VERSION

128
Makefile
View file

@ -1,101 +1,77 @@
# Reference card for usual actions in development environment.
#
# For standard installation of django-downloadview as a library, see INSTALL.
#
# For details about django-downloadview's development environment, see
# CONTRIBUTING.rst.
#
PIP = pip
TOX = tox
BLACK = black
ISORT = isort
#: help - Display callable targets.
.PHONY: help
help:
@echo "Reference card for usual actions in development environment."
@echo "Here are available targets:"
@egrep -o "^#: (.+)" [Mm]akefile | sed 's/#: /* /'
# Makefile for development.
# See INSTALL and docs/dev.txt for details.
SHELL = /bin/bash
PROJECT = 'django-downloadview'
ROOT_DIR = $(shell pwd)
DATA_DIR = $(ROOT_DIR)/var
WGET = wget
PYTHON = python
BUILDOUT_BOOTSTRAP_URL = https://raw.github.com/buildout/buildout/1.6.3/bootstrap/bootstrap.py
BUILDOUT_BOOTSTRAP = $(ROOT_DIR)/lib/buildout/bootstrap.py
BUILDOUT = $(ROOT_DIR)/bin/buildout
BUILDOUT_ARGS = -N
#: develop - Install minimal development utilities.
.PHONY: develop
develop:
$(PIP) install -e .
buildout:
# Download zc.buildout bootstrap.
if [ ! -f $(BUILDOUT_BOOTSTRAP) ]; then \
mkdir -p $(ROOT_DIR)/lib/buildout; \
$(WGET) $(BUILDOUT_BOOTSTRAP_URL) -O $(BUILDOUT_BOOTSTRAP); \
fi
# Bootstrap buildout.
if [ ! -f $(BUILDOUT) ]; then \
$(PYTHON) $(BUILDOUT_BOOTSTRAP) --distribute; \
fi
# Run zc.buildout.
$(BUILDOUT) $(BUILDOUT_ARGS)
develop: buildout
update: develop
#: clean - Basic cleanup, mostly temporary files.
.PHONY: clean
clean:
find . -name "*.pyc" -delete
find . -name '*.pyo' -delete
find . -name "__pycache__" -delete
find $(ROOT_DIR)/ -name "*.pyc" -delete
find $(ROOT_DIR)/ -name ".noseids" -delete
#: distclean - Remove local builds, such as *.egg-info.
.PHONY: distclean
distclean: clean
rm -rf *.egg
rm -rf *.egg-info
rm -rf demo/*.egg-info
rm -rf $(ROOT_DIR)/*.egg-info
rm -rf $(ROOT_DIR)/demo/*.egg-info
#: maintainer-clean - Remove almost everything that can be re-generated.
.PHONY: maintainer-clean
maintainer-clean: distclean
rm -rf build/
rm -rf dist/
rm -rf .tox/
rm -rf $(ROOT_DIR)/bin/
rm -rf $(ROOT_DIR)/lib/
#: test - Run test suites.
.PHONY: test
test:
mkdir -p var
$(PIP) install -e .[test]
$(TOX)
bin/demo test demo
#: documentation - Build documentation (Sphinx, README, ...)
.PHONY: documentation
documentation: sphinx readme
apidoc:
cp docs/api/index.txt docs/api-backup.txt
rm -rf docs/api/*
mv docs/api-backup.txt docs/api/index.txt
bin/sphinx-apidoc --suffix txt --output-dir $(ROOT_DIR)/docs/api django_downloadview
#: sphinx - Build Sphinx documentation (docs).
.PHONY: sphinx
sphinx:
$(TOX) -e sphinx
make --directory=docs clean html doctest
#: readme - Build standalone documentation files (README, CONTRIBUTING...).
.PHONY: readme
readme:
$(TOX) -e readme
documentation: apidoc sphinx
#: demo - Setup demo project.
.PHONY: demo
demo:
pip install -e .
pip install -e demo
demo migrate --noinput
# Install fixtures.
mkdir -p var/media/object var/media/object-other/ var/media/nginx
cp -r demo/demoproject/fixtures/* var/media/object/
cp -r demo/demoproject/fixtures/* var/media/object-other/
cp -r demo/demoproject/fixtures/* var/media/nginx/
demo loaddata demo.json
demo: develop
mkdir -p var/media/document
bin/demo syncdb --noinput
cp $(ROOT_DIR)/demo/demoproject/download/fixtures/hello-world.txt var/media/document/
bin/demo loaddata $(ROOT_DIR)/demo/demoproject/download/fixtures/demo.json
bin/demo runserver
#: runserver - Run demo server.
.PHONY: runserver
runserver: demo
demo runserver
.PHONY: black
black:
$(BLACK) demo tests django_downloadview
.PHONY: isort
isort:
$(ISORT) --recursive django_downloadview tests demo
release:
bin/fullrelease

31
README Normal file
View file

@ -0,0 +1,31 @@
###################
Django-DownloadView
###################
Django-DownloadView provides (class-based) generic download views for Django.
Example, in some urls.py:
.. 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'),
)
**********
Ressources
**********
* online 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

View file

@ -1,73 +0,0 @@
###################
django-downloadview
###################
.. image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/
:alt: Jazzband
.. image:: https://img.shields.io/pypi/v/django-downloadview.svg
:target: https://pypi.python.org/pypi/django-downloadview
.. image:: https://img.shields.io/pypi/pyversions/django-downloadview.svg
:target: https://pypi.python.org/pypi/django-downloadview
.. image:: https://img.shields.io/pypi/djversions/django-downloadview.svg
:target: https://pypi.python.org/pypi/django-downloadview
.. image:: https://img.shields.io/pypi/dm/django-downloadview.svg
:target: https://pypi.python.org/pypi/django-downloadview
.. image:: https://github.com/jazzband/django-downloadview/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-downloadview/actions
:alt: GitHub Actions
.. image:: https://codecov.io/gh/jazzband/django-downloadview/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jazzband/django-downloadview
:alt: Coverage
``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:
.. 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'),
)
*********
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
.. _`Django`: https://djangoproject.com

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.0

63
buildout.cfg Normal file
View file

@ -0,0 +1,63 @@
# Buildout configuration file to deploy a development environment.
[buildout]
extensions =
buildout-versions
versions = versions
# Configure directories: put buildout generated files in lib/buildout instead
# of in current directory.
bin-directory = bin
develop-eggs-directory = lib/buildout/develop-eggs
downloads-directory = lib/buildout/downloads
eggs-directory = lib/buildout/eggs
installed = lib/buildout/.installed.cfg
parts-directory = lib/buildout/parts
# Development.
develop =
${buildout:directory}/
${buildout:directory}/demo/
parts =
django-downloadview
directories
releaser
[django-downloadview]
recipe = z3c.recipe.scripts
eggs =
django-downloadview-demo
bpython
nose
rednose
coverage
sphinx
initialization =
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'demoproject.settings'
[directories]
recipe = z3c.recipe.mkdir
paths =
var/docs
docs/_static
var/test
[releaser]
recipe = z3c.recipe.scripts
eggs = zest.releaser
[versions]
Django = 1.4.2
Jinja2 = 2.6
Sphinx = 1.1.3
bpython = 0.11
buildout-versions = 1.7
coverage = 3.5.2
nose = 1.1.2
python-termstyle = 0.1.9
rednose = 0.3
z3c.recipe.mkdir = 0.5
z3c.recipe.scripts = 1.0.1
zc.recipe.egg = 1.3.2
zest.releaser = 3.37
docutils = 0.9.1
Pygments = 1.5
django-nose = 1.1

View file

@ -2,20 +2,8 @@
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!
The :file:`demo/` folder holds a demo project to illustrate (and test)
django-downloadview usage.
***********************
@ -31,8 +19,8 @@ Deploy the demo
System requirements:
* `Python`_ version 3.7+, available as ``python`` command.
* `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
@ -44,12 +32,12 @@ Execute:
.. code-block:: sh
git clone git@github.com:jazzband/django-downloadview.git
git clone git@github.com:benoitbryon/django-downloadview.git
cd django-downloadview/
make runserver
make demo
It installs and runs the demo server on localhost, port 8000. So have a look
at ``http://localhost:8000/``.
at http://localhost:8000/
.. note::
@ -66,7 +54,7 @@ References
.. target-notes::
.. _`demo folder in project's repository`:
https://github.com/jazzband/django-downloadview/tree/master/demo/demoproject/
https://github.com/benoitbryon/django-downloadview/tree/master/demo/demoproject/
.. _`Python`: http://python.org
.. _`Virtualenv`: http://virtualenv.org

View file

@ -1 +0,0 @@
"""Apache optimizations."""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,61 +0,0 @@
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",
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'apache:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("apache:modified_headers")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/apache-modified-headers/hello-world.txt",
)
self.assertEqual(response["X-Test"], "header")

View file

@ -1,24 +0,0 @@
"""URL mapping."""
from django.urls import path
from demoproject.apache import views
app_name = "apache"
urlpatterns = [
path(
"optimized-by-middleware/",
views.optimized_by_middleware,
name="optimized_by_middleware",
),
path(
"optimized-by-decorator/",
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -1,38 +0,0 @@
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
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"
)
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/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_sendfile(
_modified_headers,
source_url=storage.base_url,
destination_dir="/apache-modified-headers/",
)

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,10 @@
[
{
"pk": 1,
"model": "download.document",
"fields": {
"slug": "hello-world",
"file": "document/hello-world.txt"
}
}
]

View file

@ -0,0 +1,8 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Document(models.Model):
"""A sample model with a FileField."""
slug = models.SlugField(verbose_name=_('slug'))
file = models.FileField(verbose_name=_('file'), upload_to='document')

View file

@ -0,0 +1,123 @@
"""Test suite for django-downloadview."""
from os import listdir
from os.path import abspath, dirname, join
import shutil
import tempfile
from django.conf import settings
from django.core.files import File
from django.core.urlresolvers import reverse_lazy as reverse
from django.test import TestCase
from django.test.utils import override_settings
from django_downloadview.nginx import XAccelRedirectResponse
from demoproject.download.models import Document
app_dir = dirname(abspath(__file__))
fixtures_dir = join(app_dir, 'fixtures')
class temporary_media_root(override_settings):
"""Context manager or decorator to override settings.MEDIA_ROOT.
>>> from django.conf import settings
>>> global_media_root = settings.MEDIA_ROOT
>>> with temporary_media_root():
... global_media_root == settings.MEDIA_ROOT
False
>>> global_media_root == settings.MEDIA_ROOT
True
>>> @temporary_media_root
... def use_temporary_media_root():
... return settings.MEDIA_ROOT
>>> tmp_media_root = use_temporary_media_root()
>>> global_media_root == tmp_media_root
False
>>> global_media_root == settings.MEDIA_ROOT
True
"""
def enable(self):
"""Create a temporary directory and use it to override
settings.MEDIA_ROOT."""
tmp_dir = tempfile.mkdtemp()
self.options['MEDIA_ROOT'] = tmp_dir
super(temporary_media_root, self).enable()
def disable(self):
"""Remove directory settings.MEDIA_ROOT then restore original
setting."""
shutil.rmtree(settings.MEDIA_ROOT)
super(temporary_media_root, self).disable()
class DownloadTestCase(TestCase):
"""Base class for download tests."""
def setUp(self):
"""Common setup."""
super(DownloadTestCase, self).setUp()
self.download_hello_world_url = reverse('download_hello_world')
self.download_document_url = reverse('download_document',
kwargs={'slug': 'hello-world'})
self.files = {}
for f in listdir(fixtures_dir):
self.files[f] = abspath(join(fixtures_dir, f))
class DownloadViewTestCase(DownloadTestCase):
"""Test generic DownloadView."""
def test_download_hello_world(self):
"""Download_hello_world view returns hello-world.txt as attachement."""
response = self.client.get(self.download_hello_world_url)
self.assertEquals(response.status_code, 200)
self.assertEquals(response['Content-Type'],
'text/plain; charset=utf-8')
self.assertFalse('ContentEncoding' in response)
self.assertEquals(response['Content-Disposition'],
'attachment; filename=hello-world.txt')
self.assertEqual(open(self.files['hello-world.txt']).read(),
response.content)
class ObjectDownloadViewTestCase(DownloadTestCase):
"""Test generic ObjectDownloadView."""
@temporary_media_root()
def test_download_hello_world(self):
"""Download_hello_world view returns hello-world.txt as attachement."""
document = Document.objects.create(
slug='hello-world',
file=File(open(self.files['hello-world.txt'])),
)
response = self.client.get(self.download_document_url)
self.assertEquals(response.status_code, 200)
self.assertEquals(response['Content-Type'],
'text/plain; charset=utf-8')
self.assertFalse('ContentEncoding' in response)
self.assertEquals(response['Content-Disposition'],
'attachment; filename=hello-world.txt')
self.assertEqual(open(self.files['hello-world.txt']).read(),
response.content)
class XAccelRedirectDecoratorTestCase(DownloadTestCase):
@temporary_media_root()
def test_response(self):
document = Document.objects.create(
slug='hello-world',
file=File(open(self.files['hello-world.txt'])),
)
download_url = reverse('download_document_nginx',
kwargs={'slug': 'hello-world'})
response = self.client.get(download_url)
self.assertEquals(response.status_code, 200)
self.assertTrue(isinstance(response, XAccelRedirectResponse))
self.assertEquals(response['Content-Type'],
'text/plain; charset=utf-8')
self.assertFalse('ContentEncoding' in response)
self.assertEquals(response['Content-Disposition'],
'attachment; filename=hello-world.txt')
self.assertEquals(response['X-Accel-Redirect'],
'/download-optimized/document/hello-world.txt')

View file

@ -0,0 +1,12 @@
"""URLconf for tests."""
from django.conf.urls import patterns, include, url
urlpatterns = patterns('demoproject.download.views',
url(r'^hello-world\.txt$', 'download_hello_world',
name='download_hello_world'),
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$', 'download_document',
name='download_document'),
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
'download_document_nginx', name='download_document_nginx'),
)

View file

@ -0,0 +1,21 @@
from os.path import abspath, dirname, join
from django_downloadview import DownloadView, ObjectDownloadView
from django_downloadview.nginx import x_accel_redirect
from demoproject.download.models import Document
app_dir = dirname(abspath(__file__))
fixtures_dir = join(app_dir, 'fixtures')
hello_world_file = join(fixtures_dir, 'hello-world.txt')
download_hello_world = DownloadView.as_view(filename=hello_world_file,
storage=None)
download_document = ObjectDownloadView.as_view(model=Document)
download_document_nginx = x_accel_redirect(download_document,
media_root='/var/www/files',
media_url='/download-optimized')

View file

@ -1,10 +0,0 @@
[
{
"pk": 1,
"model": "object.document",
"fields": {
"slug": "hello-world",
"file": "object/hello-world.txt"
}
}
]

View file

@ -1,7 +0,0 @@
# -*- 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.
"""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,26 +0,0 @@
import django.test
from django.urls import reverse
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",
)
class AvatarTestCase(django.test.TestCase):
def test_download_response(self):
"""HTTPDownloadView proxies Content-Type header."""
url = reverse("http:avatar_url")
response = self.client.get(url)
assert_download_response(self, response, mime_type="image/png")

View file

@ -1,9 +0,0 @@
from django.urls import path
from demoproject.http import views
app_name = "http"
urlpatterns = [
path("simple_url/", views.simple_url, name="simple_url"),
path("avatar_url/", views.avatar_url, name="avatar_url"),
]

View file

@ -1,21 +0,0 @@
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()

View file

@ -1 +0,0 @@
"""Lighttpd optimizations."""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,61 +0,0 @@
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",
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("lighttpd:modified_headers")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-modified-headers/hello-world.txt",
)
self.assertEqual(response["X-Test"], "header")

View file

@ -1,24 +0,0 @@
"""URL mapping."""
from django.urls import path
from demoproject.lighttpd import views
app_name = "lighttpd"
urlpatterns = [
path(
"optimized-by-middleware/",
views.optimized_by_middleware,
name="optimized_by_middleware",
),
path(
"optimized-by-decorator/",
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -1,38 +0,0 @@
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
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"
)
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/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_sendfile(
_modified_headers,
source_url=storage.base_url,
destination_dir="/lighttpd-modified-headers/",
)

View file

@ -4,9 +4,8 @@ import sys
from django.core.management import execute_from_command_line
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{__package__}.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__)
execute_from_command_line(sys.argv)

View file

@ -1 +0,0 @@
"""Nginx optimizations."""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,73 +0,0 @@
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,
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'nginx:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("nginx:modified_headers")
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-modified-headers/hello-world.txt",
expires=None,
with_buffering=None,
limit_rate=None,
)
self.assertEqual(response["X-Test"], "header")

View file

@ -1,24 +0,0 @@
"""URL mapping."""
from django.urls import path
from demoproject.nginx import views
app_name = "nginx"
urlpatterns = [
path(
"optimized-by-middleware/",
views.optimized_by_middleware,
name="optimized_by_middleware",
),
path(
"optimized-by-decorator/",
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -1,38 +0,0 @@
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
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"
)
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/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_accel_redirect(
_modified_headers,
source_url=storage.base_url,
destination_url="/nginx-modified-headers/",
)

View file

@ -1,7 +0,0 @@
# -*- 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.
"""

View file

@ -1,8 +0,0 @@
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)

View file

@ -1,87 +0,0 @@
from django.core.files.base import ContentFile
import django.test
from django.urls import reverse
from django_downloadview import assert_download_response, temporary_media_root
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",
)
class InlineFileTestCase(django.test.TestCase):
@temporary_media_root()
def test_download_response(self):
"'inline_file_view' streams Document.file inline."
setup_document()
url = reverse("object:inline_file", kwargs={"slug": slug})
response = self.client.get(url)
assert_download_response(
self,
response,
content=file_content,
mime_type="text/plain",
attachment=False,
)

View file

@ -1,27 +0,0 @@
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",
),
re_path(
r"^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
views.another_file_view,
name="another_file",
),
re_path(
r"^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$",
views.deserialized_basename_view,
name="deserialized_basename",
),
re_path(
r"^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
views.inline_file_view,
name="inline_file",
),
]

View file

@ -1,20 +0,0 @@
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"
)
#: Serve ``file`` attribute of ``Document`` model, inline (not as attachment).
inline_file_view = ObjectDownloadView.as_view(model=Document, attachment=False)

View file

@ -1,7 +0,0 @@
# -*- 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.
"""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,32 +0,0 @@
import django.test
from django.urls import reverse
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",
)

View file

@ -1,13 +0,0 @@
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",
),
]

View file

@ -1,39 +0,0 @@
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().get_path()
# Make it absolute.
absolute_path = os.path.join(fixtures_dir, relative_path)
return absolute_path
dynamic_path = DynamicPathDownloadView.as_view()

265
demo/demoproject/settings.py Normal file → Executable file
View file

@ -1,134 +1,175 @@
"""Django settings for django-downloadview demo project."""
# Django settings for Django-DownloadView demo project.
from os.path import abspath, dirname, join
import os
demoproject_dir = dirname(abspath(__file__))
demo_dir = dirname(demoproject_dir)
root_dir = dirname(demo_dir)
data_dir = join(root_dir, 'var')
DEBUG = True
TEMPLATE_DEBUG = DEBUG
# Configure some relative directories.
demoproject_dir = os.path.dirname(os.path.abspath(__file__))
demo_dir = os.path.dirname(demoproject_dir)
root_dir = os.path.dirname(demo_dir)
data_dir = os.path.join(root_dir, "var")
cfg_dir = os.path.join(root_dir, "etc")
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
MANAGERS = ADMINS
# Mandatory settings.
ROOT_URLCONF = "demoproject.urls"
WSGI_APPLICATION = "demoproject.wsgi.application"
# Database.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(data_dir, "db.sqlite"),
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': join(data_dir, 'db.sqlite'),
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'America/Chicago'
# Required.
SECRET_KEY = "This is a secret made public on project's repository."
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
# Media and static files.
MEDIA_ROOT = os.path.join(data_dir, "media")
MEDIA_URL = "/media/"
STATIC_ROOT = os.path.join(data_dir, "static")
STATIC_URL = "/static/"
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# Applications.
INSTALLED_APPS = (
# The actual django-downloadview demo.
"demoproject",
"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.
"demoproject.apache", # Sample optimizations for Apache X-Sendfile.
"demoproject.lighttpd", # Sample optimizations for Lighttpd X-Sendfile.
# Standard Django applications.
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = join(data_dir, 'media')
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = join(data_dir, 'static')
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# BEGIN middlewares
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",
]
# END middlewares
# Make this unique, and don't share it with anybody.
SECRET_KEY = '123456789'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
# Specific configuration for django_downloadview.SmartDownloadMiddleware.
# BEGIN backend
DOWNLOADVIEW_BACKEND = "django_downloadview.nginx.XAccelRedirectMiddleware"
# END backend
"""Could also be:
DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware'
DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware'
"""
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
# BEGIN rules
DOWNLOADVIEW_RULES = [
{
"source_url": "/media/nginx/",
"destination_url": "/nginx-optimized-by-middleware/",
ROOT_URLCONF = 'demoproject.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'demoproject.wsgi.application'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'demoproject',
'demoproject.download',
'django_nose',
)
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
]
# END rules
DOWNLOADVIEW_RULES += [
{
"source_url": "/media/apache/",
"destination_dir": "/apache-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",
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
{
"source_url": "/media/lighttpd/",
"destination_dir": "/lighttpd-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.
DEBUG = True
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(os.path.dirname(__file__), "templates")],
"OPTIONS": {
"debug": DEBUG,
"context_processors": [
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
# list if you haven't customized them:
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
},
]
}
}
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',
]

View file

@ -1,7 +0,0 @@
# -*- 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.
"""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,3 +0,0 @@
from django.core.files.storage import FileSystemStorage
storage = FileSystemStorage()

View file

@ -1,115 +0,0 @@
import datetime
import unittest
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, headers={"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(
url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"}
)
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")

View file

@ -1,17 +0,0 @@
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",
),
re_path(
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
views.dynamic_path,
name="dynamic_path",
),
]

View file

@ -1,20 +0,0 @@
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().get_path().upper()
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)

View file

@ -1,26 +1,15 @@
{% load url from future %}
<html>
<head>
<title>django-downloadview demo</title>
</head>
<body>
<h1>Welcome to django-downloadview demo!</h1>
<p>Here are some demo links. Browse the code to see how they are implemented</p>
<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 'object:default_file' 'hello-world' %}">object:default_file</a>
</li>
</ul>
<h2>Optimized downloads</h2>
<p>In the following views, Django delegates actual streaming to another
server, for improved performances.</p>
<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>
<p>Here are some demo links. Browse the code to see how they are implemented</p>
<ul>
<li><a href="{% url 'download_hello_world' %}">DownloadView</a></li>
<li><a href="{% url 'download_document' 'hello-world' %}">ObjectDownloadView</a></li>
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">ObjectDownloadView decorated with nginx X-Accel-Redirect</a> (better if served behind nginx)</li>
</ul>
</body>
</html>

View file

@ -1,14 +0,0 @@
"""Test suite for demoproject.download."""
from django.test import TestCase
from django.urls import reverse
class HomeViewTestCase(TestCase):
"""Test homepage."""
def test_get(self):
"""Homepage returns HTTP 200."""
home_url = reverse("home")
response = self.client.get(home_url)
self.assertEqual(response.status_code, 200)

47
demo/demoproject/urls.py Normal file → Executable file
View file

@ -1,44 +1,11 @@
from django.urls import include, path
from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView
home = TemplateView.as_view(template_name="home.html")
home = TemplateView.as_view(template_name='home.html')
urlpatterns = [
# ObjectDownloadView.
path(
"object/",
include("demoproject.object.urls", namespace="object"),
),
# StorageDownloadView.
path(
"storage/",
include("demoproject.storage.urls", namespace="storage"),
),
# PathDownloadView.
path("path/", include("demoproject.path.urls", namespace="path")),
# HTTPDownloadView.
path("http/", include("demoproject.http.urls", namespace="http")),
# VirtualDownloadView.
path(
"virtual/",
include("demoproject.virtual.urls", namespace="virtual"),
),
# Nginx optimizations.
path(
"nginx/",
include("demoproject.nginx.urls", namespace="nginx"),
),
# Apache optimizations.
path(
"apache/",
include("demoproject.apache.urls", namespace="apache"),
),
# Lighttpd optimizations.
path(
"lighttpd/",
include("demoproject.lighttpd.urls", namespace="lighttpd"),
),
# An informative homepage.
path("", home, name="home"),
]
urlpatterns = patterns('',
url(r'^download/', include('demoproject.download.urls')),
url(r'', home, name='home')
)

View file

@ -1,7 +0,0 @@
# -*- 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.
"""

View file

@ -1 +0,0 @@
"""Required to make a Django application."""

View file

@ -1,46 +0,0 @@
import django.test
from django.urls import reverse
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",
)

View file

@ -1,10 +0,0 @@
from django.urls import path
from demoproject.virtual import views
app_name = "virtual"
urlpatterns = [
path("text/", views.TextDownloadView.as_view(), name="text"),
path("stringio/", views.StringIODownloadView.as_view(), name="stringio"),
path("gerenated/", views.GeneratedDownloadView.as_view(), name="generated"),
]

View file

@ -1,31 +0,0 @@
from io import StringIO
from django.core.files.base import ContentFile
from django_downloadview import TextIteratorIO, VirtualDownloadView, VirtualFile
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")
class StringIODownloadView(VirtualDownloadView):
def get_file(self):
"""Return wrapper on ``six.StringIO`` object."""
file_obj = StringIO("Hello world!\n")
return VirtualFile(file_obj, name="hello-world.txt")
def generate_hello():
yield "Hello "
yield "world!"
yield "\n"
class GeneratedDownloadView(VirtualDownloadView):
def get_file(self):
"""Return wrapper on ``StringIteratorIO`` object."""
file_obj = TextIteratorIO(generate_hello())
return VirtualFile(file_obj, name="hello-world.txt")

View file

@ -12,16 +12,14 @@ middleware here, or combine a Django application with an application of another
framework.
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__)
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.

View file

@ -1,26 +1,46 @@
# coding=utf-8
"""Python packaging."""
import os
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__))
setup(
name="django-downloadview-demo",
version="1.0",
description="Serve files with Django and reverse-proxies.",
long_description=open(os.path.join(here, "README.rst")).read(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Framework :: Django",
],
author="Benoît Bryon",
author_email="benoit@marmelune.net",
url="https://django-downloadview.readthedocs.io/",
license="BSD",
packages=["demoproject"],
include_package_data=True,
zip_safe=False,
install_requires=["django-downloadview", "pytest-django"],
entry_points={"console_scripts": ["demo = demoproject.manage:main"]},
)
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()
NAME = 'django-downloadview-demo'
README = read_relative_file('README')
VERSION = '0.1'
PACKAGES = ['demoproject']
REQUIRES = ['django-downloadview',
'django-nose']
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',
]
},
)

View file

@ -1,8 +1,9 @@
"""Serve files with Django and reverse proxies."""
"""django-downloadview provides generic download views for Django."""
from django_downloadview.views import DownloadView, ObjectDownloadView
from django_downloadview.api import * # NoQA
import importlib.metadata
pkg_resources = __import__('pkg_resources')
distribution = pkg_resources.get_distribution('django-downloadview')
#: Module version, as defined in PEP-0396.
__version__ = importlib.metadata.version(__package__.replace("-", "_"))
__version__ = distribution.version

View file

@ -1,13 +0,0 @@
"""Optimizations for Apache.
See also `documentation of mod_xsendfile for Apache
<https://tn123.org/mod_xsendfile/>`_ and :doc:`narrative documentation about
Apache optimizations </optimizations/apache>`.
"""
# API shortcuts.
from django_downloadview.apache.decorators import x_sendfile # NoQA
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
from django_downloadview.apache.response import XSendfileResponse # NoQA
from django_downloadview.apache.tests import assert_x_sendfile # NoQA

View file

@ -1,16 +0,0 @@
"""Decorators to apply Apache X-Sendfile on a specific view."""
from django_downloadview.apache.middlewares import XSendfileMiddleware
from django_downloadview.decorators import DownloadDecorator
def x_sendfile(view_func, *args, **kwargs):
"""Apply
:class:`~django_downloadview.apache.middlewares.XSendfileMiddleware` to
``view_func``.
Proxies (``*args``, ``**kwargs``) to middleware constructor.
"""
decorator = DownloadDecorator(XSendfileMiddleware)
return decorator(view_func, *args, **kwargs)

View file

@ -1,36 +0,0 @@
from django_downloadview.apache.response import XSendfileResponse
from django_downloadview.middlewares import (
NoRedirectionMatch,
ProxiedDownloadMiddleware,
)
class XSendfileMiddleware(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, get_response=None, source_dir=None, source_url=None, destination_dir=None
):
"""Constructor."""
super().__init__(get_response, source_dir, source_url, destination_dir)
def process_download_response(self, request, response):
"""Replace DownloadResponse instances by XSendfileResponse ones."""
try:
redirect_url = self.get_redirect_url(response)
except NoRedirectionMatch:
return response
return XSendfileResponse(
file_path=redirect_url,
content_type=response["Content-Type"],
basename=response.basename,
attachment=response.attachment,
headers=response.headers,
)

View file

@ -1,22 +0,0 @@
"""Apache's specific responses."""
import os.path
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
class XSendfileResponse(ProxiedDownloadResponse):
"Delegates serving file to Apache via X-Sendfile header."
def __init__(
self, file_path, content_type, basename=None, attachment=True, headers=None
):
"""Return a HttpResponse with headers for Apache X-Sendfile."""
# content-type must be provided only as keyword argument to response
if headers and content_type:
headers.pop("Content-Type", None)
super().__init__(content_type=content_type, headers=headers)
if attachment:
self.basename = basename or os.path.basename(file_path)
self["Content-Disposition"] = content_disposition(self.basename)
self["X-Sendfile"] = file_path

View file

@ -1,62 +0,0 @@
from django_downloadview.apache.response import XSendfileResponse
class XSendfileValidator:
"""Utility class to validate XSendfileResponse instances.
See also :py:func:`assert_x_sendfile` shortcut function.
"""
def __call__(self, test_case, response, **assertions):
"""Assert that ``response`` is a valid X-Sendfile 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.
* ``file_path``: the value of "X-Sendfile" header.
"""
self.assert_x_sendfile_response(test_case, response)
for key, value in assertions.items():
assert_func = getattr(self, "assert_%s" % key)
assert_func(test_case, response, value)
def assert_x_sendfile_response(self, test_case, response):
test_case.assertTrue(isinstance(response, XSendfileResponse))
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_file_path(self, test_case, response, value):
test_case.assertEqual(response["X-Sendfile"], 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_sendfile(test_case, response, **assertions):
"""Make ``test_case`` assert that ``response`` is a XSendfileResponse.
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.
* ``file_path``: the value of "X-Sendfile" header.
"""
validator = XSendfileValidator()
return validator(test_case, response, **assertions)

View file

@ -1,29 +0,0 @@
# flake8: noqa
"""Declaration of API shortcuts."""
from django_downloadview.files import HTTPFile, StorageFile, VirtualFile
from django_downloadview.io import BytesIteratorIO, TextIteratorIO
from django_downloadview.middlewares import (
BaseDownloadMiddleware,
DownloadDispatcherMiddleware,
SmartDownloadMiddleware,
)
from django_downloadview.response import DownloadResponse, ProxiedDownloadResponse
from django_downloadview.shortcuts import sendfile
from django_downloadview.test import (
assert_download_response,
setup_view,
temporary_media_root,
)
from django_downloadview.views import (
BaseDownloadView,
DownloadMixin,
HTTPDownloadView,
ObjectDownloadView,
PathDownloadView,
StorageDownloadView,
VirtualDownloadView,
)
# Backward compatibility.
StringIteratorIO = TextIteratorIO

View file

@ -1,76 +1,32 @@
"""View decorators.
See also decorators provided by server-specific modules, such as
:func:`django_downloadview.nginx.x_accel_redirect`.
:py:func:`django_downloadview.nginx.x_accel_redirect`.
"""
from functools import wraps
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
class DownloadDecorator(object):
"""View decorator factory to apply middleware to ``view_func``'s response.
"""View decorator factory to apply middleware to ``view_func`` 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.BaseDownloadMiddleware` subclass.
:py:class:`django_downloadview.middlewares.XAccelMiddleware` subclass.
Response is built from view, then the middleware's ``process_response``
method is applied on response.
"""
def __init__(self, middleware_factory):
"""Create a download view decorator."""
self.middleware_factory = middleware_factory
def __call__(self, view_func, *middleware_args, **middleware_kwargs):
"""Return ``view_func`` decorated with response middleware."""
def decorated(request, *view_args, **view_kwargs):
"""Return view's response modified by middleware."""
response = view_func(request, *view_args, **view_kwargs)
middleware = self.middleware_factory(*middleware_args, **middleware_kwargs)
middleware = self.middleware_factory(*middleware_args,
**middleware_kwargs)
return middleware.process_response(request, response)
return decorated
def _signature_is_valid(request):
"""
Validator that raises a PermissionDenied error on invalid and
mismatching signatures.
"""
signer = TimestampSigner()
signature = request.GET.get("X-Signature")
expiration = getattr(settings, "DOWNLOADVIEW_URL_EXPIRATION", None)
try:
signature_path = signer.unsign(signature, max_age=expiration)
except SignatureExpired as e:
raise PermissionDenied("Signature expired") from e
except BadSignature as e:
raise PermissionDenied("Signature invalid") from e
except Exception as e:
raise PermissionDenied("Signature error") from e
if request.path != signature_path:
raise PermissionDenied("Signature mismatch")
def signature_required(function):
"""
Decorator that checks for X-Signature query parameter to authorize access to views.
"""
@wraps(function)
def decorator(request, *args, **kwargs):
_signature_is_valid(request)
return function(request, *args, **kwargs)
return decorator

View file

@ -1,10 +0,0 @@
"""Custom exceptions."""
class FileNotFound(IOError):
"""Requested file does not exist.
This exception is to be raised when operations (such as read) fail because
file does not exist (whatever the storage or location).
"""

View file

@ -1,272 +0,0 @@
"""File wrappers for use as exchange data between views and responses."""
from io import BytesIO
from urllib.parse import urlparse
from django.core.files.base import File
from django.utils.encoding import force_bytes
from django_downloadview.io import BytesIteratorIO
import requests
class StorageFile(File):
"""A file in a Django storage.
This class looks like :py:class:`django.db.models.fields.files.FieldFile`,
but unrelated to model instance.
"""
def __init__(self, storage, name, file=None):
"""Constructor.
storage:
Some :py:class:`django.core.files.storage.Storage` instance.
name:
File identifier in storage, usually a filename as a string.
"""
self.storage = storage
self.name = name
self.file = file
def _get_file(self):
"""Getter for :py:attr:``file`` property."""
if not hasattr(self, "_file") or self._file is None:
self._file = self.storage.open(self.name, "rb")
return self._file
def _set_file(self, file):
"""Setter for :py:attr:``file`` property."""
self._file = file
def _del_file(self):
"""Deleter for :py:attr:``file`` property."""
del self._file
#: Required by django.core.files.utils.FileProxy.
file = property(_get_file, _set_file, _del_file)
def open(self, mode="rb"):
"""Retrieves the specified file from storage and return open() result.
Proxy to self.storage.open(self.name, mode).
"""
return self.storage.open(self.name, mode)
def save(self, content):
"""Saves new content to the file.
Proxy to self.storage.save(self.name).
The content should be a proper File object, ready to be read from the
beginning.
"""
return self.storage.save(self.name, content)
@property
def path(self):
"""Return a local filesystem path which is suitable for open().
Proxy to self.storage.path(self.name).
May raise NotImplementedError if storage doesn't support file access
with Python's built-in open() function
"""
return self.storage.path(self.name)
def delete(self):
"""Delete the specified file from the storage system.
Proxy to self.storage.delete(self.name).
"""
return self.storage.delete(self.name)
def exists(self):
"""Return True if file already exists in the storage system.
If False, then the name is available for a new file.
"""
return self.storage.exists(self.name)
@property
def size(self):
"""Return the total size, in bytes, of the file.
Proxy to self.storage.size(self.name).
"""
return self.storage.size(self.name)
@property
def url(self):
"""Return an absolute URL where the file's contents can be accessed.
Proxy to self.storage.url(self.name).
"""
return self.storage.url(self.name)
@property
def accessed_time(self):
"""Return the last accessed time (as datetime object) of the file.
Proxy to self.storage.accessed_time(self.name).
"""
try:
return self.storage.get_accessed_time(self.name)
except AttributeError:
return self.storage.accessed_time(self.name)
@property
def created_time(self):
"""Return the creation time (as datetime object) of the file.
Proxy to self.storage.created_time(self.name).
"""
try:
return self.storage.get_created_time(self.name)
except AttributeError:
return self.storage.created_time(self.name)
@property
def modified_time(self):
"""Return the last modification time (as datetime object) of the file.
Proxy to self.storage.modified_time(self.name).
"""
try:
return self.storage.get_modified_time(self.name)
except AttributeError:
return self.storage.modified_time(self.name)
class VirtualFile(File):
"""Wrapper for files that live in memory."""
def __init__(self, file=None, name="", url="", size=None):
"""Constructor.
file:
File object. Typically an io.StringIO.
name:
File basename.
url:
File URL.
"""
super().__init__(file, name)
self.url = url
if size is not None:
self._size = size
def _get_size(self):
try:
return self._size
except AttributeError:
try:
self._size = self.file.size
except AttributeError:
self._size = len(self.file.getvalue())
return self._size
def _set_size(self, value):
return super()._set_size(value)
size = property(_get_size, _set_size)
def __iter__(self):
"""Same as ``File.__iter__()`` but using ``force_bytes()``.
See https://code.djangoproject.com/ticket/21321
"""
# 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 HTTPFile(File):
"""Wrapper for files that live on remote HTTP servers.
Acts as a proxy.
Uses https://pypi.python.org/pypi/requests.
Always sets "stream=True" in requests kwargs.
"""
def __init__(self, request_factory=requests.get, url="", name="", **kwargs):
self.request_factory = request_factory
self.url = url
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
@property
def request(self):
try:
return self._request
except AttributeError:
self._request = self.request_factory(self.url, **self.request_kwargs)
return self._request
@property
def file(self):
try:
return self._file
except AttributeError:
content = self.request.iter_content(decode_unicode=False)
self._file = BytesIteratorIO(content)
return self._file
@property
def size(self):
"""Return the total size, in bytes, of the file.
Reads response's "content-length" header.
"""
return self.request.headers["Content-Length"]
@property
def content_type(self):
"""Return content type of the file (from original response)."""
return self.request.headers["Content-Type"]

View file

@ -1,143 +0,0 @@
"""Low-level IO operations, for use with file wrappers."""
import io
from django.utils.encoding import force_bytes, force_str
class TextIteratorIO(io.TextIOBase):
"""A dynamically generated TextIO-like object.
Original code by Matt Joiner <anacrolix@gmail.com> from:
* http://stackoverflow.com/questions/12593576/
* https://gist.github.com/anacrolix/3788413
"""
def __init__(self, iterator):
#: Iterator/generator for content.
self._iter = iterator
#: Internal buffer.
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
else:
# Make sure we handle text.
self._left = force_str(self._left)
ret = self._left[:n]
self._left = self._left[len(ret) :]
return ret
def read(self, n=None):
"""Return content up to ``n`` length."""
chunks = []
if n is None or n < 0:
while True:
m = self._read1()
if not m:
break
chunks.append(m)
else:
while n > 0:
m = self._read1(n)
if not m:
break
n -= len(m)
chunks.append(m)
return "".join(chunks)
def readline(self):
chunks = []
while True:
i = self._left.find("\n")
if i == -1:
chunks.append(self._left)
try:
self._left = next(self._iter)
except StopIteration:
self._left = ""
break
else:
chunks.append(self._left[: i + 1])
self._left = self._left[i + 1 :]
break
return "".join(chunks)
class BytesIteratorIO(io.BytesIO):
"""A dynamically generated BytesIO-like object.
Original code by Matt Joiner <anacrolix@gmail.com> from:
* http://stackoverflow.com/questions/12593576/
* https://gist.github.com/anacrolix/3788413
"""
def __init__(self, iterator):
#: Iterator/generator for content.
self._iter = iterator
#: Internal buffer.
self._left = b""
def readable(self):
return True
def _read1(self, n=None):
while not self._left:
try:
self._left = next(self._iter)
except StopIteration:
break
else:
# Make sure we handle text.
self._left = force_bytes(self._left)
ret = self._left[:n]
self._left = self._left[len(ret) :]
return ret
def read(self, n=None):
"""Return content up to ``n`` length."""
chunks = []
if n is None or n < 0:
while True:
m = self._read1()
if not m:
break
chunks.append(m)
else:
while n > 0:
m = self._read1(n)
if not m:
break
n -= len(m)
chunks.append(m)
return b"".join(chunks)
def readline(self):
chunks = []
while True:
i = self._left.find(b"\n")
if i == -1:
chunks.append(self._left)
try:
self._left = next(self._iter)
except StopIteration:
self._left = b""
break
else:
chunks.append(self._left[: i + 1])
self._left = self._left[i + 1 :]
break
return b"".join(chunks)

View file

@ -1,14 +0,0 @@
"""Optimizations for Lighttpd.
See also `documentation of X-Sendfile for Lighttpd
<http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file>`_ and
:doc:`narrative documentation about Lighttpd optimizations
</optimizations/lighttpd>`.
"""
# API shortcuts.
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
from django_downloadview.lighttpd.response import XSendfileResponse # NoQA
from django_downloadview.lighttpd.tests import assert_x_sendfile # NoQA

View file

@ -1,16 +0,0 @@
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
from django_downloadview.decorators import DownloadDecorator
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
def x_sendfile(view_func, *args, **kwargs):
"""Apply
:class:`~django_downloadview.lighttpd.middlewares.XSendfileMiddleware` to
``view_func``.
Proxies (``*args``, ``**kwargs``) to middleware constructor.
"""
decorator = DownloadDecorator(XSendfileMiddleware)
return decorator(view_func, *args, **kwargs)

View file

@ -1,36 +0,0 @@
from django_downloadview.lighttpd.response import XSendfileResponse
from django_downloadview.middlewares import (
NoRedirectionMatch,
ProxiedDownloadMiddleware,
)
class XSendfileMiddleware(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, get_response=None, source_dir=None, source_url=None, destination_dir=None
):
"""Constructor."""
super().__init__(get_response, source_dir, source_url, destination_dir)
def process_download_response(self, request, response):
"""Replace DownloadResponse instances by XSendfileResponse ones."""
try:
redirect_url = self.get_redirect_url(response)
except NoRedirectionMatch:
return response
return XSendfileResponse(
file_path=redirect_url,
content_type=response["Content-Type"],
basename=response.basename,
attachment=response.attachment,
headers=response.headers,
)

View file

@ -1,22 +0,0 @@
"""Lighttpd's specific responses."""
import os.path
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
class XSendfileResponse(ProxiedDownloadResponse):
"Delegates serving file to Lighttpd via X-Sendfile header."
def __init__(
self, file_path, content_type, basename=None, attachment=True, headers=None
):
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
# content-type must be porvided only as keyword argument to response
if headers and content_type:
headers.pop("Content-Type", None)
super().__init__(content_type=content_type, headers=headers)
if attachment:
self.basename = basename or os.path.basename(file_path)
self["Content-Disposition"] = content_disposition(self.basename)
self["X-Sendfile"] = file_path

View file

@ -1,29 +0,0 @@
import django_downloadview.apache.tests
from django_downloadview.lighttpd.response import XSendfileResponse
class XSendfileValidator(django_downloadview.apache.tests.XSendfileValidator):
"""Utility class to validate XSendfileResponse instances.
See also :py:func:`assert_x_sendfile` shortcut function.
"""
def assert_x_sendfile_response(self, test_case, response):
test_case.assertTrue(isinstance(response, XSendfileResponse))
def assert_x_sendfile(test_case, response, **assertions):
"""Make ``test_case`` assert that ``response`` is a XSendfileResponse.
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.
* ``file_path``: the value of "X-Sendfile" header.
"""
validator = XSendfileValidator()
return validator(test_case, response, **assertions)

View file

@ -1,62 +1,26 @@
"""Base material for download middlewares.
Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
responses and may replace them with optimized download responses.
"""
import collections.abc
import copy
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django_downloadview.response import DownloadResponse
from django_downloadview.utils import import_member
"""Base material for download middlewares."""
from django_downloadview.response import is_download_response
#: Sentinel value to detect whether configuration is to be loaded from Django
#: settings or not.
AUTO_CONFIGURE = object()
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:
class BaseDownloadMiddleware(object):
"""Base (abstract) Django middleware that handles download responses.
Subclasses **must** implement :py:meth:`process_download_response` method.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return self.process_response(request, response)
def is_download_response(self, response):
"""Return True if ``response`` can be considered as a file download.
By default, this method uses
:py:func:`django_downloadview.middlewares.is_download_response`.
:py:func:`django_downloadview.response.is_download_response`.
Override this method if you want a different behaviour.
"""
return is_download_response(response)
def process_response(self, request, response):
"""Call `process_download_response()` if ``response`` is download."""
"""Call :py:meth:`process_download_response` if ``response`` is
download."""
if self.is_download_response(response):
return self.process_download_response(request, response)
return response
@ -64,169 +28,3 @@ class BaseDownloadMiddleware:
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.
"""
return super().is_download_response(response) and bool(
getattr(response.file, "url", None) or getattr(response.file, "name", None)
)
class DownloadDispatcher:
def __init__(self, middlewares=AUTO_CONFIGURE):
#: List of children middlewares.
self.middlewares = middlewares
if self.middlewares is AUTO_CONFIGURE:
self.auto_configure_middlewares()
def auto_configure_middlewares(self):
"""Populate :attr:`middlewares` from
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
for key, import_string, kwargs in getattr(
settings, "DOWNLOADVIEW_MIDDLEWARES", []
):
factory = import_member(import_string)
middleware = factory(**kwargs)
self.middlewares.append((key, middleware))
def dispatch(self, request, response):
"""Dispatches job to children middlewares."""
for key, middleware in self.middlewares:
response = middleware.process_response(request, response)
return response
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
"Download middleware that dispatches job to several middleware instances."
def __init__(self, get_response, middlewares=AUTO_CONFIGURE):
super().__init__(get_response)
self.dispatcher = DownloadDispatcher(middlewares)
def process_download_response(self, request, response):
return self.dispatcher.dispatch(request, response)
class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
"""Easy to configure download middleware."""
def __init__(
self,
get_response,
backend_factory=AUTO_CONFIGURE,
backend_options=AUTO_CONFIGURE,
):
"""Constructor."""
super().__init__(get_response, middlewares=[])
#: Callable (typically a class) to instantiate backend (typically a
#: :class:`DownloadMiddleware` subclass).
self.backend_factory = backend_factory
if self.backend_factory is AUTO_CONFIGURE:
self.auto_configure_backend_factory()
#: List of positional or keyword arguments to instantiate backend
#: instances.
self.backend_options = backend_options
if self.backend_options is AUTO_CONFIGURE:
self.auto_configure_backend_options()
def auto_configure_backend_factory(self):
"Assign :attr:`backend_factory` from ``settings.DOWNLOADVIEW_BACKEND``"
try:
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
except AttributeError:
raise ImproperlyConfigured(
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_BACKEND"
)
def auto_configure_backend_options(self):
"""Populate :attr:`dispatcher` using :attr:`factory` and
``settings.DOWNLOADVIEW_RULES``."""
try:
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
except AttributeError:
raise ImproperlyConfigured(
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_RULES"
)
for key, options in enumerate(options_list):
args = []
kwargs = {}
if isinstance(options, collections.abc.Mapping): # Using kwargs.
kwargs = options
else:
args = options
if "backend" in kwargs: # Specific backend for this rule.
factory = import_member(kwargs["backend"])
del kwargs["backend"]
else: # Fallback to global backend.
factory = self.backend_factory
middleware_instance = factory(*args, **kwargs)
self.dispatcher.middlewares.append((key, middleware_instance))
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, get_response, source_dir=None, source_url=None, destination_url=None
):
"""Constructor."""
super().__init__(get_response)
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("/")))

View file

@ -0,0 +1,160 @@
"""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
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)
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):
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
basename = basename or redirect_url.split('/')[-1]
self['Content-Disposition'] = 'attachment; filename=%s' % basename
self['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 BaseXAccelRedirectMiddleware(BaseDownloadMiddleware):
"""Looks like a middleware, but it is configurable.
Standard Django middlewares are configured globally via settings. Instances
of this class are to be configured individually. It makes it possible to
use this class as the factory in
:py:class:`django_downloadview.decorators.DownloadDecorator`.
"""
def __init__(self, media_root, media_url, expires=None,
with_buffering=None, limit_rate=None):
"""Constructor."""
self.media_root = media_root
self.media_url = media_url
self.expires = expires
self.with_buffering = with_buffering
self.limit_rate = limit_rate
def get_redirect_url(self, response):
"""Return redirect URL for file wrapped into response."""
absolute_filename = response.filename
relative_filename = absolute_filename[len(self.media_root):]
return '/'.join((self.media_url.rstrip('/'),
relative_filename.strip('/')))
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)
class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware):
"""Apply X-Accel-Redirect globally, via Django settings."""
def __init__(self):
"""Use Django settings as configuration."""
try:
media_root = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
except AttributeError:
raise ImproperlyConfigured(
'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is required by '
'%s middleware' % self.__class__.name)
try:
media_url = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
except AttributeError:
raise ImproperlyConfigured(
'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is required by '
'%s middleware' % self.__class__.name)
super(XAccelRedirectMiddleware, self).__init__(
media_root,
media_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)

View file

@ -1,13 +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>`.
"""
# API shortcuts.
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
from django_downloadview.nginx.response import XAccelRedirectResponse # NoQA
from django_downloadview.nginx.tests import assert_x_accel_redirect # NoQA

View file

@ -1,16 +0,0 @@
"""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)

View file

@ -1,142 +0,0 @@
import warnings
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django_downloadview.middlewares import (
NoRedirectionMatch,
ProxiedDownloadMiddleware,
)
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,
get_response=None,
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().__init__(get_response, 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,
headers=response.headers,
)
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, get_response=None):
"""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().__init__(
get_response=get_response,
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,
)

View file

@ -1,43 +0,0 @@
"""Nginx's specific responses."""
from datetime import timedelta
from django.utils.timezone import now
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
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,
headers=None,
):
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
# content-type must be porvided only as keyword argument to response
if headers and content_type:
headers.pop("Content-Type", None)
super().__init__(content_type=content_type, headers=headers)
if attachment:
self.basename = basename or url_basename(redirect_url, content_type)
self["Content-Disposition"] = content_disposition(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"

View file

@ -1,124 +0,0 @@
"""Django settings around Nginx X-Accel.
.. warning::
These settings are deprecated since version 1.3. You can now provide custom
configuration via `DOWNLOADVIEW_BACKEND` 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.
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
if deprecated_middleware in settings.MIDDLEWARE:
raise ImproperlyConfigured(
"{deprecated_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.SmartDownloadDispatcher". '
)
deprecated_msg = (
"settings.{deprecated} is deprecated. You should combine "
'"django_downloadview.SmartDownloadDispatcher" with '
"with DOWNLOADVIEW_BACKEND and DOWNLOADVIEW_RULES instead."
)
#: 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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.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(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_DESTINATION_URL)

View file

@ -1,120 +0,0 @@
from django_downloadview.nginx.response import XAccelRedirectResponse
class XAccelRedirectValidator:
"""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.items():
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)

Some files were not shown because too many files have changed in this diff Show more