mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
Compare commits
176 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969fb7931a | ||
|
|
fe610b9c9f | ||
|
|
187787d083 | ||
|
|
614bc522d3 | ||
|
|
34a83fd09b | ||
|
|
415b76622e | ||
|
|
982432a05b | ||
|
|
055ef690d2 | ||
|
|
94b4a60917 | ||
|
|
9194e6a11c | ||
|
|
140b631d5b | ||
|
|
e3ebf67457 | ||
|
|
5f2ae9a9b0 | ||
|
|
f1a07c226e | ||
|
|
9b90a3326d | ||
|
|
45fb86b52c | ||
|
|
5dc018f17b | ||
|
|
d515804708 | ||
|
|
4e580980fe | ||
|
|
9b77252326 | ||
|
|
f9cc2c5fab | ||
|
|
ec0b34df5b | ||
|
|
9e22eb8d8f | ||
|
|
c239015d1a | ||
|
|
73f9c013cd | ||
|
|
840827da1d | ||
|
|
31e64b6a76 | ||
|
|
71a1670703 | ||
|
|
dff487aca9 | ||
|
|
7aa9b687aa | ||
|
|
b02e2f13f9 | ||
|
|
a7d182f0b4 | ||
|
|
c8486417bb | ||
|
|
ebcd3a0028 | ||
|
|
6c0e0a8c82 | ||
|
|
d3a8f6b725 | ||
|
|
13a502bc4a | ||
|
|
9f42cde8cb | ||
|
|
5c8bbda4b3 | ||
|
|
eab4fa7abf | ||
|
|
711b2e50b5 | ||
|
|
dd35f867e0 | ||
|
|
402c77b332 | ||
|
|
347f0202ab | ||
|
|
18cb41f760 | ||
|
|
1b294f00fa | ||
|
|
51deef0a7e | ||
|
|
e7e25e68dd | ||
|
|
0568c3c559 | ||
|
|
e2b4470c5b | ||
|
|
71488c49c4 | ||
|
|
6c7b7f8a31 | ||
|
|
d385cbba6f | ||
|
|
60c1839bf5 | ||
|
|
ba6be8c3cd | ||
|
|
16b241d9b5 | ||
|
|
c51720296a | ||
|
|
fba10f7b1b | ||
|
|
41caa79f46 | ||
|
|
ff5073d00b | ||
|
|
338e17195f | ||
|
|
df439fbd4f | ||
|
|
dd2e148b05 | ||
|
|
64e36826ff | ||
|
|
d19e4bee50 | ||
|
|
8c74a77ebe | ||
|
|
a6975d9669 | ||
|
|
f715f72032 | ||
|
|
293403b807 | ||
|
|
b64b1ad21a | ||
|
|
3c2951ceac | ||
|
|
aa003ed6bf | ||
|
|
6dbf06c4ea | ||
|
|
7c2af759c8 | ||
|
|
7e9f81d758 | ||
|
|
e9fbb74b2c | ||
|
|
6381dc94f1 | ||
|
|
198f6a3295 | ||
|
|
a64a0e8c33 | ||
|
|
0ab8aa3e8f | ||
|
|
2524668e86 | ||
|
|
cb3ec3a091 | ||
|
|
95b36fc843 | ||
|
|
0578729a66 | ||
|
|
051f6116f8 | ||
|
|
ca8331e2de | ||
|
|
a8d231cbed | ||
|
|
006d2a288d | ||
|
|
cbd53d813e | ||
|
|
3543a3b8fc | ||
|
|
465123b6e5 | ||
|
|
45e1219275 | ||
|
|
a06c511d8f | ||
|
|
a67f3dc41e | ||
|
|
9c01be6cc8 | ||
|
|
563b2a4f7b | ||
|
|
707c392f6e | ||
|
|
9b5fee2687 | ||
|
|
ed3d470908 | ||
|
|
eb223f169b | ||
|
|
700b1246df | ||
|
|
2586cc5a97 | ||
|
|
c602c32d69 | ||
|
|
8fd75de224 | ||
|
|
87daf8e92e | ||
|
|
4dae54179b | ||
|
|
d1972d6460 | ||
|
|
cee7810cfd | ||
|
|
8df2615a29 | ||
|
|
252e6f127c | ||
|
|
cc4636098b | ||
|
|
f225d87b85 | ||
|
|
deddd2fd2d | ||
|
|
6338f61767 | ||
|
|
9f42e65986 | ||
|
|
384e7c5b13 | ||
|
|
0c59d6d261 | ||
|
|
13d2b3ae58 | ||
|
|
f663fa4b03 | ||
|
|
7ec465d74d | ||
|
|
3bb1a73e6b | ||
|
|
c0acf77f73 | ||
|
|
d74e094fe7 | ||
|
|
3da22537df | ||
|
|
91be5c38a8 | ||
|
|
8e439d3485 | ||
|
|
6cd9ec3dd0 | ||
|
|
bd41e6f7bf | ||
|
|
e2da75ec9d | ||
|
|
cd37fd5084 | ||
|
|
bc145254ea | ||
|
|
79da070b14 | ||
|
|
aacb5c7a16 | ||
|
|
965218aafb | ||
|
|
17afa754e9 | ||
|
|
48d2a622a7 | ||
|
|
ee402dbcb8 | ||
|
|
bfb9c3cedb | ||
|
|
839b7bda38 | ||
|
|
27d72b058f | ||
|
|
f511ab3602 | ||
|
|
bdf0ba2188 | ||
|
|
c59cc37a0f | ||
|
|
56e419bfc7 | ||
|
|
8dad97c92d | ||
|
|
586a50aa5a | ||
|
|
af30524bd2 | ||
|
|
6c7c8d9a60 | ||
|
|
b094ed6cec | ||
|
|
fd3c463a46 | ||
|
|
b893e52eba | ||
|
|
2773a2b158 | ||
|
|
9014f292e7 | ||
|
|
31bb096a77 | ||
|
|
28a077b1c3 | ||
|
|
1ce8aedfd1 | ||
|
|
cc1e77c405 | ||
|
|
2988aa65e7 | ||
|
|
9a82b76faf | ||
|
|
433b2d5b80 | ||
|
|
256974ab80 | ||
|
|
ac1e4dc9ce | ||
|
|
0ad5e1238a | ||
|
|
4aa44b9dbe | ||
|
|
69bb9d864d | ||
|
|
1c829c5413 | ||
|
|
9aaa069e23 | ||
|
|
5306a54222 | ||
|
|
ade4c06670 | ||
|
|
24afd546c6 | ||
|
|
235f95b967 | ||
|
|
414c083aa3 | ||
|
|
a4cc84d758 | ||
|
|
6166783914 | ||
|
|
e0dc0b0c41 | ||
|
|
af67e0036f |
112 changed files with 2077 additions and 1338 deletions
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
61
.github/workflows/test.yml
vendored
Normal file
61
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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 }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
# Data files.
|
||||
/var/
|
||||
coverage.xml
|
||||
.coverage/
|
||||
|
||||
# Python files.
|
||||
*.pyc
|
||||
|
|
@ -14,11 +16,17 @@
|
|||
|
||||
# Tox files.
|
||||
/.tox/
|
||||
.eggs
|
||||
*.egg-info
|
||||
|
||||
# Virtualenv files (created by tox).
|
||||
/build/
|
||||
/dist/
|
||||
|
||||
# Virtual environments (created by user).
|
||||
/venv/
|
||||
|
||||
# Editors' temporary buffers.
|
||||
.*.swp
|
||||
*~
|
||||
.idea
|
||||
|
|
|
|||
16
.isort.cfg
Normal file
16
.isort.cfg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[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
|
||||
59
.pre-commit-config.yaml
Normal file
59
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# .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: .
|
||||
23
.travis.yml
23
.travis.yml
|
|
@ -1,23 +0,0 @@
|
|||
language: python
|
||||
env:
|
||||
- TOXENV=py27-django15
|
||||
- TOXENV=py27-django16
|
||||
- TOXENV=py27-django17
|
||||
- TOXENV=py27-django18
|
||||
- TOXENV=py27-django19
|
||||
- TOXENV=py33-django15
|
||||
- TOXENV=py33-django16
|
||||
- TOXENV=py33-django17
|
||||
- TOXENV=py34-django15
|
||||
- TOXENV=py34-django16
|
||||
- TOXENV=py34-django17
|
||||
- TOXENV=py34-django18
|
||||
- TOXENV=py34-django19
|
||||
- TOXENV=py35-django18
|
||||
- TOXENV=py35-django19
|
||||
- TOXENV=flake8
|
||||
- TOXENV=sphinx
|
||||
- TOXENV=readme
|
||||
install:
|
||||
script:
|
||||
- make test
|
||||
27
AUTHORS
27
AUTHORS
|
|
@ -4,12 +4,25 @@ 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:
|
||||
|
||||
* Nicolas Tobo <https://github.com/nicolastobo>
|
||||
* Lauréline Guérin <https://github.com/zebuline>
|
||||
* Gregory Tappero <https://github.com/coulix>
|
||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
||||
* Adam Chainz <adam@adamj.eu>
|
||||
* Aleksi Häkli <aleksi.hakli@iki.fi>
|
||||
* Benoît Bryon <benoit@marmelune.net>
|
||||
|
||||
Developers: https://github.com/benoitbryon/django-downloadview/graphs/contributors
|
||||
* 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>
|
||||
|
|
|
|||
61
CHANGELOG
61
CHANGELOG
|
|
@ -4,6 +4,65 @@ 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)
|
||||
----------------
|
||||
|
|
@ -207,4 +266,4 @@ Contains **backward incompatible changes.**
|
|||
|
||||
.. target-notes::
|
||||
|
||||
.. _`milestones`: https://github.com/benoitbryon/django-downloadview/milestones
|
||||
.. _`milestones`: https://github.com/jazzband/django-downloadview/milestones
|
||||
|
|
|
|||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# 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/
|
||||
|
|
@ -2,8 +2,16 @@
|
|||
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`.
|
||||
``django-downloadview``.
|
||||
|
||||
|
||||
**************
|
||||
|
|
@ -42,11 +50,11 @@ Use topic branches
|
|||
Fork, clone
|
||||
***********
|
||||
|
||||
Clone `django-downloadview` repository (adapt to use your own fork):
|
||||
Clone ``django-downloadview`` repository (adapt to use your own fork):
|
||||
|
||||
.. code:: sh
|
||||
|
||||
git clone git@github.com:benoitbryon/django-downloadview.git
|
||||
git clone git@github.com:jazzband/django-downloadview.git
|
||||
cd django-downloadview/
|
||||
|
||||
|
||||
|
|
@ -54,7 +62,7 @@ Clone `django-downloadview` repository (adapt to use your own fork):
|
|||
Usual actions
|
||||
*************
|
||||
|
||||
The `Makefile` is the reference card for usual actions in development
|
||||
The ``Makefile`` is the reference card for usual actions in development
|
||||
environment:
|
||||
|
||||
* Install development toolkit with `pip`_: ``make develop``.
|
||||
|
|
@ -62,7 +70,7 @@ environment:
|
|||
* Run tests with `tox`_: ``make test``.
|
||||
|
||||
* Build documentation: ``make documentation``. It builds `Sphinx`_
|
||||
documentation in `var/docs/html/index.html`.
|
||||
documentation in ``var/docs/html/index.html``.
|
||||
|
||||
* Release project with `zest.releaser`_: ``make release``.
|
||||
|
||||
|
|
@ -76,7 +84,7 @@ See also ``make help``.
|
|||
Demo project included
|
||||
*********************
|
||||
|
||||
The `demo` included in project's repository is part of the tests and
|
||||
The ``demo`` included in project's repository is part of the tests and
|
||||
documentation. Maintain it along with code and documentation.
|
||||
|
||||
|
||||
|
|
@ -84,11 +92,11 @@ documentation. Maintain it along with code and documentation.
|
|||
|
||||
.. target-notes::
|
||||
|
||||
.. _`bugtracker`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues
|
||||
.. _`bugtracker`:
|
||||
https://github.com/jazzband/django-downloadview/issues
|
||||
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
|
||||
.. _`merge-based rebase`: http://tech.novapost.fr/psycho-rebasing-en.html
|
||||
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html
|
||||
.. _`pip`: https://pypi.python.org/pypi/pip/
|
||||
.. _`tox`: http://tox.testrun.org
|
||||
.. _`tox`: https://tox.readthedocs.io/
|
||||
.. _`Sphinx`: https://pypi.python.org/pypi/Sphinx/
|
||||
.. _`zest.releaser`: https://pypi.python.org/pypi/zest.releaser/
|
||||
|
|
|
|||
4
INSTALL
4
INSTALL
|
|
@ -12,8 +12,8 @@ Install
|
|||
Requirements
|
||||
************
|
||||
|
||||
`django-downloadview` has been tested with `Python`_ 2.7, 3.3 and 3.4. Other
|
||||
versions may work, but they are not part of the test suite at the moment.
|
||||
`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:
|
||||
|
|
|
|||
|
|
@ -6,4 +6,3 @@ include CONTRIBUTING.rst
|
|||
include INSTALL
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include VERSION
|
||||
|
|
|
|||
13
Makefile
13
Makefile
|
|
@ -7,7 +7,8 @@
|
|||
#
|
||||
PIP = pip
|
||||
TOX = tox
|
||||
|
||||
BLACK = black
|
||||
ISORT = isort
|
||||
|
||||
#: help - Display callable targets.
|
||||
.PHONY: help
|
||||
|
|
@ -91,8 +92,10 @@ demo:
|
|||
runserver: demo
|
||||
demo runserver
|
||||
|
||||
.PHONY: black
|
||||
black:
|
||||
$(BLACK) demo tests django_downloadview
|
||||
|
||||
#: release - Tag and push to PyPI.
|
||||
.PHONY: release
|
||||
release:
|
||||
$(TOX) -e release
|
||||
.PHONY: isort
|
||||
isort:
|
||||
$(ISORT) --recursive django_downloadview tests demo
|
||||
|
|
|
|||
41
README.rst
41
README.rst
|
|
@ -2,16 +2,40 @@
|
|||
django-downloadview
|
||||
###################
|
||||
|
||||
`django-downloadview` makes it easy to serve files with `Django`_:
|
||||
.. 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 stream the files with very little code;
|
||||
|
||||
* `django-downloadview` helps you improve performances with reverse proxies,
|
||||
* ``django-downloadview`` helps you improve performances with reverse proxies,
|
||||
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.
|
||||
|
||||
|
||||
|
|
@ -39,12 +63,11 @@ Let's serve a file stored in a file field of some model:
|
|||
Resources
|
||||
*********
|
||||
|
||||
* Documentation: http://django-downloadview.readthedocs.org
|
||||
* Documentation: https://django-downloadview.readthedocs.io
|
||||
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
||||
* Code repository: https://github.com/benoitbryon/django-downloadview
|
||||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
||||
* Roadmap: https://github.com/benoitbryon/django-downloadview/milestones
|
||||
|
||||
* 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
1
VERSION
|
|
@ -1 +0,0 @@
|
|||
1.9
|
||||
|
|
@ -3,7 +3,7 @@ Demo project
|
|||
############
|
||||
|
||||
`Demo folder in project's repository`_ contains a Django project to illustrate
|
||||
`django-downloadview` usage.
|
||||
``django-downloadview`` usage.
|
||||
|
||||
|
||||
*****************************************
|
||||
|
|
@ -31,8 +31,8 @@ Deploy the demo
|
|||
|
||||
System requirements:
|
||||
|
||||
* `Python`_ version 2.7, available as ``python`` command.
|
||||
|
||||
* `Python`_ version 3.7+, available as ``python`` command.
|
||||
|
||||
.. note::
|
||||
|
||||
You may use `Virtualenv`_ to make sure the active ``python`` is the right
|
||||
|
|
@ -44,7 +44,7 @@ Execute:
|
|||
|
||||
.. code-block:: sh
|
||||
|
||||
git clone git@github.com:benoitbryon/django-downloadview.git
|
||||
git clone git@github.com:jazzband/django-downloadview.git
|
||||
cd django-downloadview/
|
||||
make runserver
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ References
|
|||
.. target-notes::
|
||||
|
||||
.. _`demo folder in project's repository`:
|
||||
https://github.com/benoitbryon/django-downloadview/tree/master/demo/demoproject/
|
||||
https://github.com/jazzband/django-downloadview/tree/master/demo/demoproject/
|
||||
|
||||
.. _`Python`: http://python.org
|
||||
.. _`Virtualenv`: http://virtualenv.org
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview.apache import assert_x_sendfile
|
||||
|
||||
|
|
@ -12,32 +12,50 @@ 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(u'Hello world!\n'))
|
||||
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')
|
||||
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")
|
||||
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')
|
||||
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")
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
"""URL mapping."""
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = patterns(
|
||||
'demoproject.apache.views',
|
||||
url(r'^optimized-by-middleware/$',
|
||||
'optimized_by_middleware',
|
||||
name='optimized_by_middleware'),
|
||||
url(r'^optimized-by-decorator/$',
|
||||
'optimized_by_decorator',
|
||||
name='optimized_by_decorator'),
|
||||
)
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,17 +6,33 @@ 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/']))
|
||||
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_middleware = StorageDownloadView.as_view(
|
||||
storage=storage, path="hello-world.txt"
|
||||
)
|
||||
|
||||
|
||||
optimized_by_decorator = x_sendfile(
|
||||
StorageDownloadView.as_view(storage=storage, path='hello-world.txt'),
|
||||
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||
source_url=storage.base_url,
|
||||
destination_dir='/apache-optimized-by-decorator/')
|
||||
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/",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "object.document",
|
||||
"pk": 1,
|
||||
"model": "object.document",
|
||||
"fields": {
|
||||
"slug": "hello-world",
|
||||
"slug": "hello-world",
|
||||
"file": "object/hello-world.txt"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
|
@ -7,20 +7,20 @@ 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')
|
||||
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')
|
||||
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')
|
||||
url = reverse("http:avatar_url")
|
||||
response = self.client.get(url)
|
||||
assert_download_response(self,
|
||||
response,
|
||||
mime_type='image/png')
|
||||
assert_download_response(self, response, mime_type="image/png")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.urls import path
|
||||
|
||||
from demoproject.http import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^simple_url/$',
|
||||
views.simple_url,
|
||||
name='simple_url'),
|
||||
url(r'^avatar_url/$',
|
||||
views.avatar_url,
|
||||
name='avatar_url'),
|
||||
)
|
||||
app_name = "http"
|
||||
urlpatterns = [
|
||||
path("simple_url/", views.simple_url, name="simple_url"),
|
||||
path("avatar_url/", views.avatar_url, name="avatar_url"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@ 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' \
|
||||
'/benoitbryon/django-downloadview' \
|
||||
'/b7f660c5e3f37d918b106b02c5af7a887acc0111' \
|
||||
'/demo/demoproject/download/fixtures/hello-world.txt'
|
||||
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'
|
||||
return "https://avatars0.githubusercontent.com/u/235204"
|
||||
|
||||
|
||||
simple_url = SimpleURLDownloadView.as_view()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview.lighttpd import assert_x_sendfile
|
||||
|
||||
|
|
@ -12,32 +12,50 @@ 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(u'Hello world!\n'))
|
||||
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')
|
||||
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")
|
||||
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')
|
||||
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")
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
"""URL mapping."""
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = patterns(
|
||||
'demoproject.lighttpd.views',
|
||||
url(r'^optimized-by-middleware/$',
|
||||
'optimized_by_middleware',
|
||||
name='optimized_by_middleware'),
|
||||
url(r'^optimized-by-decorator/$',
|
||||
'optimized_by_decorator',
|
||||
name='optimized_by_decorator'),
|
||||
)
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,18 +6,33 @@ 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_dir = os.path.join(settings.MEDIA_ROOT, "lighttpd")
|
||||
storage = FileSystemStorage(
|
||||
location=storage_dir,
|
||||
base_url=''.join([settings.MEDIA_URL, 'lighttpd/']))
|
||||
location=storage_dir, base_url="".join([settings.MEDIA_URL, "lighttpd/"])
|
||||
)
|
||||
|
||||
|
||||
optimized_by_middleware = StorageDownloadView.as_view(storage=storage,
|
||||
path='hello-world.txt')
|
||||
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'),
|
||||
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||
source_url=storage.base_url,
|
||||
destination_dir='/lighttpd-optimized-by-decorator/')
|
||||
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/",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ from django.core.management import execute_from_command_line
|
|||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
|
||||
"{package}.settings".format(package=__package__))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{__package__}.settings")
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview.nginx import assert_x_accel_redirect
|
||||
|
||||
|
|
@ -12,14 +12,14 @@ 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(u'Hello world!\n'))
|
||||
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')
|
||||
url = reverse("nginx:optimized_by_middleware")
|
||||
response = self.client.get(url)
|
||||
assert_x_accel_redirect(
|
||||
self,
|
||||
|
|
@ -30,14 +30,15 @@ class OptimizedByMiddlewareTestCase(django.test.TestCase):
|
|||
redirect_url="/nginx-optimized-by-middleware/hello-world.txt",
|
||||
expires=None,
|
||||
with_buffering=None,
|
||||
limit_rate=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')
|
||||
url = reverse("nginx:optimized_by_decorator")
|
||||
response = self.client.get(url)
|
||||
assert_x_accel_redirect(
|
||||
self,
|
||||
|
|
@ -48,4 +49,25 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
|
|||
redirect_url="/nginx-optimized-by-decorator/hello-world.txt",
|
||||
expires=None,
|
||||
with_buffering=None,
|
||||
limit_rate=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")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
"""URL mapping."""
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = patterns(
|
||||
'demoproject.nginx.views',
|
||||
url(r'^optimized-by-middleware/$',
|
||||
'optimized_by_middleware',
|
||||
name='optimized_by_middleware'),
|
||||
url(r'^optimized-by-decorator/$',
|
||||
'optimized_by_decorator',
|
||||
name='optimized_by_decorator'),
|
||||
)
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,17 +6,33 @@ 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/']))
|
||||
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_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'),
|
||||
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||
source_url=storage.base_url,
|
||||
destination_url='/nginx-optimized-by-decorator/')
|
||||
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/",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ 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')
|
||||
file = models.FileField(upload_to="object")
|
||||
another_file = models.FileField(upload_to="object-other")
|
||||
basename = models.CharField(max_length=100)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview import temporary_media_root, assert_download_response
|
||||
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'
|
||||
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.file.save(file_name, ContentFile(file_content), save=False)
|
||||
document.another_file.save(another_name, ContentFile(another_content), save=False)
|
||||
document.save()
|
||||
return document
|
||||
|
||||
|
|
@ -33,13 +28,15 @@ class DefaultFileTestCase(django.test.TestCase):
|
|||
def test_download_response(self):
|
||||
"""'default_file' streams Document.file."""
|
||||
setup_document()
|
||||
url = reverse('object:default_file', kwargs={'slug': slug})
|
||||
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')
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename=file_name,
|
||||
mime_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
class AnotherFileTestCase(django.test.TestCase):
|
||||
|
|
@ -47,13 +44,15 @@ class AnotherFileTestCase(django.test.TestCase):
|
|||
def test_download_response(self):
|
||||
"""'another_file' streams Document.another_file."""
|
||||
setup_document()
|
||||
url = reverse('object:another_file', kwargs={'slug': slug})
|
||||
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')
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content=another_content,
|
||||
basename=another_name,
|
||||
mime_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
class DeserializedBasenameTestCase(django.test.TestCase):
|
||||
|
|
@ -61,13 +60,15 @@ class DeserializedBasenameTestCase(django.test.TestCase):
|
|||
def test_download_response(self):
|
||||
"'deserialized_basename' streams Document.file with custom basename."
|
||||
setup_document()
|
||||
url = reverse('object:deserialized_basename', kwargs={'slug': slug})
|
||||
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')
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content=file_content,
|
||||
basename=basename,
|
||||
mime_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
class InlineFileTestCase(django.test.TestCase):
|
||||
|
|
@ -75,10 +76,12 @@ class InlineFileTestCase(django.test.TestCase):
|
|||
def test_download_response(self):
|
||||
"'inline_file_view' streams Document.file inline."
|
||||
setup_document()
|
||||
url = reverse('object:inline_file', kwargs={'slug': slug})
|
||||
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)
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content=file_content,
|
||||
mime_type="text/plain",
|
||||
attachment=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.urls import re_path
|
||||
|
||||
from demoproject.object import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
app_name = "object"
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r"^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||
views.default_file_view,
|
||||
name='default_file'),
|
||||
url(r'^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
name="default_file",
|
||||
),
|
||||
re_path(
|
||||
r"^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||
views.another_file_view,
|
||||
name='another_file'),
|
||||
url(r'^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
name="another_file",
|
||||
),
|
||||
re_path(
|
||||
r"^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||
views.deserialized_basename_view,
|
||||
name='deserialized_basename'),
|
||||
url(r'^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
name="deserialized_basename",
|
||||
),
|
||||
re_path(
|
||||
r"^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||
views.inline_file_view,
|
||||
name='inline_file'),
|
||||
)
|
||||
name="inline_file",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,22 +2,19 @@ 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')
|
||||
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')
|
||||
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)
|
||||
inline_file_view = ObjectDownloadView.as_view(model=Document, attachment=False)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
|
@ -7,22 +7,26 @@ 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')
|
||||
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')
|
||||
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'})
|
||||
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')
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content="Hello world!\n",
|
||||
basename="hello-world.txt",
|
||||
mime_type="text/plain",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.urls import path, re_path
|
||||
|
||||
from demoproject.path import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^static-path/$',
|
||||
views.static_path,
|
||||
name='static_path'),
|
||||
url(r'^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
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'),
|
||||
)
|
||||
name="dynamic_path",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ 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')
|
||||
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')
|
||||
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)
|
||||
|
|
@ -27,10 +26,11 @@ class DynamicPathDownloadView(PathDownloadView):
|
|||
:class:`StorageDownloadView`
|
||||
|
||||
"""
|
||||
|
||||
def get_path(self):
|
||||
"""Return path inside fixtures directory."""
|
||||
# Get path from URL resolvers or as_view kwarg.
|
||||
relative_path = super(DynamicPathDownloadView, self).get_path()
|
||||
relative_path = super().get_path()
|
||||
# Make it absolute.
|
||||
absolute_path = os.path.join(fixtures_dir, relative_path)
|
||||
return absolute_path
|
||||
|
|
|
|||
118
demo/demoproject/settings.py
Executable file → Normal file
118
demo/demoproject/settings.py
Executable file → Normal file
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Django settings for django-downloadview demo project."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
|
|
@ -7,20 +7,20 @@ import os
|
|||
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')
|
||||
data_dir = os.path.join(root_dir, "var")
|
||||
cfg_dir = os.path.join(root_dir, "etc")
|
||||
|
||||
|
||||
# Mandatory settings.
|
||||
ROOT_URLCONF = 'demoproject.urls'
|
||||
WSGI_APPLICATION = 'demoproject.wsgi.application'
|
||||
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": os.path.join(data_dir, "db.sqlite"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,51 +29,49 @@ DATABASES = {
|
|||
SECRET_KEY = "This is a secret made public on project's repository."
|
||||
|
||||
# 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/'
|
||||
MEDIA_ROOT = os.path.join(data_dir, "media")
|
||||
MEDIA_URL = "/media/"
|
||||
STATIC_ROOT = os.path.join(data_dir, "static")
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
|
||||
# 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.
|
||||
"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',
|
||||
# Stuff that must be at the end.
|
||||
'django_nose',
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
)
|
||||
|
||||
|
||||
# BEGIN middlewares
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django_downloadview.SmartDownloadMiddleware'
|
||||
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
|
||||
|
||||
|
||||
# Specific configuration for django_downloadview.SmartDownloadMiddleware.
|
||||
# BEGIN backend
|
||||
DOWNLOADVIEW_BACKEND = 'django_downloadview.nginx.XAccelRedirectMiddleware'
|
||||
DOWNLOADVIEW_BACKEND = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
||||
# END backend
|
||||
"""Could also be:
|
||||
DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware'
|
||||
|
|
@ -83,42 +81,54 @@ DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware'
|
|||
# BEGIN rules
|
||||
DOWNLOADVIEW_RULES = [
|
||||
{
|
||||
'source_url': '/media/nginx/',
|
||||
'destination_url': '/nginx-optimized-by-middleware/',
|
||||
"source_url": "/media/nginx/",
|
||||
"destination_url": "/nginx-optimized-by-middleware/",
|
||||
},
|
||||
]
|
||||
# END rules
|
||||
DOWNLOADVIEW_RULES += [
|
||||
{
|
||||
'source_url': '/media/apache/',
|
||||
'destination_dir': '/apache-optimized-by-middleware/',
|
||||
"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',
|
||||
"backend": "django_downloadview.apache.XSendfileMiddleware",
|
||||
},
|
||||
{
|
||||
'source_url': '/media/lighttpd/',
|
||||
'destination_dir': '/lighttpd-optimized-by-middleware/',
|
||||
"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',
|
||||
"backend": "django_downloadview.lighttpd.XSendfileMiddleware",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Test/development settings.
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
NOSE_ARGS = [
|
||||
'--verbosity=2',
|
||||
'--no-path-adjustment',
|
||||
'--nocapture',
|
||||
'--all-modules',
|
||||
'--with-coverage',
|
||||
'--with-doctest',
|
||||
|
||||
|
||||
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",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
||||
storage = FileSystemStorage()
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ import datetime
|
|||
import unittest
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http.response import HttpResponseNotModified
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview import assert_download_response, temporary_media_root
|
||||
from django_downloadview import setup_view
|
||||
from django_downloadview import (
|
||||
assert_download_response,
|
||||
setup_view,
|
||||
temporary_media_root,
|
||||
)
|
||||
|
||||
from demoproject.storage import views
|
||||
|
||||
|
||||
# Fixtures.
|
||||
file_content = 'Hello world!\n'
|
||||
file_content = "Hello world!\n"
|
||||
|
||||
|
||||
def setup_file(path):
|
||||
|
|
@ -24,44 +26,48 @@ 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'})
|
||||
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')
|
||||
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'})
|
||||
setup_file("1.txt")
|
||||
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||
year = datetime.date.today().year + 4
|
||||
response = self.client.get(
|
||||
url,
|
||||
HTTP_IF_MODIFIED_SINCE='Sat, 29 Oct {year} 19:43:31 GMT'.format(
|
||||
year=datetime.date.today().year + 4)
|
||||
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'})
|
||||
setup_file("1.txt")
|
||||
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||
response = self.client.get(
|
||||
url,
|
||||
HTTP_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')
|
||||
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.
|
||||
|
|
@ -74,18 +80,21 @@ class DynamicPathIntegrationTestCase(django.test.TestCase):
|
|||
file in storage.
|
||||
|
||||
"""
|
||||
setup_file('1.TXT')
|
||||
url = reverse('storage:dynamic_path', kwargs={'path': '1.txt'})
|
||||
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')
|
||||
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.
|
||||
|
||||
|
|
@ -97,8 +106,10 @@ class DynamicPathUnitTestCase(unittest.TestCase):
|
|||
URL works. It targets only custom ``DynamicStorageDownloadView`` class.
|
||||
|
||||
"""
|
||||
view = setup_view(views.DynamicStorageDownloadView(),
|
||||
django.test.RequestFactory().get('/fake-url'),
|
||||
path='dummy path')
|
||||
view = setup_view(
|
||||
views.DynamicStorageDownloadView(),
|
||||
django.test.RequestFactory().get("/fake-url"),
|
||||
path="dummy path",
|
||||
)
|
||||
path = view.get_path()
|
||||
self.assertEqual(path, 'DUMMY PATH')
|
||||
self.assertEqual(path, "DUMMY PATH")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.urls import re_path
|
||||
|
||||
from demoproject.storage import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
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'),
|
||||
url(r'^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
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'),
|
||||
)
|
||||
name="dynamic_path",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from django.core.files.storage import FileSystemStorage
|
|||
|
||||
from django_downloadview import StorageDownloadView
|
||||
|
||||
|
||||
storage = FileSystemStorage()
|
||||
|
||||
|
||||
|
|
@ -12,9 +11,10 @@ static_path = StorageDownloadView.as_view(storage=storage)
|
|||
|
||||
class DynamicStorageDownloadView(StorageDownloadView):
|
||||
"""Serve file of storage by path.upper()."""
|
||||
|
||||
def get_path(self):
|
||||
"""Return uppercase path."""
|
||||
return super(DynamicStorageDownloadView, self).get_path().upper()
|
||||
return super().get_path().upper()
|
||||
|
||||
|
||||
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
# coding=utf8
|
||||
"""Test suite for demoproject.download."""
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
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')
|
||||
home_url = reverse("home")
|
||||
response = self.client.get(home_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
|||
62
demo/demoproject/urls.py
Executable file → Normal file
62
demo/demoproject/urls.py
Executable file → Normal file
|
|
@ -1,44 +1,44 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
home = TemplateView.as_view(template_name='home.html')
|
||||
home = TemplateView.as_view(template_name="home.html")
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
urlpatterns = [
|
||||
# ObjectDownloadView.
|
||||
url(r'^object/', include('demoproject.object.urls',
|
||||
app_name='object',
|
||||
namespace='object')),
|
||||
path(
|
||||
"object/",
|
||||
include("demoproject.object.urls", namespace="object"),
|
||||
),
|
||||
# StorageDownloadView.
|
||||
url(r'^storage/', include('demoproject.storage.urls',
|
||||
app_name='storage',
|
||||
namespace='storage')),
|
||||
path(
|
||||
"storage/",
|
||||
include("demoproject.storage.urls", namespace="storage"),
|
||||
),
|
||||
# PathDownloadView.
|
||||
url(r'^path/', include('demoproject.path.urls',
|
||||
app_name='path',
|
||||
namespace='path')),
|
||||
path("path/", include("demoproject.path.urls", namespace="path")),
|
||||
# HTTPDownloadView.
|
||||
url(r'^http/', include('demoproject.http.urls',
|
||||
app_name='http',
|
||||
namespace='http')),
|
||||
path("http/", include("demoproject.http.urls", namespace="http")),
|
||||
# VirtualDownloadView.
|
||||
url(r'^virtual/', include('demoproject.virtual.urls',
|
||||
app_name='virtual',
|
||||
namespace='virtual')),
|
||||
path(
|
||||
"virtual/",
|
||||
include("demoproject.virtual.urls", namespace="virtual"),
|
||||
),
|
||||
# Nginx optimizations.
|
||||
url(r'^nginx/', include('demoproject.nginx.urls',
|
||||
app_name='nginx',
|
||||
namespace='nginx')),
|
||||
path(
|
||||
"nginx/",
|
||||
include("demoproject.nginx.urls", namespace="nginx"),
|
||||
),
|
||||
# Apache optimizations.
|
||||
url(r'^apache/', include('demoproject.apache.urls',
|
||||
app_name='apache',
|
||||
namespace='apache')),
|
||||
path(
|
||||
"apache/",
|
||||
include("demoproject.apache.urls", namespace="apache"),
|
||||
),
|
||||
# Lighttpd optimizations.
|
||||
url(r'^lighttpd/', include('demoproject.lighttpd.urls',
|
||||
app_name='lighttpd',
|
||||
namespace='lighttpd')),
|
||||
path(
|
||||
"lighttpd/",
|
||||
include("demoproject.lighttpd.urls", namespace="lighttpd"),
|
||||
),
|
||||
# An informative homepage.
|
||||
url(r'$', home, name='home')
|
||||
)
|
||||
path("", home, name="home"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
import django.test
|
||||
from django.urls import reverse
|
||||
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
|
|
@ -7,34 +7,40 @@ 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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
assert_download_response(
|
||||
self,
|
||||
response,
|
||||
content="Hello world!\n",
|
||||
basename="hello-world.txt",
|
||||
mime_type="text/plain",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.urls import path
|
||||
|
||||
from demoproject.virtual import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^text/$',
|
||||
views.TextDownloadView.as_view(),
|
||||
name='text'),
|
||||
url(r'^stringio/$',
|
||||
views.StringIODownloadView.as_view(),
|
||||
name='stringio'),
|
||||
url(r'^gerenated/$',
|
||||
views.GeneratedDownloadView.as_view(),
|
||||
name='generated'),
|
||||
)
|
||||
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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,33 +1,31 @@
|
|||
from six import StringIO
|
||||
from io import StringIO
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from django_downloadview import VirtualDownloadView
|
||||
from django_downloadview import VirtualFile
|
||||
from django_downloadview import TextIteratorIO
|
||||
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')
|
||||
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(u"Hello world!\n")
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
file_obj = StringIO("Hello world!\n")
|
||||
return VirtualFile(file_obj, name="hello-world.txt")
|
||||
|
||||
|
||||
def generate_hello():
|
||||
yield u'Hello '
|
||||
yield u'world!'
|
||||
yield u'\n'
|
||||
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')
|
||||
return VirtualFile(file_obj, name="hello-world.txt")
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,48 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Python packaging."""
|
||||
import os
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
project_root = os.path.dirname(here)
|
||||
|
||||
|
||||
NAME = 'django-downloadview-demo'
|
||||
DESCRIPTION = 'Serve files with Django and reverse-proxies.'
|
||||
README = open(os.path.join(here, 'README.rst')).read()
|
||||
VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
|
||||
AUTHOR = u'Benoît Bryon'
|
||||
EMAIL = u'benoit@marmelune.net'
|
||||
URL = 'https://django-downloadview.readthedocs.org/'
|
||||
CLASSIFIERS = ['Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Framework :: Django']
|
||||
KEYWORDS = []
|
||||
PACKAGES = ['demoproject']
|
||||
REQUIREMENTS = [
|
||||
'django-downloadview',
|
||||
'django-nose']
|
||||
ENTRY_POINTS = {
|
||||
'console_scripts': ['demo = demoproject.manage:main']
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__': # Don't run setup() when we import this module.
|
||||
setup(name=NAME,
|
||||
version=VERSION,
|
||||
description=DESCRIPTION,
|
||||
long_description=README,
|
||||
classifiers=CLASSIFIERS,
|
||||
keywords=' '.join(KEYWORDS),
|
||||
author=AUTHOR,
|
||||
author_email=EMAIL,
|
||||
url=URL,
|
||||
license='BSD',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIREMENTS,
|
||||
entry_points=ENTRY_POINTS)
|
||||
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"]},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Serve files with Django and reverse proxies."""
|
||||
import pkg_resources
|
||||
|
||||
from django_downloadview.api import * # NoQA
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
#: Module version, as defined in PEP-0396.
|
||||
__version__ = pkg_resources.get_distribution(__package__.replace('-', '_')) \
|
||||
.version
|
||||
|
||||
|
||||
# API shortcuts.
|
||||
from django_downloadview.api import * # NoQA
|
||||
__version__ = importlib.metadata.version(__package__.replace("-", "_"))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Optimizations for Apache.
|
||||
|
||||
See also `documentation of mod_xsendfile for Apache
|
||||
|
|
@ -6,8 +5,9 @@ See also `documentation of mod_xsendfile for Apache
|
|||
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
|
||||
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
|
||||
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
|
||||
|
||||
def x_sendfile(view_func, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from django_downloadview.apache.response import XSendfileResponse
|
||||
from django_downloadview.middlewares import (ProxiedDownloadMiddleware,
|
||||
NoRedirectionMatch)
|
||||
from django_downloadview.middlewares import (
|
||||
NoRedirectionMatch,
|
||||
ProxiedDownloadMiddleware,
|
||||
)
|
||||
|
||||
|
||||
class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||
|
|
@ -12,11 +14,12 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
|||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_dir=None):
|
||||
|
||||
def __init__(
|
||||
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||
):
|
||||
"""Constructor."""
|
||||
super(XSendfileMiddleware, self).__init__(source_dir,
|
||||
source_url,
|
||||
destination_dir)
|
||||
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||
|
|
@ -24,7 +27,10 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
|||
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)
|
||||
return XSendfileResponse(
|
||||
file_path=redirect_url,
|
||||
content_type=response["Content-Type"],
|
||||
basename=response.basename,
|
||||
attachment=response.attachment,
|
||||
headers=response.headers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Apache's specific responses."""
|
||||
|
||||
import os.path
|
||||
|
||||
from django_downloadview.response import (ProxiedDownloadResponse,
|
||||
content_disposition)
|
||||
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):
|
||||
|
||||
def __init__(
|
||||
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||
):
|
||||
"""Return a HttpResponse with headers for Apache X-Sendfile."""
|
||||
super(XSendfileResponse, self).__init__(content_type=content_type)
|
||||
# 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
|
||||
self["Content-Disposition"] = content_disposition(self.basename)
|
||||
self["X-Sendfile"] = file_path
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from six import iteritems
|
||||
from django_downloadview.apache.response import XSendfileResponse
|
||||
|
||||
|
||||
class XSendfileValidator(object):
|
||||
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.
|
||||
|
||||
|
|
@ -22,8 +22,8 @@ class XSendfileValidator(object):
|
|||
|
||||
"""
|
||||
self.assert_x_sendfile_response(test_case, response)
|
||||
for key, value in iteritems(assertions):
|
||||
assert_func = getattr(self, 'assert_%s' % key)
|
||||
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):
|
||||
|
|
@ -33,15 +33,15 @@ class XSendfileValidator(object):
|
|||
test_case.assertEqual(response.basename, value)
|
||||
|
||||
def assert_content_type(self, test_case, response, value):
|
||||
test_case.assertEqual(response['Content-Type'], value)
|
||||
test_case.assertEqual(response["Content-Type"], value)
|
||||
|
||||
def assert_file_path(self, test_case, response, value):
|
||||
test_case.assertEqual(response['X-Sendfile'], value)
|
||||
test_case.assertEqual(response["X-Sendfile"], value)
|
||||
|
||||
def assert_attachment(self, test_case, response, value):
|
||||
header = 'Content-Disposition'
|
||||
header = "Content-Disposition"
|
||||
if value:
|
||||
test_case.assertTrue(response[header].startswith('attachment'))
|
||||
test_case.assertTrue(response[header].startswith("attachment"))
|
||||
else:
|
||||
test_case.assertFalse(header in response)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
"""Declaration of API shortcuts."""
|
||||
from django_downloadview.io import (BytesIteratorIO, # NoQA
|
||||
TextIteratorIO)
|
||||
from django_downloadview.files import (StorageFile, # NoQA
|
||||
VirtualFile,
|
||||
HTTPFile)
|
||||
from django_downloadview.response import (DownloadResponse, # NoQA
|
||||
ProxiedDownloadResponse)
|
||||
from django_downloadview.middlewares import (BaseDownloadMiddleware, # NoQA
|
||||
DownloadDispatcherMiddleware,
|
||||
SmartDownloadMiddleware)
|
||||
from django_downloadview.views import (PathDownloadView, # NoQA
|
||||
ObjectDownloadView,
|
||||
StorageDownloadView,
|
||||
HTTPDownloadView,
|
||||
VirtualDownloadView,
|
||||
BaseDownloadView,
|
||||
DownloadMixin)
|
||||
from django_downloadview.shortcuts import sendfile # NoQA
|
||||
from django_downloadview.test import (assert_download_response, # NoQA
|
||||
setup_view,
|
||||
temporary_media_root)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ See also decorators provided by server-specific modules, such as
|
|||
|
||||
"""
|
||||
|
||||
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.
|
||||
|
|
@ -17,16 +23,54 @@ class DownloadDecorator(object):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Custom exceptions."""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""File wrappers for use as exchange data between views and responses."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from io import BytesIO
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.files.base import File
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
import requests
|
||||
|
||||
from django_downloadview.io import BytesIteratorIO
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class StorageFile(File):
|
||||
"""A file in a Django storage.
|
||||
|
|
@ -19,6 +18,7 @@ class StorageFile(File):
|
|||
but unrelated to model instance.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, storage, name, file=None):
|
||||
"""Constructor.
|
||||
|
||||
|
|
@ -35,8 +35,8 @@ class StorageFile(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')
|
||||
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):
|
||||
|
|
@ -50,7 +50,7 @@ class StorageFile(File):
|
|||
#: Required by django.core.files.utils.FileProxy.
|
||||
file = property(_get_file, _set_file, _del_file)
|
||||
|
||||
def open(self, mode='rb'):
|
||||
def open(self, mode="rb"):
|
||||
"""Retrieves the specified file from storage and return open() result.
|
||||
|
||||
Proxy to self.storage.open(self.name, mode).
|
||||
|
|
@ -122,7 +122,10 @@ class StorageFile(File):
|
|||
Proxy to self.storage.accessed_time(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.accessed(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):
|
||||
|
|
@ -131,7 +134,10 @@ class StorageFile(File):
|
|||
Proxy to self.storage.created_time(self.name).
|
||||
|
||||
"""
|
||||
return 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):
|
||||
|
|
@ -140,12 +146,16 @@ class StorageFile(File):
|
|||
Proxy to self.storage.modified_time(self.name).
|
||||
|
||||
"""
|
||||
return 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=u'', url='', size=None):
|
||||
|
||||
def __init__(self, file=None, name="", url="", size=None):
|
||||
"""Constructor.
|
||||
|
||||
file:
|
||||
|
|
@ -158,7 +168,7 @@ class VirtualFile(File):
|
|||
File URL.
|
||||
|
||||
"""
|
||||
super(VirtualFile, self).__init__(file, name)
|
||||
super().__init__(file, name)
|
||||
self.url = url
|
||||
if size is not None:
|
||||
self._size = size
|
||||
|
|
@ -174,7 +184,7 @@ class VirtualFile(File):
|
|||
return self._size
|
||||
|
||||
def _set_size(self, value):
|
||||
return super(VirtualFile, self)._set_size(value)
|
||||
return super()._set_size(value)
|
||||
|
||||
size = property(_get_size, _set_size)
|
||||
|
||||
|
|
@ -196,7 +206,7 @@ class VirtualFile(File):
|
|||
|
||||
# If this is the end of a line, yield
|
||||
# otherwise, wait for the next round
|
||||
if line[-1] in ('\n', '\r'):
|
||||
if line[-1] in ("\n", "\r"):
|
||||
yield line
|
||||
else:
|
||||
buffer_ = line
|
||||
|
|
@ -215,19 +225,19 @@ class HTTPFile(File):
|
|||
Always sets "stream=True" in requests kwargs.
|
||||
|
||||
"""
|
||||
def __init__(self, request_factory=requests.get, url='', name=u'',
|
||||
**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]
|
||||
self.name = parts.path.strip("/").rsplit("/", 1)[-1]
|
||||
else: # Name from domain.
|
||||
self.name = parts.netloc
|
||||
else:
|
||||
self.name = name
|
||||
kwargs['stream'] = True
|
||||
kwargs["stream"] = True
|
||||
self.request_kwargs = kwargs
|
||||
|
||||
@property
|
||||
|
|
@ -235,8 +245,7 @@ class HTTPFile(File):
|
|||
try:
|
||||
return self._request
|
||||
except AttributeError:
|
||||
self._request = self.request_factory(self.url,
|
||||
**self.request_kwargs)
|
||||
self._request = self.request_factory(self.url, **self.request_kwargs)
|
||||
return self._request
|
||||
|
||||
@property
|
||||
|
|
@ -255,9 +264,9 @@ class HTTPFile(File):
|
|||
Reads response's "content-length" header.
|
||||
|
||||
"""
|
||||
return self.request.headers['Content-Length']
|
||||
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']
|
||||
return self.request.headers["Content-Type"]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Low-level IO operations, for use with file wrappers."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
|
||||
from django.utils.encoding import force_text, force_bytes
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
|
||||
class TextIteratorIO(io.TextIOBase):
|
||||
|
|
@ -15,12 +14,13 @@ class TextIteratorIO(io.TextIOBase):
|
|||
* https://gist.github.com/anacrolix/3788413
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iterator):
|
||||
#: Iterator/generator for content.
|
||||
self._iter = iterator
|
||||
|
||||
#: Internal buffer.
|
||||
self._left = u''
|
||||
self._left = ""
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
|
@ -33,45 +33,45 @@ class TextIteratorIO(io.TextIOBase):
|
|||
break
|
||||
else:
|
||||
# Make sure we handle text.
|
||||
self._left = force_text(self._left)
|
||||
self._left = force_str(self._left)
|
||||
ret = self._left[:n]
|
||||
self._left = self._left[len(ret):]
|
||||
self._left = self._left[len(ret) :]
|
||||
return ret
|
||||
|
||||
def read(self, n=None):
|
||||
"""Return content up to ``n`` length."""
|
||||
l = []
|
||||
chunks = []
|
||||
if n is None or n < 0:
|
||||
while True:
|
||||
m = self._read1()
|
||||
if not m:
|
||||
break
|
||||
l.append(m)
|
||||
chunks.append(m)
|
||||
else:
|
||||
while n > 0:
|
||||
m = self._read1(n)
|
||||
if not m:
|
||||
break
|
||||
n -= len(m)
|
||||
l.append(m)
|
||||
return u''.join(l)
|
||||
chunks.append(m)
|
||||
return "".join(chunks)
|
||||
|
||||
def readline(self):
|
||||
l = []
|
||||
chunks = []
|
||||
while True:
|
||||
i = self._left.find(u'\n')
|
||||
i = self._left.find("\n")
|
||||
if i == -1:
|
||||
l.append(self._left)
|
||||
chunks.append(self._left)
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
self._left = u''
|
||||
self._left = ""
|
||||
break
|
||||
else:
|
||||
l.append(self._left[:i + 1])
|
||||
self._left = self._left[i + 1:]
|
||||
chunks.append(self._left[: i + 1])
|
||||
self._left = self._left[i + 1 :]
|
||||
break
|
||||
return u''.join(l)
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
class BytesIteratorIO(io.BytesIO):
|
||||
|
|
@ -83,12 +83,13 @@ class BytesIteratorIO(io.BytesIO):
|
|||
* https://gist.github.com/anacrolix/3788413
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iterator):
|
||||
#: Iterator/generator for content.
|
||||
self._iter = iterator
|
||||
|
||||
#: Internal buffer.
|
||||
self._left = b''
|
||||
self._left = b""
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
|
@ -103,40 +104,40 @@ class BytesIteratorIO(io.BytesIO):
|
|||
# Make sure we handle text.
|
||||
self._left = force_bytes(self._left)
|
||||
ret = self._left[:n]
|
||||
self._left = self._left[len(ret):]
|
||||
self._left = self._left[len(ret) :]
|
||||
return ret
|
||||
|
||||
def read(self, n=None):
|
||||
"""Return content up to ``n`` length."""
|
||||
l = []
|
||||
chunks = []
|
||||
if n is None or n < 0:
|
||||
while True:
|
||||
m = self._read1()
|
||||
if not m:
|
||||
break
|
||||
l.append(m)
|
||||
chunks.append(m)
|
||||
else:
|
||||
while n > 0:
|
||||
m = self._read1(n)
|
||||
if not m:
|
||||
break
|
||||
n -= len(m)
|
||||
l.append(m)
|
||||
return b''.join(l)
|
||||
chunks.append(m)
|
||||
return b"".join(chunks)
|
||||
|
||||
def readline(self):
|
||||
l = []
|
||||
chunks = []
|
||||
while True:
|
||||
i = self._left.find(b'\n')
|
||||
i = self._left.find(b"\n")
|
||||
if i == -1:
|
||||
l.append(self._left)
|
||||
chunks.append(self._left)
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
self._left = b''
|
||||
self._left = b""
|
||||
break
|
||||
else:
|
||||
l.append(self._left[:i + 1])
|
||||
self._left = self._left[i + 1:]
|
||||
chunks.append(self._left[: i + 1])
|
||||
self._left = self._left[i + 1 :]
|
||||
break
|
||||
return b''.join(l)
|
||||
return b"".join(chunks)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Optimizations for Lighttpd.
|
||||
|
||||
See also `documentation of X-Sendfile for Lighttpd
|
||||
|
|
@ -7,8 +6,9 @@ See also `documentation of X-Sendfile for Lighttpd
|
|||
</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
|
||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
||||
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from django_downloadview.lighttpd.response import XSendfileResponse
|
||||
from django_downloadview.middlewares import (ProxiedDownloadMiddleware,
|
||||
NoRedirectionMatch)
|
||||
from django_downloadview.middlewares import (
|
||||
NoRedirectionMatch,
|
||||
ProxiedDownloadMiddleware,
|
||||
)
|
||||
|
||||
|
||||
class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||
|
|
@ -12,11 +14,12 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
|||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_dir=None):
|
||||
|
||||
def __init__(
|
||||
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||
):
|
||||
"""Constructor."""
|
||||
super(XSendfileMiddleware, self).__init__(source_dir,
|
||||
source_url,
|
||||
destination_dir)
|
||||
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||
|
|
@ -24,7 +27,10 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
|||
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)
|
||||
return XSendfileResponse(
|
||||
file_path=redirect_url,
|
||||
content_type=response["Content-Type"],
|
||||
basename=response.basename,
|
||||
attachment=response.attachment,
|
||||
headers=response.headers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Lighttpd's specific responses."""
|
||||
|
||||
import os.path
|
||||
|
||||
from django_downloadview.response import (ProxiedDownloadResponse,
|
||||
content_disposition)
|
||||
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):
|
||||
|
||||
def __init__(
|
||||
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||
):
|
||||
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
|
||||
super(XSendfileResponse, self).__init__(content_type=content_type)
|
||||
# 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
|
||||
self["Content-Disposition"] = content_disposition(self.basename)
|
||||
self["X-Sendfile"] = file_path
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class XSendfileValidator(django_downloadview.apache.tests.XSendfileValidator):
|
|||
See also :py:func:`assert_x_sendfile` shortcut function.
|
||||
|
||||
"""
|
||||
|
||||
def assert_x_sendfile_response(self, test_case, response):
|
||||
test_case.assertTrue(isinstance(response, XSendfileResponse))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Base material for download middlewares.
|
||||
|
||||
Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
|
||||
responses and may replace them with optimized download responses.
|
||||
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
import copy
|
||||
import collections
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -31,12 +31,20 @@ def is_download_response(response):
|
|||
return isinstance(response, DownloadResponse)
|
||||
|
||||
|
||||
class BaseDownloadMiddleware(object):
|
||||
class BaseDownloadMiddleware:
|
||||
"""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.
|
||||
|
||||
|
|
@ -60,6 +68,7 @@ class BaseDownloadMiddleware(object):
|
|||
|
||||
class RealDownloadMiddleware(BaseDownloadMiddleware):
|
||||
"""Download middleware that cannot handle virtual files."""
|
||||
|
||||
def is_download_response(self, response):
|
||||
"""Return True for DownloadResponse, except for "virtual" files.
|
||||
|
||||
|
|
@ -68,18 +77,12 @@ class RealDownloadMiddleware(BaseDownloadMiddleware):
|
|||
whose file attribute have either an URL or a file name.
|
||||
|
||||
"""
|
||||
if super(RealDownloadMiddleware, self).is_download_response(response):
|
||||
try:
|
||||
return response.file.url or response.file.name
|
||||
except AttributeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
return super().is_download_response(response) and bool(
|
||||
getattr(response.file, "url", None) or getattr(response.file, "name", None)
|
||||
)
|
||||
|
||||
|
||||
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
||||
"Download middleware that dispatches job to several middleware instances."
|
||||
class DownloadDispatcher:
|
||||
def __init__(self, middlewares=AUTO_CONFIGURE):
|
||||
#: List of children middlewares.
|
||||
self.middlewares = middlewares
|
||||
|
|
@ -89,35 +92,48 @@ class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
|||
def auto_configure_middlewares(self):
|
||||
"""Populate :attr:`middlewares` from
|
||||
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
|
||||
for (key, import_string, kwargs) in getattr(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 process_download_response(self, request, response):
|
||||
def dispatch(self, request, response):
|
||||
"""Dispatches job to children middlewares."""
|
||||
for (key, middleware) in self.middlewares:
|
||||
for key, middleware in self.middlewares:
|
||||
response = middleware.process_response(request, response)
|
||||
return response
|
||||
|
||||
|
||||
class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
||||
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,
|
||||
backend_factory=AUTO_CONFIGURE,
|
||||
backend_options=AUTO_CONFIGURE):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
get_response,
|
||||
backend_factory=AUTO_CONFIGURE,
|
||||
backend_options=AUTO_CONFIGURE,
|
||||
):
|
||||
"""Constructor."""
|
||||
#: :class:`DownloadDispatcher` instance that can hold multiple
|
||||
#: backend instances.
|
||||
self.dispatcher = DownloadDispatcherMiddleware(middlewares=[])
|
||||
#: Callable (typically a class) to instanciate backend (typically a
|
||||
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 instanciate backend
|
||||
#: List of positional or keyword arguments to instantiate backend
|
||||
#: instances.
|
||||
self.backend_options = backend_options
|
||||
if self.backend_options is AUTO_CONFIGURE:
|
||||
|
|
@ -128,8 +144,9 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
|||
try:
|
||||
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('SmartDownloadMiddleware requires '
|
||||
'settings.DOWNLOADVIEW_BACKEND')
|
||||
raise ImproperlyConfigured(
|
||||
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_BACKEND"
|
||||
)
|
||||
|
||||
def auto_configure_backend_options(self):
|
||||
"""Populate :attr:`dispatcher` using :attr:`factory` and
|
||||
|
|
@ -137,27 +154,24 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
|||
try:
|
||||
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('SmartDownloadMiddleware requires '
|
||||
'settings.DOWNLOADVIEW_RULES')
|
||||
raise ImproperlyConfigured(
|
||||
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_RULES"
|
||||
)
|
||||
for key, options in enumerate(options_list):
|
||||
args = []
|
||||
kwargs = {}
|
||||
if isinstance(options, collections.Mapping): # Using 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']
|
||||
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))
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Use :attr:`dispatcher` to process download response."""
|
||||
return self.dispatcher.process_download_response(request, response)
|
||||
|
||||
|
||||
class NoRedirectionMatch(Exception):
|
||||
"""Response object does not match redirection rules."""
|
||||
|
|
@ -165,8 +179,13 @@ class NoRedirectionMatch(Exception):
|
|||
|
||||
class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
||||
"""Base class for middlewares that use optimizations of reverse proxies."""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None):
|
||||
|
||||
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
|
||||
|
|
@ -174,7 +193,7 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
|||
def get_redirect_url(self, response):
|
||||
"""Return redirect URL for file wrapped into response."""
|
||||
url = None
|
||||
file_url = ''
|
||||
file_url = ""
|
||||
if self.source_url:
|
||||
try:
|
||||
file_url = response.file.url
|
||||
|
|
@ -182,9 +201,9 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
|||
pass
|
||||
else:
|
||||
if file_url.startswith(self.source_url):
|
||||
file_url = file_url[len(self.source_url):]
|
||||
file_url = file_url[len(self.source_url) :]
|
||||
url = file_url
|
||||
file_name = ''
|
||||
file_name = ""
|
||||
if url is None and self.source_dir:
|
||||
try:
|
||||
file_name = response.file.name
|
||||
|
|
@ -193,17 +212,21 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
|||
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, '/')
|
||||
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})
|
||||
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('/')))
|
||||
return "/".join((self.destination_url.rstrip("/"), url.lstrip("/")))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Optimizations for Nginx.
|
||||
|
||||
See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||
|
|
@ -6,9 +5,9 @@ See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
|||
</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
|
||||
from django_downloadview.nginx.middlewares import ( # NoQA
|
||||
XAccelRedirectMiddleware)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Decorators to apply Nginx X-Accel on a specific view."""
|
||||
|
||||
from django_downloadview.decorators import DownloadDecorator
|
||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import warnings
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from django_downloadview.middlewares import (ProxiedDownloadMiddleware,
|
||||
NoRedirectionMatch)
|
||||
from django_downloadview.middlewares import (
|
||||
NoRedirectionMatch,
|
||||
ProxiedDownloadMiddleware,
|
||||
)
|
||||
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||
|
||||
|
||||
|
|
@ -17,15 +19,26 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
|||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None,
|
||||
expires=None, with_buffering=None, limit_rate=None,
|
||||
media_root=None, media_url=None):
|
||||
|
||||
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)
|
||||
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:
|
||||
|
|
@ -33,18 +46,20 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
|||
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)
|
||||
warnings.warn(
|
||||
"%s ``media_root`` is deprecated. Use "
|
||||
"``source_dir`` instead." % self.__class__.__name__,
|
||||
DeprecationWarning,
|
||||
)
|
||||
if source_dir is None:
|
||||
source_dir = media_root
|
||||
else:
|
||||
source_dir = source_dir
|
||||
else:
|
||||
source_dir = source_dir
|
||||
super(XAccelRedirectMiddleware, self).__init__(source_dir,
|
||||
source_url,
|
||||
destination_url)
|
||||
|
||||
super().__init__(get_response, source_dir, source_url, destination_url)
|
||||
|
||||
self.expires = expires
|
||||
self.with_buffering = with_buffering
|
||||
self.limit_rate = limit_rate
|
||||
|
|
@ -62,13 +77,16 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
|||
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)
|
||||
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):
|
||||
|
|
@ -105,16 +123,20 @@ class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware):
|
|||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
|
||||
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(SingleXAccelRedirectMiddleware, self).__init__(
|
||||
"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)
|
||||
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""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.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):
|
||||
|
||||
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."""
|
||||
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
|
||||
# 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)
|
||||
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'
|
||||
self["X-Accel-Buffering"] = with_buffering and "yes" or "no"
|
||||
if expires:
|
||||
expire_seconds = timedelta(expires - now()).seconds
|
||||
self['X-Accel-Expires'] = expire_seconds
|
||||
self["X-Accel-Expires"] = expire_seconds
|
||||
elif expires is not None: # We explicitely want it off.
|
||||
self['X-Accel-Expires'] = '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'
|
||||
self["X-Accel-Limit-Rate"] = limit_rate and "%d" % limit_rate or "off"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Django settings around Nginx X-Accel.
|
||||
|
||||
.. warning::
|
||||
|
|
@ -8,26 +7,31 @@
|
|||
for details.
|
||||
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
# In version 1.3, former XAccelRedirectMiddleware has been renamed to
|
||||
# SingleXAccelRedirectMiddleware. So tell the users.
|
||||
middleware = 'django_downloadview.nginx.XAccelRedirectMiddleware'
|
||||
if middleware in settings.MIDDLEWARE_CLASSES:
|
||||
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
||||
|
||||
|
||||
if deprecated_middleware in settings.MIDDLEWARE:
|
||||
raise ImproperlyConfigured(
|
||||
'{middleware} middleware has been renamed as of django-downloadview '
|
||||
'version 1.3. You may use '
|
||||
"{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". ')
|
||||
'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.'
|
||||
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.
|
||||
|
|
@ -42,10 +46,9 @@ deprecated_msg = 'settings.{deprecated} is deprecated. You should combine ' \
|
|||
#: 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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_WITH_BUFFERING)
|
||||
|
||||
|
|
@ -61,10 +64,9 @@ if not hasattr(settings, setting_name):
|
|||
#: 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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_LIMIT_RATE)
|
||||
|
||||
|
|
@ -80,49 +82,43 @@ if not hasattr(settings, setting_name):
|
|||
#: 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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
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'
|
||||
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL"
|
||||
if hasattr(settings, setting_name):
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name),
|
||||
DeprecationWarning)
|
||||
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||
if not hasattr(settings, setting_name):
|
||||
setattr(settings, setting_name, DEFAULT_DESTINATION_URL)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from six import iteritems
|
||||
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||
|
||||
|
||||
class XAccelRedirectValidator(object):
|
||||
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.
|
||||
|
||||
|
|
@ -36,8 +36,8 @@ class XAccelRedirectValidator(object):
|
|||
|
||||
"""
|
||||
self.assert_x_accel_redirect_response(test_case, response)
|
||||
for key, value in iteritems(assertions):
|
||||
assert_func = getattr(self, 'assert_%s' % key)
|
||||
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):
|
||||
|
|
@ -47,45 +47,45 @@ class XAccelRedirectValidator(object):
|
|||
test_case.assertEqual(response.basename, value)
|
||||
|
||||
def assert_content_type(self, test_case, response, value):
|
||||
test_case.assertEqual(response['Content-Type'], 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)
|
||||
test_case.assertEqual(response["X-Accel-Redirect"], value)
|
||||
|
||||
def assert_charset(self, test_case, response, value):
|
||||
test_case.assertEqual(response['X-Accel-Charset'], value)
|
||||
test_case.assertEqual(response["X-Accel-Charset"], value)
|
||||
|
||||
def assert_with_buffering(self, test_case, response, value):
|
||||
header = 'X-Accel-Buffering'
|
||||
header = "X-Accel-Buffering"
|
||||
if value is None:
|
||||
test_case.assertFalse(header in response)
|
||||
elif value:
|
||||
test_case.assertEqual(header, 'yes')
|
||||
test_case.assertEqual(header, "yes")
|
||||
else:
|
||||
test_case.assertEqual(header, 'no')
|
||||
test_case.assertEqual(header, "no")
|
||||
|
||||
def assert_expires(self, test_case, response, value):
|
||||
header = 'X-Accel-Expires'
|
||||
header = "X-Accel-Expires"
|
||||
if value is None:
|
||||
test_case.assertFalse(header in response)
|
||||
elif not value:
|
||||
test_case.assertEqual(header, 'off')
|
||||
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'
|
||||
header = "X-Accel-Limit-Rate"
|
||||
if value is None:
|
||||
test_case.assertFalse(header in response)
|
||||
elif not value:
|
||||
test_case.assertEqual(header, 'off')
|
||||
test_case.assertEqual(header, "off")
|
||||
else:
|
||||
test_case.assertEqual(header, value)
|
||||
|
||||
def assert_attachment(self, test_case, response, value):
|
||||
header = 'Content-Disposition'
|
||||
header = "Content-Disposition"
|
||||
if value:
|
||||
test_case.assertTrue(response[header].startswith('attachment'))
|
||||
test_case.assertTrue(response[header].startswith("attachment"))
|
||||
else:
|
||||
test_case.assertFalse(header in response)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
""":py:class:`django.http.HttpResponse` subclasses."""
|
||||
import os
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
import six
|
||||
from six.moves import urllib
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
|
|
@ -13,7 +12,7 @@ from django.utils.encoding import force_str
|
|||
|
||||
|
||||
def encode_basename_ascii(value):
|
||||
u"""Return US-ASCII encoded ``value`` for Content-Disposition header.
|
||||
"""Return US-ASCII encoded ``value`` for Content-Disposition header.
|
||||
|
||||
>>> print(encode_basename_ascii(u'éà'))
|
||||
ea
|
||||
|
|
@ -31,18 +30,18 @@ def encode_basename_ascii(value):
|
|||
ea
|
||||
|
||||
"""
|
||||
if isinstance(value, six.binary_type):
|
||||
value = value.decode('utf-8')
|
||||
ascii_basename = six.text_type(value)
|
||||
ascii_basename = unicodedata.normalize('NFKD', ascii_basename)
|
||||
ascii_basename = ascii_basename.encode('ascii', 'ignore')
|
||||
ascii_basename = ascii_basename.decode('ascii')
|
||||
ascii_basename = re.sub(r'[\s]', '_', ascii_basename)
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
ascii_basename = str(value)
|
||||
ascii_basename = unicodedata.normalize("NFKD", ascii_basename)
|
||||
ascii_basename = ascii_basename.encode("ascii", "ignore")
|
||||
ascii_basename = ascii_basename.decode("ascii")
|
||||
ascii_basename = re.sub(r"[\s]", "_", ascii_basename)
|
||||
return ascii_basename
|
||||
|
||||
|
||||
def encode_basename_utf8(value):
|
||||
u"""Return UTF-8 encoded ``value`` for use in Content-Disposition header.
|
||||
"""Return UTF-8 encoded ``value`` for use in Content-Disposition header.
|
||||
|
||||
>>> print(encode_basename_utf8(u' .txt'))
|
||||
%20.txt
|
||||
|
|
@ -51,11 +50,11 @@ def encode_basename_utf8(value):
|
|||
%C3%A9%C3%A0
|
||||
|
||||
"""
|
||||
return urllib.parse.quote(force_str(value))
|
||||
return quote(force_str(value))
|
||||
|
||||
|
||||
def content_disposition(filename):
|
||||
u"""Return value of ``Content-Disposition`` header with 'attachment'.
|
||||
"""Return value of ``Content-Disposition`` header with 'attachment'.
|
||||
|
||||
>>> print(content_disposition('demo.txt'))
|
||||
attachment; filename="demo.txt"
|
||||
|
|
@ -73,15 +72,22 @@ def content_disposition(filename):
|
|||
|
||||
"""
|
||||
if not filename:
|
||||
return 'attachment'
|
||||
ascii_filename = encode_basename_ascii(filename)
|
||||
return "attachment"
|
||||
# ASCII filenames are quoted and must ensure escape sequences
|
||||
# in the filename won't break out of the quoted header value
|
||||
# which can permit a reflected file download attack. The UTF-8
|
||||
# version is immune because it's not quoted.
|
||||
ascii_filename = (
|
||||
encode_basename_ascii(filename).replace("\\", "\\\\").replace('"', r"\"")
|
||||
)
|
||||
utf8_filename = encode_basename_utf8(filename)
|
||||
if ascii_filename == utf8_filename: # ASCII only.
|
||||
return "attachment; filename=\"{ascii}\"".format(ascii=ascii_filename)
|
||||
return f'attachment; filename="{ascii_filename}"'
|
||||
else:
|
||||
return "attachment; filename=\"{ascii}\"; filename*=UTF-8''{utf8}" \
|
||||
.format(ascii=ascii_filename,
|
||||
utf8=utf8_filename)
|
||||
return (
|
||||
f'attachment; filename="{ascii_filename}"; '
|
||||
f"filename*=UTF-8''{utf8_filename}"
|
||||
)
|
||||
|
||||
|
||||
class DownloadResponse(StreamingHttpResponse):
|
||||
|
|
@ -117,9 +123,17 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
attributes (size, name, ...).
|
||||
|
||||
"""
|
||||
def __init__(self, file_instance, attachment=True, basename=None,
|
||||
status=200, content_type=None, file_mimetype=None,
|
||||
file_encoding=None):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_instance,
|
||||
attachment=True,
|
||||
basename=None,
|
||||
status=200,
|
||||
content_type=None,
|
||||
file_mimetype=None,
|
||||
file_encoding=None,
|
||||
):
|
||||
"""Constructor.
|
||||
|
||||
:param content_type: Value for ``Content-Type`` header.
|
||||
|
|
@ -131,9 +145,9 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
#: A :doc:`file wrapper instance </files>`, such as
|
||||
#: :class:`~django.core.files.base.File`.
|
||||
self.file = file_instance
|
||||
super(DownloadResponse, self).__init__(streaming_content=self.file,
|
||||
status=status,
|
||||
content_type=content_type)
|
||||
super().__init__(
|
||||
streaming_content=self.file, status=status, content_type=content_type
|
||||
)
|
||||
|
||||
#: Client-side name of the file to stream.
|
||||
#: Only used if ``attachment`` is ``True``.
|
||||
|
|
@ -144,7 +158,7 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
#: Affects ``Content-Disposition`` header.
|
||||
self.attachment = attachment
|
||||
if not content_type:
|
||||
del self['Content-Type'] # Will be set later.
|
||||
del self["Content-Type"] # Will be set later.
|
||||
|
||||
#: Value for file's mimetype.
|
||||
#: If ``None`` (the default), then the file's mimetype will be guessed
|
||||
|
|
@ -177,27 +191,17 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
return self._default_headers
|
||||
except AttributeError:
|
||||
headers = {}
|
||||
headers['Content-Type'] = self.get_content_type()
|
||||
headers["Content-Type"] = self.get_content_type()
|
||||
try:
|
||||
headers['Content-Length'] = self.file.size
|
||||
headers["Content-Length"] = self.file.size
|
||||
except (AttributeError, NotImplementedError):
|
||||
pass # Generated files.
|
||||
if self.attachment:
|
||||
basename = self.get_basename()
|
||||
headers['Content-Disposition'] = content_disposition(basename)
|
||||
headers["Content-Disposition"] = content_disposition(basename)
|
||||
self._default_headers = headers
|
||||
return self._default_headers
|
||||
|
||||
def items(self):
|
||||
"""Return iterable of (header, value).
|
||||
|
||||
This method is called by http handlers just before WSGI's
|
||||
start_response() is called... but it is not called by
|
||||
django.test.ClientHandler! :'(
|
||||
|
||||
"""
|
||||
return super(DownloadResponse, self).items()
|
||||
|
||||
def get_basename(self):
|
||||
"""Return basename."""
|
||||
if self.basename:
|
||||
|
|
@ -210,15 +214,13 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
try:
|
||||
return self.file.content_type
|
||||
except AttributeError:
|
||||
content_type_template = '{mime_type}; charset={charset}'
|
||||
return content_type_template.format(mime_type=self.get_mime_type(),
|
||||
charset=self.get_charset())
|
||||
return f"{self.get_mime_type()}; charset={self.get_charset()}"
|
||||
|
||||
def get_mime_type(self):
|
||||
"""Return mime-type of the file."""
|
||||
if self.file_mimetype is not None:
|
||||
return self.file_mimetype
|
||||
default_mime_type = 'application/octet-stream'
|
||||
default_mime_type = "application/octet-stream"
|
||||
basename = self.get_basename()
|
||||
mime_type, encoding = mimetypes.guess_type(basename)
|
||||
return mime_type or default_mime_type
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Port of django-sendfile in django-downloadview."""
|
||||
|
||||
from django_downloadview.views.path import PathDownloadView
|
||||
|
||||
|
||||
def sendfile(request, filename, attachment=False, attachment_filename=None,
|
||||
mimetype=None, encoding=None):
|
||||
def sendfile(
|
||||
request,
|
||||
filename,
|
||||
attachment=False,
|
||||
attachment_filename=None,
|
||||
mimetype=None,
|
||||
encoding=None,
|
||||
):
|
||||
"""Port of django-sendfile's API in django-downloadview.
|
||||
|
||||
Instantiates a :class:`~django_downloadview.views.path.PathDownloadView` to
|
||||
stream the file by ``filename``.
|
||||
|
||||
"""
|
||||
view = PathDownloadView.as_view(path=filename,
|
||||
attachment=attachment,
|
||||
basename=attachment_filename,
|
||||
mimetype=mimetype,
|
||||
encoding=encoding)
|
||||
view = PathDownloadView.as_view(
|
||||
path=filename,
|
||||
attachment=attachment,
|
||||
basename=attachment_filename,
|
||||
mimetype=mimetype,
|
||||
encoding=encoding,
|
||||
)
|
||||
return view(request)
|
||||
|
|
|
|||
22
django_downloadview/storage.py
Normal file
22
django_downloadview/storage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from django.core.files.storage import FileSystemStorage
|
||||
from django.core.signing import TimestampSigner
|
||||
|
||||
|
||||
class SignedURLMixin:
|
||||
"""
|
||||
Mixin for generating signed file URLs with compatible storage backends.
|
||||
|
||||
Adds X-Signature query parameters to the normal URLs generated by the storage class.
|
||||
"""
|
||||
|
||||
def url(self, name):
|
||||
path = super().url(name)
|
||||
signer = TimestampSigner()
|
||||
signature = signer.sign(path)
|
||||
return "{}?X-Signature={}".format(path, signature)
|
||||
|
||||
|
||||
class SignedFileSystemStorage(SignedURLMixin, FileSystemStorage):
|
||||
"""
|
||||
Specialized filesystem storage that signs file URLs for clients.
|
||||
"""
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
"""Testing utilities."""
|
||||
import shutil
|
||||
from six import iteritems
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -8,8 +6,7 @@ from django.test.utils import override_settings
|
|||
from django.utils.encoding import force_bytes
|
||||
|
||||
from django_downloadview.middlewares import is_download_response
|
||||
from django_downloadview.response import (encode_basename_ascii,
|
||||
encode_basename_utf8)
|
||||
from django_downloadview.response import encode_basename_ascii, encode_basename_utf8
|
||||
|
||||
|
||||
def setup_view(view, request, *args, **kwargs):
|
||||
|
|
@ -68,22 +65,24 @@ class temporary_media_root(override_settings):
|
|||
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()
|
||||
self.options["MEDIA_ROOT"] = tmp_dir
|
||||
super().enable()
|
||||
|
||||
def disable(self):
|
||||
"""Remove directory settings.MEDIA_ROOT then restore original
|
||||
setting."""
|
||||
shutil.rmtree(settings.MEDIA_ROOT)
|
||||
super(temporary_media_root, self).disable()
|
||||
super().disable()
|
||||
|
||||
|
||||
class DownloadResponseValidator(object):
|
||||
"""Utility class to validate DownloadResponse instances."""
|
||||
|
||||
def __call__(self, test_case, response, **assertions):
|
||||
"""Assert that ``response`` is a valid DownloadResponse instance.
|
||||
|
||||
|
|
@ -103,8 +102,8 @@ class DownloadResponseValidator(object):
|
|||
|
||||
"""
|
||||
self.assert_download_response(test_case, response)
|
||||
for key, value in iteritems(assertions):
|
||||
assert_func = getattr(self, 'assert_%s' % key)
|
||||
for key, value in assertions.items():
|
||||
assert_func = getattr(self, "assert_%s" % key)
|
||||
assert_func(test_case, response, value)
|
||||
|
||||
def assert_download_response(self, test_case, response):
|
||||
|
|
@ -118,40 +117,42 @@ class DownloadResponseValidator(object):
|
|||
check_ascii = False
|
||||
if ascii_name == utf8_name: # Only ASCII characters.
|
||||
check_ascii = True
|
||||
if "filename*=" in response['Content-Disposition']:
|
||||
if "filename*=" in response["Content-Disposition"]:
|
||||
check_utf8 = True
|
||||
else:
|
||||
check_utf8 = True
|
||||
if "filename=" in response['Content-Disposition']:
|
||||
if "filename=" in response["Content-Disposition"]:
|
||||
check_ascii = True
|
||||
if check_ascii:
|
||||
test_case.assertIn('filename="{name}"'.format(
|
||||
name=ascii_name),
|
||||
response['Content-Disposition'])
|
||||
test_case.assertIn(
|
||||
f'filename="{ascii_name}"',
|
||||
response["Content-Disposition"],
|
||||
)
|
||||
if check_utf8:
|
||||
test_case.assertIn(
|
||||
"filename*=UTF-8''{name}".format(name=utf8_name),
|
||||
response['Content-Disposition'])
|
||||
f"filename*=UTF-8''{utf8_name}",
|
||||
response["Content-Disposition"],
|
||||
)
|
||||
|
||||
def assert_content_type(self, test_case, response, value):
|
||||
test_case.assertEqual(response['Content-Type'], value)
|
||||
test_case.assertEqual(response["Content-Type"], value)
|
||||
|
||||
def assert_mime_type(self, test_case, response, value):
|
||||
test_case.assertTrue(response['Content-Type'].startswith(value))
|
||||
test_case.assertTrue(response["Content-Type"].startswith(value))
|
||||
|
||||
def assert_content(self, test_case, response, value):
|
||||
"""Assert value equals response's content (byte comparison)."""
|
||||
parts = [force_bytes(s) for s in response.streaming_content]
|
||||
test_case.assertEqual(b''.join(parts), force_bytes(value))
|
||||
test_case.assertEqual(b"".join(parts), force_bytes(value))
|
||||
|
||||
def assert_attachment(self, test_case, response, value):
|
||||
if value:
|
||||
test_case.assertTrue(
|
||||
'attachment;' in response['Content-Disposition'])
|
||||
test_case.assertTrue("attachment;" in response["Content-Disposition"])
|
||||
else:
|
||||
test_case.assertTrue(
|
||||
'Content-Disposition' not in response or
|
||||
'attachment;' not in response['Content-Disposition'])
|
||||
"Content-Disposition" not in response
|
||||
or "attachment;" not in response["Content-Disposition"]
|
||||
)
|
||||
|
||||
|
||||
def assert_download_response(test_case, response, **assertions):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Utility functions that may be implemented in external packages."""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
charset_pattern = re.compile(r'charset=(?P<charset>.+)$', re.I | re.U)
|
||||
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)
|
||||
|
||||
|
||||
def content_type_to_charset(content_type):
|
||||
|
|
@ -16,7 +15,7 @@ def content_type_to_charset(content_type):
|
|||
"""
|
||||
match = re.search(charset_pattern, content_type)
|
||||
if match:
|
||||
return match.group('charset')
|
||||
return match.group("charset")
|
||||
|
||||
|
||||
def url_basename(url, content_type):
|
||||
|
|
@ -30,7 +29,7 @@ def url_basename(url, content_type):
|
|||
somefile.rst
|
||||
|
||||
"""
|
||||
return url.split('/')[-1]
|
||||
return url.split("/")[-1]
|
||||
|
||||
|
||||
def import_member(import_string):
|
||||
|
|
@ -42,6 +41,6 @@ def import_member(import_string):
|
|||
True
|
||||
|
||||
"""
|
||||
module_name, factory_name = str(import_string).rsplit('.', 1)
|
||||
module_name, factory_name = str(import_string).rsplit(".", 1)
|
||||
module = __import__(module_name, globals(), locals(), [factory_name], 0)
|
||||
return getattr(module, factory_name)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""Views."""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Views to stream files."""
|
||||
|
||||
# API shortcuts.
|
||||
from django_downloadview.views.base import (DownloadMixin, # NoQA
|
||||
BaseDownloadView)
|
||||
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
|
||||
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
||||
from django_downloadview.views.object import ObjectDownloadView # NoQA
|
||||
from django_downloadview.views.path import PathDownloadView # NoQA
|
||||
from django_downloadview.views.storage import StorageDownloadView # NoQA
|
||||
from django_downloadview.views.object import ObjectDownloadView # NoQA
|
||||
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
||||
from django_downloadview.views.virtual import VirtualDownloadView # NoQA
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Base material for download views: :class:`DownloadMixin` and
|
||||
:class:`BaseDownloadView`"""
|
||||
|
||||
import calendar
|
||||
|
||||
from django.http import HttpResponseNotModified, Http404
|
||||
from django.http import Http404, HttpResponseNotModified
|
||||
from django.views.generic.base import View
|
||||
from django.views.static import was_modified_since
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ class DownloadMixin(object):
|
|||
returned by :py:meth:`get_file`.
|
||||
|
||||
"""
|
||||
|
||||
#: Response class, to be used in :py:meth:`render_to_response`.
|
||||
response_class = DownloadResponse
|
||||
|
||||
|
|
@ -102,9 +103,9 @@ class DownloadMixin(object):
|
|||
Else, fallbacks to default implementation, which uses
|
||||
:py:func:`django.views.static.was_modified_since`.
|
||||
|
||||
Django's ``was_modified_since`` function needs a datetime and a size.
|
||||
It is passed ``modified_time`` and ``size`` attributes from file
|
||||
wrapper. If file wrapper does not support these attributes
|
||||
Django's ``was_modified_since`` function needs a datetime.
|
||||
It is passed the ``modified_time`` attribute from file
|
||||
wrapper. If file wrapper does not support this attribute
|
||||
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
||||
the file is considered as modified and ``True`` is returned.
|
||||
|
||||
|
|
@ -114,12 +115,13 @@ class DownloadMixin(object):
|
|||
except (AttributeError, NotImplementedError):
|
||||
try:
|
||||
modification_time = calendar.timegm(
|
||||
file_instance.modified_time.utctimetuple())
|
||||
size = file_instance.size
|
||||
except (AttributeError, NotImplementedError):
|
||||
file_instance.modified_time.utctimetuple()
|
||||
)
|
||||
except (AttributeError, NotImplementedError) as e:
|
||||
print("!=======!", e)
|
||||
return True
|
||||
else:
|
||||
return was_modified_since(since, modification_time, size)
|
||||
return was_modified_since(since, modification_time)
|
||||
|
||||
def not_modified_response(self, *response_args, **response_kwargs):
|
||||
"""Return :class:`django.http.HttpResponseNotModified` instance."""
|
||||
|
|
@ -127,11 +129,11 @@ class DownloadMixin(object):
|
|||
|
||||
def download_response(self, *response_args, **response_kwargs):
|
||||
"""Return :class:`~django_downloadview.response.DownloadResponse`."""
|
||||
response_kwargs.setdefault('file_instance', self.file_instance)
|
||||
response_kwargs.setdefault('attachment', self.attachment)
|
||||
response_kwargs.setdefault('basename', self.get_basename())
|
||||
response_kwargs.setdefault('file_mimetype', self.get_mimetype())
|
||||
response_kwargs.setdefault('file_encoding', self.get_encoding())
|
||||
response_kwargs.setdefault("file_instance", self.file_instance)
|
||||
response_kwargs.setdefault("attachment", self.attachment)
|
||||
response_kwargs.setdefault("basename", self.get_basename())
|
||||
response_kwargs.setdefault("file_mimetype", self.get_mimetype())
|
||||
response_kwargs.setdefault("file_encoding", self.get_encoding())
|
||||
response = self.response_class(*response_args, **response_kwargs)
|
||||
return response
|
||||
|
||||
|
|
@ -155,7 +157,7 @@ class DownloadMixin(object):
|
|||
except exceptions.FileNotFound:
|
||||
return self.file_not_found_response()
|
||||
# Respect the If-Modified-Since header.
|
||||
since = self.request.META.get('HTTP_IF_MODIFIED_SINCE', None)
|
||||
since = self.request.headers.get("if-modified-since", None)
|
||||
if since is not None:
|
||||
if not self.was_modified_since(self.file_instance, since):
|
||||
return self.not_modified_response(**response_kwargs)
|
||||
|
|
@ -165,6 +167,7 @@ class DownloadMixin(object):
|
|||
|
||||
class BaseDownloadView(DownloadMixin, View):
|
||||
"""A base :class:`DownloadMixin` that implements :meth:`get`."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files given an URL, i.e. files you want to proxy."""
|
||||
import requests
|
||||
|
||||
from django_downloadview.files import HTTPFile
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class HTTPDownloadView(BaseDownloadView):
|
||||
"""Proxy files that live on remote servers."""
|
||||
|
||||
#: URL to download (the one we are proxying).
|
||||
url = u''
|
||||
url = ""
|
||||
|
||||
#: Additional keyword arguments for request handler.
|
||||
request_kwargs = {}
|
||||
|
|
@ -40,7 +41,9 @@ class HTTPDownloadView(BaseDownloadView):
|
|||
|
||||
def get_file(self):
|
||||
"""Return wrapper which has an ``url`` attribute."""
|
||||
return HTTPFile(request_factory=self.get_request_factory(),
|
||||
name=self.get_basename(),
|
||||
url=self.get_url(),
|
||||
**self.get_request_kwargs())
|
||||
return HTTPFile(
|
||||
request_factory=self.get_request_factory(),
|
||||
name=self.get_basename(),
|
||||
url=self.get_url(),
|
||||
**self.get_request_kwargs(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files that live in models."""
|
||||
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from django_downloadview.exceptions import FileNotFound
|
||||
|
|
@ -31,9 +31,10 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
|||
local filesystem.
|
||||
|
||||
"""
|
||||
|
||||
#: Name of the model's attribute which contains the file to be streamed.
|
||||
#: Typically the name of a FileField.
|
||||
file_field = 'file'
|
||||
file_field = "file"
|
||||
|
||||
#: Optional name of the model's attribute which contains the basename.
|
||||
basename_field = None
|
||||
|
|
@ -71,13 +72,11 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
|||
"""
|
||||
file_instance = getattr(self.object, self.file_field)
|
||||
if not file_instance:
|
||||
raise FileNotFound('Field="{field}" on object="{object}" is '
|
||||
'empty'.format(
|
||||
field=self.file_field,
|
||||
object=self.object))
|
||||
for field in ('encoding', 'mime_type', 'charset', 'modification_time',
|
||||
'size'):
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
raise FileNotFound(
|
||||
f'Field="{self.file_field}" on object="{self.object}" is empty'
|
||||
)
|
||||
for field in ("encoding", "mime_type", "charset", "modification_time", "size"):
|
||||
model_field = getattr(self, "%s_field" % field, False)
|
||||
if model_field:
|
||||
value = getattr(self.object, model_field)
|
||||
setattr(file_instance, field, value)
|
||||
|
|
@ -85,14 +84,14 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
|||
|
||||
def get_basename(self):
|
||||
"""Return client-side filename."""
|
||||
basename = super(ObjectDownloadView, self).get_basename()
|
||||
basename = super().get_basename()
|
||||
if basename is None:
|
||||
field = 'basename'
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
field = "basename"
|
||||
model_field = getattr(self, "%s_field" % field, False)
|
||||
if model_field:
|
||||
basename = getattr(self.object, model_field)
|
||||
return basename
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super(ObjectDownloadView, self).get(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
""":class:`PathDownloadView`."""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.files import File
|
||||
|
|
@ -10,6 +10,7 @@ from django_downloadview.views.base import BaseDownloadView
|
|||
|
||||
class PathDownloadView(BaseDownloadView):
|
||||
"""Serve a file using filename."""
|
||||
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
#: Filename is supposed to be an absolute filename of a file located on the
|
||||
|
|
@ -17,7 +18,7 @@ class PathDownloadView(BaseDownloadView):
|
|||
path = None
|
||||
|
||||
#: Name of the URL argument that contains path.
|
||||
path_url_kwarg = 'path'
|
||||
path_url_kwarg = "path"
|
||||
|
||||
def get_path(self):
|
||||
"""Return actual path of the file to serve.
|
||||
|
|
@ -35,5 +36,5 @@ class PathDownloadView(BaseDownloadView):
|
|||
"""Use path to return wrapper around file to serve."""
|
||||
filename = self.get_path()
|
||||
if not os.path.isfile(filename):
|
||||
raise FileNotFound('File "{0}" does not exists'.format(filename))
|
||||
return File(open(filename, 'rb'))
|
||||
raise FileNotFound(f'File "{filename}" does not exists')
|
||||
return File(open(filename, "rb"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files from storage."""
|
||||
|
||||
from django.core.files.storage import DefaultStorage
|
||||
|
||||
from django_downloadview.files import StorageFile
|
||||
|
|
@ -8,22 +8,13 @@ from django_downloadview.views.path import PathDownloadView
|
|||
|
||||
class StorageDownloadView(PathDownloadView):
|
||||
"""Serve a file using storage and filename."""
|
||||
|
||||
#: Storage the file to serve belongs to.
|
||||
storage = DefaultStorage()
|
||||
|
||||
#: Path to the file to serve relative to storage.
|
||||
path = None # Override docstring.
|
||||
|
||||
def get_path(self):
|
||||
"""Return path of the file to serve, relative to storage.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path` attribute.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
|
||||
"""
|
||||
return super(StorageDownloadView, self).get_path()
|
||||
|
||||
def get_file(self):
|
||||
"""Return :class:`~django_downloadview.files.StorageFile` instance."""
|
||||
return StorageFile(self.storage, self.get_path())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Stream files that you generate or that live in memory."""
|
||||
|
||||
from django_downloadview.views.base import BaseDownloadView
|
||||
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ class VirtualDownloadView(BaseDownloadView):
|
|||
Override the :py:meth:`get_file` method to customize file wrapper.
|
||||
|
||||
"""
|
||||
|
||||
def was_modified_since(self, file_instance, since):
|
||||
"""Delegate to file wrapper's was_modified_since, or return True.
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,6 @@ Here are main differences between the two projects:
|
|||
.. target-notes::
|
||||
|
||||
.. _`django.contrib.staticfiles provides a view to serve files`:
|
||||
https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#static-file-development-view
|
||||
https://docs.djangoproject.com/en/3.0/ref/contrib/staticfiles/#static-file-development-view
|
||||
.. _`Django ticket #2131`: https://code.djangoproject.com/ticket/2131
|
||||
.. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ optimizations.
|
|||
|
||||
* :doc:`/about/alternatives`
|
||||
* `roadmap
|
||||
<https://github.com/benoitbryon/django-downloadview/milestones>`_
|
||||
<https://github.com/jazzband/django-downloadview/milestones>`_
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Django`: https://djangoproject.com
|
||||
.. _`Django`: https://www.djangoproject.com
|
||||
|
|
|
|||
100
docs/conf.py
100
docs/conf.py
|
|
@ -1,14 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""django-downloadview documentation build configuration file."""
|
||||
import os
|
||||
|
||||
import re
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
|
||||
# django-downloadview depends on Django...
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
settings.configure(
|
||||
DATABASES={}, # Required to load ``django.views.generic``.
|
||||
)
|
||||
|
|
@ -18,63 +18,58 @@ settings.configure(
|
|||
|
||||
# Extensions.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.intersphinx',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.intersphinx",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.txt'
|
||||
source_suffix = ".txt"
|
||||
|
||||
# The encoding of source files.
|
||||
source_encoding = 'utf-8'
|
||||
source_encoding = "utf-8"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-downloadview'
|
||||
project_slug = re.sub(r'([\w_.-]+)', u'-', project)
|
||||
copyright = u'2012-2015, Benoît Bryon'
|
||||
author = u'Benoît Bryon'
|
||||
author_slug = re.sub(r'([\w_.-]+)', u'-', author)
|
||||
project = "django-downloadview"
|
||||
project_slug = re.sub(r"([\w_.-]+)", "-", project)
|
||||
copyright = "2012-2015, Benoît Bryon"
|
||||
author = "Benoît Bryon"
|
||||
author_slug = re.sub(r"([\w_.-]+)", "-", author)
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
configuration_dir = os.path.dirname(__file__)
|
||||
documentation_dir = configuration_dir
|
||||
version_file = os.path.normpath(os.path.join(
|
||||
documentation_dir,
|
||||
'../VERSION'))
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = open(version_file).read().strip()
|
||||
release = importlib.metadata.version("django-downloadview")
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[0:1])
|
||||
version = ".".join(release.split(".")[:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
language = 'en'
|
||||
language = "en"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
html_theme = "alabaster"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
|
|
@ -83,23 +78,22 @@ html_static_path = []
|
|||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'**': ['globaltoc.html',
|
||||
'relations.html',
|
||||
'sourcelink.html',
|
||||
'searchbox.html'],
|
||||
"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"],
|
||||
}
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = u'{project}doc'.format(project=project_slug)
|
||||
htmlhelp_basename = "{project}doc".format(project=project_slug)
|
||||
|
||||
|
||||
# -- Options for sphinx.ext.intersphinx ---------------------------------------
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/2.7', None),
|
||||
'django': ('http://docs.djangoproject.com/en/1.8/',
|
||||
'http://docs.djangoproject.com/en/1.8/_objects/'),
|
||||
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
"django": (
|
||||
"https://docs.djangoproject.com/en/3.1/",
|
||||
"https://docs.djangoproject.com/en/3.1/_objects/",
|
||||
),
|
||||
"requests": ("https://requests.readthedocs.io/en/master/", None),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -111,11 +105,13 @@ latex_elements = {}
|
|||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index',
|
||||
u'{project}.tex'.format(project=project_slug),
|
||||
u'{project} Documentation'.format(project=project),
|
||||
author,
|
||||
'manual'),
|
||||
(
|
||||
"index",
|
||||
"{project}.tex".format(project=project_slug),
|
||||
"{project} Documentation".format(project=project),
|
||||
author,
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -124,11 +120,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index',
|
||||
project,
|
||||
u'{project} Documentation'.format(project=project),
|
||||
[author],
|
||||
1)
|
||||
("index", project, "{project} Documentation".format(project=project), [author], 1)
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -138,11 +130,13 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index',
|
||||
project_slug,
|
||||
u'{project} Documentation'.format(project=project),
|
||||
author,
|
||||
project,
|
||||
'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
project_slug,
|
||||
"{project} Documentation".format(project=project),
|
||||
author,
|
||||
project,
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Here are tips to migrate from `django-sendfile` to `django-downloadview`...
|
|||
* setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do
|
||||
more.
|
||||
* register ``django_downloadview.SmartDownloadMiddleware`` in
|
||||
``MIDDLEWARE_CLASSES``.
|
||||
``MIDDLEWARE``.
|
||||
|
||||
4. Change your tests if any. You can no longer use `django-senfile`'s
|
||||
``development`` backend. See :doc:`/testing` for `django-downloadview`'s
|
||||
|
|
@ -54,4 +54,4 @@ API reference
|
|||
|
||||
.. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile
|
||||
.. _`django-downloadview's bugtracker`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues
|
||||
https://github.com/jazzband/django-downloadview/issues
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Django's builtins
|
|||
wrapper in :doc:`/views/path`.
|
||||
|
||||
* :class:`django.db.models.fields.files.FieldFile` wraps a file that is
|
||||
managed in a model. ``django-downloadview`` uses this wrapper in
|
||||
managed in a model. ``django-downloadview`` uses this wrapper in
|
||||
:doc:`/views/object`.
|
||||
|
||||
* :class:`django.core.files.base.ContentFile` wraps a bytes, string or
|
||||
|
|
@ -127,4 +127,4 @@ TextIteratorIO
|
|||
.. target-notes::
|
||||
|
||||
.. _`Django itself provides some file wrappers`:
|
||||
https://docs.djangoproject.com/en/1.9/ref/files/file/
|
||||
https://docs.djangoproject.com/en/3.0/ref/files/file/
|
||||
|
|
|
|||
|
|
@ -45,26 +45,26 @@ Setup XSendfile middlewares
|
|||
***************************
|
||||
|
||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
||||
``MIDDLEWARE`` of your `Django` settings.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 64-71
|
||||
:lines: 63-70
|
||||
|
||||
Then set ``django_downloadview.apache.XSendfileMiddleware`` as
|
||||
``DOWNLOADVIEW_BACKEND``:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 77
|
||||
:lines: 79
|
||||
|
||||
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 80, 85-87, 93, 103
|
||||
:lines: 84, 92-100, 110
|
||||
|
||||
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
|
||||
to the middleware factory. In the example above, we capture responses by
|
||||
|
|
@ -128,4 +128,4 @@ setup.
|
|||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/
|
||||
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/
|
||||
|
|
|
|||
|
|
@ -87,6 +87,6 @@ Here are optimizations builtin `django_downloadview`:
|
|||
.. target-notes::
|
||||
|
||||
.. _`tell us`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues?labels=optimizations
|
||||
https://github.com/jazzband/django-downloadview/issues?labels=optimizations
|
||||
.. _`a feature request about "local cache" for streamed files`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues/70
|
||||
https://github.com/jazzband/django-downloadview/issues/70
|
||||
|
|
|
|||
|
|
@ -51,26 +51,26 @@ Setup XSendfile middlewares
|
|||
***************************
|
||||
|
||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
||||
``MIDDLEWARE`` of your `Django` settings.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 64-71
|
||||
:lines: 63-70
|
||||
|
||||
Then set ``django_downloadview.lighttpd.XSendfileMiddleware`` as
|
||||
``DOWNLOADVIEW_BACKEND``:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 78
|
||||
:lines: 80
|
||||
|
||||
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 80, 94-96, 102, 103
|
||||
:lines: 84, 101-110
|
||||
|
||||
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
|
||||
to the middleware factory. In the example above, we capture responses by
|
||||
|
|
@ -137,4 +137,4 @@ setup.
|
|||
.. _`Lighttpd X-Sendfile documentation`:
|
||||
http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file
|
||||
.. _`X-Sendfile2 feature request on django_downloadview's bugtracker`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues/67
|
||||
https://github.com/jazzband/django-downloadview/issues/67
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Let's consider the following view:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/nginx/views.py
|
||||
:language: python
|
||||
:lines: 1-6, 8-16
|
||||
:lines: 1-6, 8-17
|
||||
|
||||
What is important here is that the files will have an ``url`` property
|
||||
implemented by storage. Let's setup an optimization rule based on that URL.
|
||||
|
|
@ -46,13 +46,13 @@ Setup XAccelRedirect middlewares
|
|||
********************************
|
||||
|
||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
||||
``MIDDLEWARE`` of your `Django` settings.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 64-71
|
||||
:lines: 62-69
|
||||
|
||||
Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as
|
||||
``DOWNLOADVIEW_BACKEND``:
|
||||
|
|
@ -65,7 +65,7 @@ Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 80, 81-84, 103
|
||||
:lines: 83-88
|
||||
|
||||
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
|
||||
to the middleware factory. In the example above, we capture responses by
|
||||
|
|
@ -136,13 +136,13 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
|
|||
# like /optimized-download/myfile.tar.gz
|
||||
#
|
||||
# See http://wiki.nginx.org/X-accel
|
||||
# and https://django-downloadview.readthedocs.org
|
||||
# and https://django-downloadview.readthedocs.io
|
||||
#
|
||||
location /proxied-download {
|
||||
internal;
|
||||
# Location to files on disk.
|
||||
alias /var/www/files/;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy to Django-powered frontend.
|
||||
location / {
|
||||
|
|
@ -154,7 +154,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
|
|||
}
|
||||
|
||||
... where specific configuration is the ``location /optimized-download``
|
||||
section.
|
||||
section.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ Add ``charset utf-8;`` in your nginx configuration file.
|
|||
``open() "path/to/something" failed (2: No such file or directory)``
|
||||
====================================================================
|
||||
|
||||
Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT`` in Django
|
||||
Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR`` in Django
|
||||
configuration VS ``alias`` in nginx configuration: in a standard configuration,
|
||||
they should be equal.
|
||||
|
||||
|
|
@ -192,4 +192,4 @@ they should be equal.
|
|||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
||||
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ possible, file wrappers do not embed file data, in order to save memory.
|
|||
|
||||
Learn more about available file wrappers in :doc:`files`.
|
||||
|
||||
|
||||
|
||||
*****************************************************************
|
||||
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
||||
*****************************************************************
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ There is no need to register this application in ``INSTALLED_APPS``.
|
|||
|
||||
|
||||
******************
|
||||
MIDDLEWARE_CLASSES
|
||||
MIDDLEWARE
|
||||
******************
|
||||
|
||||
If you plan to setup :doc:`reverse-proxy optimizations </optimizations/index>`,
|
||||
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
|
||||
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE``.
|
||||
It is a response middleware. Move it after middlewares that compute the
|
||||
response content such as gzip middleware.
|
||||
|
||||
|
|
@ -29,6 +29,48 @@ Example:
|
|||
:end-before: END middlewares
|
||||
|
||||
|
||||
********************
|
||||
DEFAULT_FILE_STORAGE
|
||||
********************
|
||||
|
||||
django-downloadview offers a built-in signed file storage, which cryptographically
|
||||
signs requested file URLs with the Django's built-in TimeStampSigner.
|
||||
|
||||
To utilize the signed storage views you can configure
|
||||
|
||||
.. code:: python
|
||||
|
||||
DEFAULT_FILE_STORAGE='django_downloadview.storage.SignedStorage'
|
||||
|
||||
The signed file storage system inserts a ``X-Signature`` header to the requested file
|
||||
URLs, and they can then be verified with the supplied ``signature_required`` wrapper function:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.conf.urls import url, url_patterns
|
||||
|
||||
from django_downloadview import ObjectDownloadView
|
||||
from django_downloadview.decorators import signature_required
|
||||
|
||||
from demoproject.download.models import Document # A model with a FileField
|
||||
|
||||
# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
|
||||
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
||||
|
||||
urlpatterns = [
|
||||
path('download/<str:slug>/', signature_required(download)),
|
||||
]
|
||||
|
||||
Make sure to test the desired functionality after configuration.
|
||||
|
||||
***************************
|
||||
DOWNLOADVIEW_URL_EXPIRATION
|
||||
***************************
|
||||
|
||||
Number of seconds signed download URLs are valid before expiring.
|
||||
|
||||
Default value for this flag is None and URLs never expire.
|
||||
|
||||
********************
|
||||
DOWNLOADVIEW_BACKEND
|
||||
********************
|
||||
|
|
@ -49,7 +91,7 @@ Example:
|
|||
See :doc:`/optimizations/index` for a list of available backends (middlewares).
|
||||
|
||||
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
||||
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
|
||||
``MIDDLEWARE``, this setting must be explicitely configured (no default
|
||||
value). Else, you can ignore this setting.
|
||||
|
||||
|
||||
|
|
@ -77,5 +119,5 @@ See :doc:`/optimizations/index` for details about builtin backends
|
|||
(middlewares) and their options.
|
||||
|
||||
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
||||
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
|
||||
``MIDDLEWARE``, this setting must be explicitely configured (no default
|
||||
value). Else, you can ignore this setting.
|
||||
|
|
|
|||
|
|
@ -41,4 +41,3 @@ Example, related to :doc:`StorageDownloadView demo </views/storage>`:
|
|||
.. literalinclude:: /../demo/demoproject/storage/tests.py
|
||||
:language: python
|
||||
:lines: 1-2, 8-12, 59-
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ See details in :attr:`attachment API documentation
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||
:language: python
|
||||
:lines: 1-5, 20-23
|
||||
:lines: 1-2, 19
|
||||
|
||||
|
||||
************************************
|
||||
|
|
@ -87,7 +87,7 @@ Modified" response:
|
|||
class TextDownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return :class:`django.core.files.base.ContentFile` object."""
|
||||
return ContentFile(u"Hello world!", name='hello-world.txt')
|
||||
return ContentFile("Hello world!", name='hello-world.txt')
|
||||
|
||||
def was_modified_since(self, file_instance, since):
|
||||
return False # Never modified, always u"Hello world!".
|
||||
return False # Never modified, always "Hello world!".
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Setup a view to stream the ``file`` attribute:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||
:language: python
|
||||
:lines: 1-5, 7
|
||||
:lines: 1-6
|
||||
|
||||
:class:`~django_downloadview.views.object.ObjectDownloadView` inherits from
|
||||
:class:`~django.views.generic.detail.BaseDetailView`, i.e. it expects either
|
||||
|
|
@ -38,7 +38,7 @@ Setup a view to stream the ``file`` attribute:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/object/urls.py
|
||||
:language: python
|
||||
:lines: 1-7, 8-10, 20
|
||||
:lines: 1-7, 8-11, 27
|
||||
|
||||
|
||||
************
|
||||
|
|
@ -69,7 +69,7 @@ Then here is the code to serve "another_file" instead of the default "file":
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||
:language: python
|
||||
:lines: 1-5, 10-12
|
||||
:lines: 1-4, 8-11
|
||||
|
||||
|
||||
**********************************
|
||||
|
|
@ -90,7 +90,7 @@ Then you can configure the :attr:`ObjectDownloadView.basename_field` option:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||
:language: python
|
||||
:lines: 1-5, 16-18
|
||||
:lines: 1-4, 13-17
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue