mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-05-10 16:44:48 +00:00
Compare commits
331 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b711c4ecf9 | ||
|
|
202395db9f | ||
|
|
b5f95124ef | ||
|
|
4fed740987 | ||
|
|
64e75d79d4 | ||
|
|
37e4db85be | ||
|
|
ec17a5bad6 | ||
|
|
668df75d21 | ||
|
|
4b7159e8f2 | ||
|
|
3a9ca52f16 | ||
|
|
6ad8a1e6bd | ||
|
|
30e2c7ef80 | ||
|
|
354b17b2c7 | ||
|
|
6be44f275b | ||
|
|
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 | ||
|
|
2862d2f24e | ||
|
|
0c445389f7 | ||
|
|
e37612f2b9 | ||
|
|
56214264f1 | ||
|
|
18fd9abcc8 | ||
|
|
5fecce8017 | ||
|
|
ec969efa7e | ||
|
|
08031dab21 | ||
|
|
a3203f2298 | ||
|
|
1ec890fb20 | ||
|
|
ae54019a27 | ||
|
|
c18e57037b | ||
|
|
3a3d3ebb9f | ||
|
|
bdf8e6e368 | ||
|
|
150e50653f | ||
|
|
ea033b8eb2 | ||
|
|
bf7b0f23fd | ||
|
|
5e4ae2fdc9 | ||
|
|
75b51ce214 | ||
|
|
142d8de7fa | ||
|
|
2596bc60e1 | ||
|
|
652e5de854 | ||
|
|
cfc57c19f0 | ||
|
|
0c282664ca | ||
|
|
5fa0a6caea | ||
|
|
8a0883d461 | ||
|
|
211fd5461b | ||
|
|
2d04d30ba4 | ||
|
|
6f9a79c1df | ||
|
|
62c0c01304 | ||
|
|
0160c00df1 | ||
|
|
ec96afda11 | ||
|
|
d122c68455 | ||
|
|
c54131db6e | ||
|
|
618aa78455 | ||
|
|
f04a4b4cd4 | ||
|
|
047745f084 | ||
|
|
b2fa7754cc | ||
|
|
6a3bb9affb | ||
|
|
e685f9909d | ||
|
|
9b87f181bf | ||
|
|
ca9c9b3caf | ||
|
|
8a6b04c5a4 | ||
|
|
3717696dda | ||
|
|
6ad1ae9704 | ||
|
|
7d30adb802 | ||
|
|
def8df90ee | ||
|
|
fbb0f30f2a | ||
|
|
3b124f0cdb | ||
|
|
d7073be658 | ||
|
|
616c822b6c | ||
|
|
77166d2969 | ||
|
|
1407c0cc4e | ||
|
|
7b2d8215e6 | ||
|
|
5c262dfa52 | ||
|
|
5c92f5ac87 | ||
|
|
df6c52b571 | ||
|
|
7aca2833bd | ||
|
|
abd6d5daff | ||
|
|
8bfbbb36fc | ||
|
|
d29e3532e8 | ||
|
|
eeaabd2a37 | ||
|
|
ffef9ce703 | ||
|
|
7875999fe2 | ||
|
|
8e83ec559f | ||
|
|
af0ffa3e2c | ||
|
|
3b725c2ab4 | ||
|
|
5850ffac81 | ||
|
|
5cde2b0d2d | ||
|
|
2d4f8c9773 | ||
|
|
cfb6964af3 | ||
|
|
70c6b00442 | ||
|
|
546bdc2461 | ||
|
|
1ea2790ba9 | ||
|
|
21cb8f6409 | ||
|
|
558bd757a3 | ||
|
|
6232fcb736 | ||
|
|
d587c515f0 | ||
|
|
2962b84135 | ||
|
|
e28951d0a1 | ||
|
|
32322c1163 | ||
|
|
40f407b627 | ||
|
|
9b2a745383 | ||
|
|
6dd090757a | ||
|
|
59d9b4966e | ||
|
|
194699045c | ||
|
|
e33a8165ef | ||
|
|
b5191c6a6f | ||
|
|
ce31392efb | ||
|
|
8b44724c90 | ||
|
|
13c0f8b103 | ||
|
|
8210e87bc6 | ||
|
|
62b8a80f7f | ||
|
|
dd6c4bf70a | ||
|
|
b8c5120f20 | ||
|
|
cf086e0458 | ||
|
|
8d3d119be8 | ||
|
|
567cf591b9 | ||
|
|
0b2a26e180 | ||
|
|
b31e7b16ed | ||
|
|
f2d5a92b99 | ||
|
|
cbf1e19f84 | ||
|
|
fce6583583 | ||
|
|
448e9e849c | ||
|
|
bb982b84be | ||
|
|
a0f7b7400b | ||
|
|
b0ddcc9202 | ||
|
|
fb1de9996d | ||
|
|
f170c8904b | ||
|
|
443d76b636 | ||
|
|
c141f027de | ||
|
|
c372cac003 | ||
|
|
c32313ca8e | ||
|
|
e9d9b3dfaa | ||
|
|
e900c1a253 | ||
|
|
970820c5e2 | ||
|
|
6a4f421892 | ||
|
|
2c1ad3c730 | ||
|
|
0371f84f26 | ||
|
|
def3e97a39 | ||
|
|
4c3787b2a0 | ||
|
|
1790094422 | ||
|
|
2e5d778651 | ||
|
|
9f82aea15b | ||
|
|
874f3b9b54 | ||
|
|
413f7a9052 | ||
|
|
cb68d7f8e5 | ||
|
|
47cc4ce04d | ||
|
|
4547cc2f90 | ||
|
|
a38876d15c | ||
|
|
7c3b3a8de2 | ||
|
|
f34deba503 | ||
|
|
5f0e40e23f | ||
|
|
80392705b7 | ||
|
|
81e9607b82 | ||
|
|
f623728bb9 | ||
|
|
a93561601a | ||
|
|
fbfd2ecee4 | ||
|
|
588b1b0a6e | ||
|
|
47fc5bc9e3 | ||
|
|
b7f660c5e3 |
157 changed files with 6508 additions and 2453 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@v6
|
||||||
|
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@v6
|
||||||
|
|
||||||
|
- 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@v5
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
name: Python ${{ matrix.python-version }}
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
|
|
@ -6,11 +6,27 @@
|
||||||
|
|
||||||
# Data files.
|
# Data files.
|
||||||
/var/
|
/var/
|
||||||
|
coverage.xml
|
||||||
|
.coverage/
|
||||||
|
|
||||||
# Python files.
|
# Python files.
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|
||||||
|
# Tox files.
|
||||||
|
/.tox/
|
||||||
|
.eggs
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtualenv files (created by tox).
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Virtual environments (created by user).
|
||||||
|
/venv/
|
||||||
|
|
||||||
# Editors' temporary buffers.
|
# Editors' temporary buffers.
|
||||||
.*.swp
|
.*.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.30.0
|
||||||
|
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: v10.3.0
|
||||||
|
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.15.12'
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
|
rev: v2.21.1
|
||||||
|
hooks:
|
||||||
|
- id: pyproject-fmt
|
||||||
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
|
rev: v0.25
|
||||||
|
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: .
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
|
||||||
install: make configure develop
|
|
||||||
script: make test
|
|
||||||
25
AUTHORS
25
AUTHORS
|
|
@ -4,10 +4,25 @@ Authors & contributors
|
||||||
|
|
||||||
Maintainer: Benoît Bryon <benoit@marmelune.net>
|
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>
|
* Adam Chainz <adam@adamj.eu>
|
||||||
* Lauréline Guérin <https://github.com/zebuline>
|
* Aleksi Häkli <aleksi.hakli@iki.fi>
|
||||||
* Gregory Tappero <https://github.com/coulix>
|
|
||||||
* Benoît Bryon <benoit@marmelune.net>
|
* Benoît Bryon <benoit@marmelune.net>
|
||||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
* 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>
|
||||||
|
|
|
||||||
234
CHANGELOG
234
CHANGELOG
|
|
@ -1,15 +1,236 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
This document describes changes between past releases. For information about
|
||||||
|
future releases, check `milestones`_ and :doc:`/about/vision`.
|
||||||
|
|
||||||
|
2.6.0 (unreleased)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- No changes yet
|
||||||
|
|
||||||
|
|
||||||
|
2.5.0 (2025-10-28)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Upgrade to Django 5.2 and Python 3.14
|
||||||
|
|
||||||
|
|
||||||
|
2.4.0 (2024-08-05)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Drop support for Python 3.6
|
||||||
|
- Escape malicious filenames
|
||||||
|
- Handle headers in XAccel responses
|
||||||
|
|
||||||
|
|
||||||
|
2.3.0 (2022-01-11)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Drop Django 3.0 support
|
||||||
|
- Add Django 3.2 support
|
||||||
|
- Add support for Python 3.10
|
||||||
|
- Add support for Django 4.0
|
||||||
|
- Remove support for Python 3.5 and Django 1.11
|
||||||
|
- Add support for Python 3.9 and Django 3.1
|
||||||
|
- Remove old urls syntax and adopt the new one
|
||||||
|
- Move the project to the jazzband organization
|
||||||
|
- Adopt black automatic formatting rules
|
||||||
|
|
||||||
|
|
||||||
|
2.1.1 (2020-01-14)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix missing function parameter. (#152)
|
||||||
|
|
||||||
|
|
||||||
|
2.1 (2020-01-13)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Add a SignedFileSystemStorage that signs file URLs for clients. (#151)
|
||||||
|
|
||||||
|
|
||||||
|
2.0 (2020-01-07)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Drop support for Python 2.7.
|
||||||
|
- Add black and isort.
|
||||||
|
|
||||||
|
|
||||||
|
1.10 (2020-01-07)
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Introduced support from Django 1.11, 2.2 and 3.0.
|
||||||
|
- Drop support of Django 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 2.0 and 2.1
|
||||||
|
|
||||||
|
|
||||||
|
1.9 (2016-03-15)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Feature #112 - Introduced support of Django 1.9.
|
||||||
|
|
||||||
|
- Feature #113 - Introduced support of Python 3.5.
|
||||||
|
|
||||||
|
- Feature #116 - ``HTTPFile`` has ``content_type`` property. It makes
|
||||||
|
``HTTPDownloadView`` proxy ``Content-Type`` header from remote location.
|
||||||
|
|
||||||
|
|
||||||
|
1.8 (2015-07-20)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Bugfixes.
|
||||||
|
|
||||||
|
- Bugfix #103 - ``PathDownloadView.get_file()`` makes a single call to
|
||||||
|
``PathDownloadView.get_file()`` (was doing it twice).
|
||||||
|
|
||||||
|
- Bugfix #104 - Pass numeric timestamp to Django's ``was_modified_since()``
|
||||||
|
(was passing a datetime).
|
||||||
|
|
||||||
|
|
||||||
|
1.7 (2015-06-13)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Bugfixes.
|
||||||
|
|
||||||
|
- Bugfix #87 - Filenames with commas are now supported. In download responses,
|
||||||
|
filename is now surrounded by double quotes.
|
||||||
|
|
||||||
|
- Bugfix #97 - ``HTTPFile`` proxies bytes as ``BytesIteratorIO`` (was undecoded
|
||||||
|
urllib3 file object). ``StringIteratorIO`` has been split into
|
||||||
|
``TextIteratorIO`` and ``BytesIteratorIO``. ``StringIteratorIO`` is
|
||||||
|
deprecated but kept for backward compatibility as an alias for
|
||||||
|
``TextIteratorIO``.
|
||||||
|
|
||||||
|
- Bugfix #92 - Run demo using ``make demo runserver`` (was broken).
|
||||||
|
|
||||||
|
- Feature #99 - Tox runs project's tests with Python 2.7, 3.3 and 3.4, and with
|
||||||
|
Django 1.5 to 1.8.
|
||||||
|
|
||||||
|
- Refactoring #98 - Refreshed development environment: packaging, Tox and
|
||||||
|
Sphinx.
|
||||||
|
|
||||||
|
|
||||||
|
1.6 (2014-03-03)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Python 3 support, development environment refactoring.
|
||||||
|
|
||||||
|
- Feature #46: introduced support for Python>=3.3.
|
||||||
|
|
||||||
|
- Feature #80: added documentation about "how to serve a file inline VS how to
|
||||||
|
serve a file as attachment". Improved documentation of views' base options
|
||||||
|
inherited from ``DownloadMixin``.
|
||||||
|
|
||||||
|
- Feature #74: the Makefile in project's repository no longer creates a
|
||||||
|
virtualenv. Developers setup the environment as they like, i.e. using
|
||||||
|
virtualenv, virtualenvwrapper or whatever. Tests are run with tox.
|
||||||
|
|
||||||
|
|
||||||
|
1.5 (2013-11-29)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
X-Sendfile support and helpers to migrate for `django-sendfile`.
|
||||||
|
|
||||||
|
- Feature #2 - Introduced support of Lighttpd's x-Sendfile.
|
||||||
|
|
||||||
|
- Feature #36 - Introduced support of Apache's mod_xsendfile.
|
||||||
|
|
||||||
|
- Feature #41 - ``django_downloadview.sendfile`` is a port of
|
||||||
|
`django-sendfile`'s ``sendfile`` function. The documentation contains notes
|
||||||
|
about migrating from `django-sendfile` to `django-downloadview`.
|
||||||
|
|
||||||
|
|
||||||
|
1.4 (2013-11-24)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Bugfixes and documentation features.
|
||||||
|
|
||||||
|
- Bugfix #43 - ``ObjectDownloadView`` returns HTTP 404 if model instance's file
|
||||||
|
field is empty (was HTTP 500).
|
||||||
|
|
||||||
|
- Bugfix #7 - Special characters in file names (``Content-Disposition`` header)
|
||||||
|
are urlencoded. An US-ASCII fallback is also provided.
|
||||||
|
|
||||||
|
- Feature #10 - `django-downloadview` is registered on djangopackages.com.
|
||||||
|
|
||||||
|
- Feature #65 - INSTALL documentation shows "known good set" (KGS) of versions,
|
||||||
|
i.e. versions that have been used in test environment.
|
||||||
|
|
||||||
|
|
||||||
|
1.3 (2013-11-08)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Big refactoring around middleware configuration, API readability and
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
- Bugfix #57 - ``PathDownloadView`` opens files in binary mode (was text mode).
|
||||||
|
|
||||||
|
- Bugfix #48 - Fixed ``basename`` assertion in ``assert_download_response``:
|
||||||
|
checks ``Content-Disposition`` header.
|
||||||
|
|
||||||
|
- Bugfix #49 - Fixed ``content`` assertion in ``assert_download_response``:
|
||||||
|
checks only response's ``streaming_content`` attribute.
|
||||||
|
|
||||||
|
- Bugfix #60 - ``VirtualFile.__iter__`` uses ``force_bytes()`` to support both
|
||||||
|
"text-mode" and "binary-mode" content.
|
||||||
|
See https://code.djangoproject.com/ticket/21321
|
||||||
|
|
||||||
|
- Feature #50 - Introduced ``django_downloadview.DownloadDispatcherMiddleware``
|
||||||
|
that iterates over a list of configurable download middlewares. Allows to
|
||||||
|
plug several download middlewares with different configurations.
|
||||||
|
|
||||||
|
This middleware is mostly dedicated to internal usage. It is used by
|
||||||
|
``SmartDownloadMiddleware`` described below.
|
||||||
|
|
||||||
|
- Feature #42 - Documentation shows how to stream generated content (yield).
|
||||||
|
Introduced ``django_downloadview.StringIteratorIO``.
|
||||||
|
|
||||||
|
- Refactoring #51 - Dropped support of Python 2.6
|
||||||
|
|
||||||
|
- Refactoring #25 - Introduced ``django_downloadview.SmartDownloadMiddleware``
|
||||||
|
which allows to setup multiple optimization rules for one backend.
|
||||||
|
|
||||||
|
Deprecates the following settings related to previous single-and-global
|
||||||
|
middleware:
|
||||||
|
|
||||||
|
* ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT``
|
||||||
|
* ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL``
|
||||||
|
* ``NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``
|
||||||
|
* ``NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``
|
||||||
|
* ``NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``
|
||||||
|
|
||||||
|
- Refactoring #52 - ObjectDownloadView now inherits from SingleObjectMixin and
|
||||||
|
BaseDownloadView (was DownloadMixin and BaseDetailView).
|
||||||
|
Simplified DownloadMixin.render_to_response() signature.
|
||||||
|
|
||||||
|
- Refactoring #40 - Documentation includes examples from demo project.
|
||||||
|
|
||||||
|
- Refactoring #39 - Documentation focuses on usage, rather than API. Improved
|
||||||
|
narrative documentation.
|
||||||
|
|
||||||
|
- Refactoring #53 - Added base classes in ``django_downloadview.middlewares``,
|
||||||
|
such as ``ProxiedDownloadMiddleware``.
|
||||||
|
|
||||||
|
- Refactoring #54 - Expose most Python API directly in `django_downloadview`
|
||||||
|
package. Simplifies ``import`` statements in client applications.
|
||||||
|
Splitted nginx module in a package.
|
||||||
|
|
||||||
|
- Added unit tests, improved code coverage.
|
||||||
|
|
||||||
|
|
||||||
1.2 (2013-05-28)
|
1.2 (2013-05-28)
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Bugfixes and documentation improvements.
|
Bugfixes and documentation improvements.
|
||||||
|
|
||||||
- Bug #26 - Prevented computation of virtual file's size, unless the file
|
- Bugfix #26 - Prevented computation of virtual file's size, unless the file
|
||||||
wrapper implements was_modified_since() method.
|
wrapper implements was_modified_since() method.
|
||||||
- Bug #34 - Improved support of files that do not implement modification time.
|
|
||||||
- Bug #35 - Fixed README conversion from reStructuredText to HTML (PyPI).
|
- Bugfix #34 - Improved support of files that do not implement modification
|
||||||
|
time.
|
||||||
|
|
||||||
|
- Bugfix #35 - Fixed README conversion from reStructuredText to HTML (PyPI).
|
||||||
|
|
||||||
|
|
||||||
1.1 (2013-04-11)
|
1.1 (2013-04-11)
|
||||||
----------------
|
----------------
|
||||||
|
|
@ -39,3 +260,10 @@ Contains **backward incompatible changes.**
|
||||||
- Introduced optimizations for Nginx X-Accel: a middleware and a decorator
|
- Introduced optimizations for Nginx X-Accel: a middleware and a decorator
|
||||||
- Introduced generic views: DownloadView and ObjectDownloadView
|
- Introduced generic views: DownloadView and ObjectDownloadView
|
||||||
- Initialized project
|
- Initialized project
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Notes & references
|
||||||
|
|
||||||
|
.. target-notes::
|
||||||
|
|
||||||
|
.. _`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/
|
||||||
102
CONTRIBUTING.rst
Normal file
102
CONTRIBUTING.rst
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
############
|
||||||
|
Contributing
|
||||||
|
############
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://jazzband.co/static/img/jazzband.svg
|
||||||
|
:target: https://jazzband.co/
|
||||||
|
:alt: Jazzband
|
||||||
|
|
||||||
|
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_ and follow the `guidelines <https://jazzband.co/about/guidelines>`_.
|
||||||
|
|
||||||
|
|
||||||
|
This document provides guidelines for people who want to contribute to
|
||||||
|
``django-downloadview``.
|
||||||
|
|
||||||
|
|
||||||
|
**************
|
||||||
|
Create tickets
|
||||||
|
**************
|
||||||
|
|
||||||
|
Please use the `bugtracker`_ **before** starting some work:
|
||||||
|
|
||||||
|
* check if the bug or feature request has already been filed. It may have been
|
||||||
|
answered too!
|
||||||
|
|
||||||
|
* else create a new ticket.
|
||||||
|
|
||||||
|
* if you plan to contribute, tell us, so that we are given an opportunity to
|
||||||
|
give feedback as soon as possible.
|
||||||
|
|
||||||
|
* Then, in your commit messages, reference the ticket with some
|
||||||
|
``refs #TICKET-ID`` syntax.
|
||||||
|
|
||||||
|
|
||||||
|
******************
|
||||||
|
Use topic branches
|
||||||
|
******************
|
||||||
|
|
||||||
|
* Work in branches.
|
||||||
|
|
||||||
|
* Prefix your branch with the ticket ID corresponding to the issue. As an
|
||||||
|
example, if you are working on ticket #23 which is about contribute
|
||||||
|
documentation, name your branch like ``23-contribute-doc``.
|
||||||
|
|
||||||
|
* If you work in a development branch and want to refresh it with changes from
|
||||||
|
master, please `rebase`_ or `merge-based rebase`_, i.e. do not merge master.
|
||||||
|
|
||||||
|
|
||||||
|
***********
|
||||||
|
Fork, clone
|
||||||
|
***********
|
||||||
|
|
||||||
|
Clone ``django-downloadview`` repository (adapt to use your own fork):
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
git clone git@github.com:jazzband/django-downloadview.git
|
||||||
|
cd django-downloadview/
|
||||||
|
|
||||||
|
|
||||||
|
*************
|
||||||
|
Usual actions
|
||||||
|
*************
|
||||||
|
|
||||||
|
The ``Makefile`` is the reference card for usual actions in development
|
||||||
|
environment:
|
||||||
|
|
||||||
|
* Install development toolkit with `pip`_: ``make develop``.
|
||||||
|
|
||||||
|
* Run tests with `tox`_: ``make test``.
|
||||||
|
|
||||||
|
* Build documentation: ``make documentation``. It builds `Sphinx`_
|
||||||
|
documentation in ``var/docs/html/index.html``.
|
||||||
|
|
||||||
|
* Release project with `zest.releaser`_: ``make release``.
|
||||||
|
|
||||||
|
* Cleanup local repository: ``make clean``, ``make distclean`` and
|
||||||
|
``make maintainer-clean``.
|
||||||
|
|
||||||
|
See also ``make help``.
|
||||||
|
|
||||||
|
|
||||||
|
*********************
|
||||||
|
Demo project included
|
||||||
|
*********************
|
||||||
|
|
||||||
|
The ``demo`` included in project's repository is part of the tests and
|
||||||
|
documentation. Maintain it along with code and documentation.
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Notes & references
|
||||||
|
|
||||||
|
.. target-notes::
|
||||||
|
|
||||||
|
.. _`bugtracker`:
|
||||||
|
https://github.com/jazzband/django-downloadview/issues
|
||||||
|
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
|
||||||
|
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html
|
||||||
|
.. _`pip`: https://pypi.python.org/pypi/pip/
|
||||||
|
.. _`tox`: https://tox.readthedocs.io/
|
||||||
|
.. _`Sphinx`: https://pypi.python.org/pypi/Sphinx/
|
||||||
|
.. _`zest.releaser`: https://pypi.python.org/pypi/zest.releaser/
|
||||||
106
INSTALL
106
INSTALL
|
|
@ -1,30 +1,88 @@
|
||||||
############
|
#######
|
||||||
Installation
|
Install
|
||||||
############
|
#######
|
||||||
|
|
||||||
This project is open-source, published under BSD license.
|
|
||||||
See :doc:`/about/license` for details.
|
|
||||||
|
|
||||||
If you want to install a development environment, you should go to :doc:`/dev`
|
|
||||||
documentation.
|
|
||||||
|
|
||||||
Install the package with your favorite Python installer. As an example, with
|
|
||||||
pip:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip install django-downloadview
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Since version 1.1, django-downloadview requires Django>=1.5, which provides
|
If you want to install a development environment, please see
|
||||||
StreamingHttpResponse.
|
:doc:`/contributing`.
|
||||||
|
|
||||||
There is no need to register this application in your Django's
|
|
||||||
``INSTALLED_APPS`` setting.
|
|
||||||
|
|
||||||
Next, you'll have to setup some download view(s). See :doc:`demo project
|
************
|
||||||
<demo>` for examples, and :doc:`API documentation <api/django_downloadview>`.
|
Requirements
|
||||||
|
************
|
||||||
|
|
||||||
Optionally, you may setup additional :doc:`server optimizations
|
`django-downloadview` has been tested with `Python`_ 3.7, 3.8, 3.9 and 3.10.
|
||||||
<optimizations/index>`.
|
Other versions may work, but they are not part of the test suite at the moment.
|
||||||
|
|
||||||
|
Installing `django-downloadview` will automatically trigger the installation of
|
||||||
|
the following requirements:
|
||||||
|
|
||||||
|
.. literalinclude:: /../setup.py
|
||||||
|
:language: python
|
||||||
|
:start-after: BEGIN requirements
|
||||||
|
:end-before: END requirements
|
||||||
|
|
||||||
|
|
||||||
|
************
|
||||||
|
As a library
|
||||||
|
************
|
||||||
|
|
||||||
|
In most cases, you will use `django-downloadview` as a dependency of another
|
||||||
|
project. In such a case, you should add `django-downloadview` in your main
|
||||||
|
project's requirements. Typically in :file:`setup.py`:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
install_requires=[
|
||||||
|
'django-downloadview',
|
||||||
|
#...
|
||||||
|
]
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
|
Then when you install your main project with your favorite package manager
|
||||||
|
(like `pip`_), `django-downloadview` and its recursive dependencies will
|
||||||
|
automatically be installed.
|
||||||
|
|
||||||
|
|
||||||
|
**********
|
||||||
|
Standalone
|
||||||
|
**********
|
||||||
|
|
||||||
|
You can install `django-downloadview` with your favorite Python package
|
||||||
|
manager. As an example with `pip`_:
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
pip install django-downloadview
|
||||||
|
|
||||||
|
|
||||||
|
*****
|
||||||
|
Check
|
||||||
|
*****
|
||||||
|
|
||||||
|
Check `django-downloadview` has been installed:
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
python -c "import django_downloadview;print(django_downloadview.__version__)"
|
||||||
|
|
||||||
|
You should get installed `django-downloadview`'s version.
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Notes & references
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
* :doc:`/settings`
|
||||||
|
* :doc:`/about/changelog`
|
||||||
|
* :doc:`/about/license`
|
||||||
|
|
||||||
|
.. target-notes::
|
||||||
|
|
||||||
|
.. _`Python`: https://www.python.org/
|
||||||
|
.. _`pip`: https://pip.pypa.io/
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -2,7 +2,7 @@
|
||||||
License
|
License
|
||||||
#######
|
#######
|
||||||
|
|
||||||
Copyright (c) 2012, Benoît Bryon.
|
Copyright (c) 2012-2014, Benoît Bryon.
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
||||||
10
MANIFEST.in
10
MANIFEST.in
|
|
@ -1,4 +1,8 @@
|
||||||
recursive-include django_downloadview *
|
recursive-include django_downloadview *
|
||||||
global-exclude *.pyc .*.swp
|
global-exclude *.pyc
|
||||||
include *.txt
|
include AUTHORS
|
||||||
include AUTHORS CHANGELOG INSTALL LICENSE README VERSION
|
include CHANGELOG
|
||||||
|
include CONTRIBUTING.rst
|
||||||
|
include INSTALL
|
||||||
|
include LICENSE
|
||||||
|
include README.rst
|
||||||
|
|
|
||||||
143
Makefile
143
Makefile
|
|
@ -1,90 +1,101 @@
|
||||||
# Makefile for development.
|
# Reference card for usual actions in development environment.
|
||||||
# See INSTALL and docs/dev.txt for details.
|
#
|
||||||
SHELL = /bin/bash
|
# For standard installation of django-downloadview as a library, see INSTALL.
|
||||||
ROOT_DIR = $(shell pwd)
|
#
|
||||||
BIN_DIR = $(ROOT_DIR)/bin
|
# For details about django-downloadview's development environment, see
|
||||||
DATA_DIR = $(ROOT_DIR)/var
|
# CONTRIBUTING.rst.
|
||||||
WGET = wget
|
#
|
||||||
PYTHON = $(shell which python)
|
PIP = pip
|
||||||
PROJECT = $(shell $(PYTHON) -c "import setup; print setup.NAME")
|
TOX = tox
|
||||||
BUILDOUT_CFG = $(ROOT_DIR)/etc/buildout.cfg
|
BLACK = black
|
||||||
BUILDOUT_DIR = $(ROOT_DIR)/lib/buildout
|
ISORT = isort
|
||||||
BUILDOUT_VERSION = 1.7.0
|
|
||||||
BUILDOUT_BOOTSTRAP_URL = https://raw.github.com/buildout/buildout/$(BUILDOUT_VERSION)/bootstrap/bootstrap.py
|
#: help - Display callable targets.
|
||||||
BUILDOUT_BOOTSTRAP = $(BUILDOUT_DIR)/bootstrap.py
|
.PHONY: help
|
||||||
BUILDOUT_BOOTSTRAP_ARGS = -c $(BUILDOUT_CFG) --version=$(BUILDOUT_VERSION) --distribute buildout:directory=$(ROOT_DIR)
|
help:
|
||||||
BUILDOUT = $(BIN_DIR)/buildout
|
@echo "Reference card for usual actions in development environment."
|
||||||
BUILDOUT_ARGS = -N -c $(BUILDOUT_CFG) buildout:directory=$(ROOT_DIR)
|
@echo "Here are available targets:"
|
||||||
NOSE = $(BIN_DIR)/nosetests
|
@egrep -o "^#: (.+)" [Mm]akefile | sed 's/#: /* /'
|
||||||
|
|
||||||
|
|
||||||
configure:
|
#: develop - Install minimal development utilities.
|
||||||
# Configuration is stored in etc/ folder. Not generated yet.
|
.PHONY: develop
|
||||||
|
develop:
|
||||||
|
$(PIP) install -e .
|
||||||
develop: buildout
|
|
||||||
|
|
||||||
|
|
||||||
buildout:
|
|
||||||
if [ ! -d $(BUILDOUT_DIR) ]; then mkdir -p $(BUILDOUT_DIR); fi
|
|
||||||
if [ ! -f $(BUILDOUT_BOOTSTRAP) ]; then wget -O $(BUILDOUT_BOOTSTRAP) $(BUILDOUT_BOOTSTRAP_URL); fi
|
|
||||||
if [ ! -x $(BUILDOUT) ]; then $(PYTHON) $(BUILDOUT_BOOTSTRAP) $(BUILDOUT_BOOTSTRAP_ARGS); fi
|
|
||||||
$(BUILDOUT) $(BUILDOUT_ARGS)
|
|
||||||
|
|
||||||
|
|
||||||
|
#: clean - Basic cleanup, mostly temporary files.
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
find $(ROOT_DIR)/ -name "*.pyc" -delete
|
find . -name "*.pyc" -delete
|
||||||
find $(ROOT_DIR)/ -name ".noseids" -delete
|
find . -name '*.pyo' -delete
|
||||||
|
find . -name "__pycache__" -delete
|
||||||
|
|
||||||
|
|
||||||
|
#: distclean - Remove local builds, such as *.egg-info.
|
||||||
|
.PHONY: distclean
|
||||||
distclean: clean
|
distclean: clean
|
||||||
rm -rf $(ROOT_DIR)/*.egg-info
|
rm -rf *.egg
|
||||||
rm -rf $(ROOT_DIR)/demo/*.egg-info
|
rm -rf *.egg-info
|
||||||
|
rm -rf demo/*.egg-info
|
||||||
|
|
||||||
|
|
||||||
|
#: maintainer-clean - Remove almost everything that can be re-generated.
|
||||||
|
.PHONY: maintainer-clean
|
||||||
maintainer-clean: distclean
|
maintainer-clean: distclean
|
||||||
rm -rf $(BIN_DIR)/
|
rm -rf build/
|
||||||
rm -rf $(ROOT_DIR)/lib/
|
rm -rf dist/
|
||||||
|
rm -rf .tox/
|
||||||
|
|
||||||
|
|
||||||
test: test-app test-demo test-documentation
|
#: test - Run test suites.
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
mkdir -p var
|
||||||
|
$(PIP) install -e .[test]
|
||||||
|
$(TOX)
|
||||||
|
|
||||||
|
|
||||||
test-app:
|
#: documentation - Build documentation (Sphinx, README, ...)
|
||||||
$(NOSE) -c $(ROOT_DIR)/etc/nose.cfg --with-coverage --cover-package=django_downloadview django_downloadview tests
|
.PHONY: documentation
|
||||||
mv $(ROOT_DIR)/.coverage $(ROOT_DIR)/var/test/app.coverage
|
documentation: sphinx readme
|
||||||
|
|
||||||
|
|
||||||
test-demo:
|
|
||||||
$(BIN_DIR)/demo test demo
|
|
||||||
mv $(ROOT_DIR)/.coverage $(ROOT_DIR)/var/test/demo.coverage
|
|
||||||
|
|
||||||
|
|
||||||
test-documentation:
|
|
||||||
$(NOSE) -c $(ROOT_DIR)/etc/nose.cfg sphinxcontrib.testbuild.tests
|
|
||||||
|
|
||||||
|
|
||||||
apidoc:
|
|
||||||
cp docs/api/index.txt docs/api-backup.txt
|
|
||||||
rm -rf docs/api/*
|
|
||||||
mv docs/api-backup.txt docs/api/index.txt
|
|
||||||
$(BIN_DIR)/sphinx-apidoc --suffix txt --output-dir $(ROOT_DIR)/docs/api django_downloadview
|
|
||||||
|
|
||||||
|
|
||||||
|
#: sphinx - Build Sphinx documentation (docs).
|
||||||
|
.PHONY: sphinx
|
||||||
sphinx:
|
sphinx:
|
||||||
make --directory=docs clean html doctest
|
$(TOX) -e sphinx
|
||||||
|
|
||||||
|
|
||||||
documentation: apidoc sphinx
|
#: readme - Build standalone documentation files (README, CONTRIBUTING...).
|
||||||
|
.PHONY: readme
|
||||||
|
readme:
|
||||||
|
$(TOX) -e readme
|
||||||
|
|
||||||
|
|
||||||
demo: develop
|
#: demo - Setup demo project.
|
||||||
mkdir -p var/media/document
|
.PHONY: demo
|
||||||
$(BIN_DIR)/demo syncdb --noinput
|
demo:
|
||||||
cp $(ROOT_DIR)/demo/demoproject/download/fixtures/hello-world.txt var/media/document/
|
pip install -e .
|
||||||
$(BIN_DIR)/demo loaddata $(ROOT_DIR)/demo/demoproject/download/fixtures/demo.json
|
pip install -e demo
|
||||||
$(BIN_DIR)/demo runserver
|
demo migrate --noinput
|
||||||
|
# Install fixtures.
|
||||||
|
mkdir -p var/media/object var/media/object-other/ var/media/nginx
|
||||||
|
cp -r demo/demoproject/fixtures/* var/media/object/
|
||||||
|
cp -r demo/demoproject/fixtures/* var/media/object-other/
|
||||||
|
cp -r demo/demoproject/fixtures/* var/media/nginx/
|
||||||
|
demo loaddata demo.json
|
||||||
|
|
||||||
|
|
||||||
release:
|
#: runserver - Run demo server.
|
||||||
$(BIN_DIR)/fullrelease
|
.PHONY: runserver
|
||||||
|
runserver: demo
|
||||||
|
demo runserver
|
||||||
|
|
||||||
|
.PHONY: black
|
||||||
|
black:
|
||||||
|
$(BLACK) demo tests django_downloadview
|
||||||
|
|
||||||
|
.PHONY: isort
|
||||||
|
isort:
|
||||||
|
$(ISORT) --recursive django_downloadview tests demo
|
||||||
|
|
|
||||||
61
README
61
README
|
|
@ -1,61 +0,0 @@
|
||||||
###################
|
|
||||||
django-downloadview
|
|
||||||
###################
|
|
||||||
|
|
||||||
Django-DownloadView provides generic views to make Django serve files.
|
|
||||||
|
|
||||||
It can serve files from models, storages, local filesystem, arbitrary URL...
|
|
||||||
and even generated files.
|
|
||||||
|
|
||||||
For increased performances, it can delegate the actual streaming to a reverse
|
|
||||||
proxy, via mechanisms such as Nginx's X-Accel.
|
|
||||||
|
|
||||||
|
|
||||||
*******
|
|
||||||
Example
|
|
||||||
*******
|
|
||||||
|
|
||||||
In some ``urls.py``, serve files managed in a model:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.conf.urls import url, url_patterns
|
|
||||||
from django_downloadview import ObjectDownloadView
|
|
||||||
from demoproject.download.models import Document # A model with a FileField
|
|
||||||
|
|
||||||
# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
|
|
||||||
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
|
||||||
|
|
||||||
url_patterns = ('',
|
|
||||||
url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', download, name='download'),
|
|
||||||
)
|
|
||||||
|
|
||||||
More examples in the "demo" documentation!
|
|
||||||
|
|
||||||
|
|
||||||
*****
|
|
||||||
Views
|
|
||||||
*****
|
|
||||||
|
|
||||||
Several views are provided to cover frequent use cases:
|
|
||||||
|
|
||||||
* ``ObjectDownloadView`` when you have a model with a file field.
|
|
||||||
* ``StorageDownloadView`` when you manage files in a storage.
|
|
||||||
* ``PathDownloadView`` when you have an absolute filename on local filesystem.
|
|
||||||
* ``HTTPDownloadView`` when you have an URL (the resource is proxied).
|
|
||||||
* ``VirtualDownloadView`` when you the file is generated on the fly.
|
|
||||||
|
|
||||||
See "views" documentation for details.
|
|
||||||
|
|
||||||
See also "optimizations" documentation to get increased performances.
|
|
||||||
|
|
||||||
|
|
||||||
**********
|
|
||||||
Ressources
|
|
||||||
**********
|
|
||||||
|
|
||||||
* Documentation: http://django-downloadview.readthedocs.org
|
|
||||||
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
|
||||||
* Code repository: https://github.com/benoitbryon/django-downloadview
|
|
||||||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
|
||||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
|
||||||
73
README.rst
Normal file
73
README.rst
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
###################
|
||||||
|
django-downloadview
|
||||||
|
###################
|
||||||
|
|
||||||
|
.. image:: https://jazzband.co/static/img/badge.svg
|
||||||
|
:target: https://jazzband.co/
|
||||||
|
:alt: Jazzband
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/djversions/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/dm/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://github.com/jazzband/django-downloadview/workflows/Test/badge.svg
|
||||||
|
:target: https://github.com/jazzband/django-downloadview/actions
|
||||||
|
:alt: GitHub Actions
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/gh/jazzband/django-downloadview/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/jazzband/django-downloadview
|
||||||
|
:alt: Coverage
|
||||||
|
|
||||||
|
``django-downloadview`` makes it easy to serve files with `Django`_:
|
||||||
|
|
||||||
|
* you manage files with Django (permissions, filters, generation, ...);
|
||||||
|
|
||||||
|
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||||
|
storage, memory...);
|
||||||
|
|
||||||
|
* ``django-downloadview`` helps you stream the files with very little code;
|
||||||
|
|
||||||
|
* ``django-downloadview`` helps you improve performances with reverse proxies,
|
||||||
|
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.
|
||||||
|
|
||||||
|
|
||||||
|
*******
|
||||||
|
Example
|
||||||
|
*******
|
||||||
|
|
||||||
|
Let's serve a file stored in a file field of some model:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.conf.urls import url, url_patterns
|
||||||
|
from django_downloadview import ObjectDownloadView
|
||||||
|
from demoproject.download.models import Document # A model with a FileField
|
||||||
|
|
||||||
|
# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
|
||||||
|
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
||||||
|
|
||||||
|
url_patterns = ('',
|
||||||
|
url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', download, name='download'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
*********
|
||||||
|
Resources
|
||||||
|
*********
|
||||||
|
|
||||||
|
* Documentation: https://django-downloadview.readthedocs.io
|
||||||
|
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
||||||
|
* Code repository: https://github.com/jazzband/django-downloadview
|
||||||
|
* Bugtracker: https://github.com/jazzband/django-downloadview/issues
|
||||||
|
* Continuous integration: https://github.com/jazzband/django-downloadview/actions
|
||||||
|
* Roadmap: https://github.com/jazzband/django-downloadview/milestones
|
||||||
|
|
||||||
|
.. _`Django`: https://djangoproject.com
|
||||||
1
VERSION
1
VERSION
|
|
@ -1 +0,0 @@
|
||||||
1.2
|
|
||||||
98
demo/README
98
demo/README
|
|
@ -1,98 +0,0 @@
|
||||||
############
|
|
||||||
Demo project
|
|
||||||
############
|
|
||||||
|
|
||||||
The :file:`demo/` folder holds a demo project to illustrate django-downloadview
|
|
||||||
usage.
|
|
||||||
|
|
||||||
|
|
||||||
***********************
|
|
||||||
Browse demo code online
|
|
||||||
***********************
|
|
||||||
|
|
||||||
See `demo folder in project's repository`_.
|
|
||||||
|
|
||||||
|
|
||||||
***************
|
|
||||||
Deploy the demo
|
|
||||||
***************
|
|
||||||
|
|
||||||
System requirements:
|
|
||||||
|
|
||||||
* `Python`_ version 2.6 or 2.7, available as ``python`` command.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
You may use `Virtualenv`_ to make sure the active ``python`` is the right
|
|
||||||
one.
|
|
||||||
|
|
||||||
* ``make`` and ``wget`` to use the provided :file:`Makefile`.
|
|
||||||
|
|
||||||
Execute:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
git clone git@github.com:benoitbryon/django-downloadview.git
|
|
||||||
cd django-downloadview/
|
|
||||||
make demo
|
|
||||||
|
|
||||||
It installs and runs the demo server on localhost, port 8000. So have a look
|
|
||||||
at http://localhost:8000/
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you cannot execute the Makefile, read it and adapt the few commands it
|
|
||||||
contains to your needs.
|
|
||||||
|
|
||||||
Browse and use :file:`demo/demoproject/` as a sandbox.
|
|
||||||
|
|
||||||
|
|
||||||
*********************************
|
|
||||||
Base example provided in the demo
|
|
||||||
*********************************
|
|
||||||
|
|
||||||
In the "demoproject" project, there is an application called "download".
|
|
||||||
|
|
||||||
:file:`demo/demoproject/settings.py`:
|
|
||||||
|
|
||||||
.. literalinclude:: ../demo/demoproject/settings.py
|
|
||||||
:language: python
|
|
||||||
:lines: 33-49
|
|
||||||
:emphasize-lines: 44
|
|
||||||
|
|
||||||
This application holds a ``Document`` model.
|
|
||||||
|
|
||||||
:file:`demo/demoproject/download/models.py`:
|
|
||||||
|
|
||||||
.. literalinclude:: ../demo/demoproject/download/models.py
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The ``storage`` is the default one, i.e. it uses ``settings.MEDIA_ROOT``.
|
|
||||||
Combined to this ``upload_to`` configuration, files for ``Document`` model
|
|
||||||
live in :file:`var/media/document/` folder, relative to your
|
|
||||||
django-downloadview clone root.
|
|
||||||
|
|
||||||
There is a download view named "download_document" for this model:
|
|
||||||
|
|
||||||
:file:`demo/demoproject/download/urls.py`:
|
|
||||||
|
|
||||||
.. literalinclude:: ../demo/demoproject/download/urls.py
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
As is, Django is to serve the files, i.e. load chunks into memory and stream
|
|
||||||
them.
|
|
||||||
|
|
||||||
|
|
||||||
**********
|
|
||||||
References
|
|
||||||
**********
|
|
||||||
|
|
||||||
.. target-notes::
|
|
||||||
|
|
||||||
.. _`demo folder in project's repository`:
|
|
||||||
https://github.com/benoitbryon/django-downloadview/tree/master/demo/demoproject/
|
|
||||||
|
|
||||||
.. _`Python`: http://python.org
|
|
||||||
.. _`Virtualenv`: http://virtualenv.org
|
|
||||||
72
demo/README.rst
Normal file
72
demo/README.rst
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
############
|
||||||
|
Demo project
|
||||||
|
############
|
||||||
|
|
||||||
|
`Demo folder in project's repository`_ contains a Django project to illustrate
|
||||||
|
``django-downloadview`` usage.
|
||||||
|
|
||||||
|
|
||||||
|
*****************************************
|
||||||
|
Documentation includes code from the demo
|
||||||
|
*****************************************
|
||||||
|
|
||||||
|
Almost every example in the documentation comes from the demo:
|
||||||
|
|
||||||
|
* discover examples in the documentation;
|
||||||
|
* browse related code and tests in demo project.
|
||||||
|
|
||||||
|
Examples in documentation are tested via demo project!
|
||||||
|
|
||||||
|
|
||||||
|
***********************
|
||||||
|
Browse demo code online
|
||||||
|
***********************
|
||||||
|
|
||||||
|
See `demo folder in project's repository`_.
|
||||||
|
|
||||||
|
|
||||||
|
***************
|
||||||
|
Deploy the demo
|
||||||
|
***************
|
||||||
|
|
||||||
|
System requirements:
|
||||||
|
|
||||||
|
* `Python`_ version 3.7+, available as ``python`` command.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You may use `Virtualenv`_ to make sure the active ``python`` is the right
|
||||||
|
one.
|
||||||
|
|
||||||
|
* ``make`` and ``wget`` to use the provided :file:`Makefile`.
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
git clone git@github.com:jazzband/django-downloadview.git
|
||||||
|
cd django-downloadview/
|
||||||
|
make runserver
|
||||||
|
|
||||||
|
It installs and runs the demo server on localhost, port 8000. So have a look
|
||||||
|
at ``http://localhost:8000/``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you cannot execute the Makefile, read it and adapt the few commands it
|
||||||
|
contains to your needs.
|
||||||
|
|
||||||
|
Browse and use :file:`demo/demoproject/` as a sandbox.
|
||||||
|
|
||||||
|
|
||||||
|
**********
|
||||||
|
References
|
||||||
|
**********
|
||||||
|
|
||||||
|
.. target-notes::
|
||||||
|
|
||||||
|
.. _`demo folder in project's repository`:
|
||||||
|
https://github.com/jazzband/django-downloadview/tree/master/demo/demoproject/
|
||||||
|
|
||||||
|
.. _`Python`: http://python.org
|
||||||
|
.. _`Virtualenv`: http://virtualenv.org
|
||||||
1
demo/demoproject/apache/__init__.py
Normal file
1
demo/demoproject/apache/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Apache optimizations."""
|
||||||
1
demo/demoproject/apache/models.py
Normal file
1
demo/demoproject/apache/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
61
demo/demoproject/apache/tests.py
Normal file
61
demo/demoproject/apache/tests.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview.apache import assert_x_sendfile
|
||||||
|
|
||||||
|
from demoproject.apache.views import storage, storage_dir
|
||||||
|
|
||||||
|
|
||||||
|
def setup_file():
|
||||||
|
if not os.path.exists(storage_dir):
|
||||||
|
os.makedirs(storage_dir)
|
||||||
|
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizedByMiddlewareTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'apache:optimized_by_middleware' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("apache:optimized_by_middleware")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/apache-optimized-by-middleware/hello-world.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'apache:optimized_by_decorator' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("apache:optimized_by_decorator")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/apache-optimized-by-decorator/hello-world.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'apache:modified_headers' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("apache:modified_headers")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/apache-modified-headers/hello-world.txt",
|
||||||
|
)
|
||||||
|
self.assertEqual(response["X-Test"], "header")
|
||||||
24
demo/demoproject/apache/urls.py
Normal file
24
demo/demoproject/apache/urls.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""URL mapping."""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from demoproject.apache import views
|
||||||
|
|
||||||
|
app_name = "apache"
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"optimized-by-middleware/",
|
||||||
|
views.optimized_by_middleware,
|
||||||
|
name="optimized_by_middleware",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"optimized-by-decorator/",
|
||||||
|
views.optimized_by_decorator,
|
||||||
|
name="optimized_by_decorator",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"modified_headers/",
|
||||||
|
views.modified_headers,
|
||||||
|
name="modified_headers",
|
||||||
|
),
|
||||||
|
]
|
||||||
38
demo/demoproject/apache/views.py
Normal file
38
demo/demoproject/apache/views.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
from django_downloadview import StorageDownloadView
|
||||||
|
from django_downloadview.apache import x_sendfile
|
||||||
|
|
||||||
|
storage_dir = os.path.join(settings.MEDIA_ROOT, "apache")
|
||||||
|
storage = FileSystemStorage(
|
||||||
|
location=storage_dir, base_url="".join([settings.MEDIA_URL, "apache/"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
optimized_by_middleware = StorageDownloadView.as_view(
|
||||||
|
storage=storage, path="hello-world.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
optimized_by_decorator = x_sendfile(
|
||||||
|
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/apache-optimized-by-decorator/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _modified_headers(request):
|
||||||
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
|
response = view(request)
|
||||||
|
response["X-Test"] = "header"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
modified_headers = x_sendfile(
|
||||||
|
_modified_headers,
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/apache-modified-headers/",
|
||||||
|
)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"pk": 1,
|
|
||||||
"model": "download.document",
|
|
||||||
"fields": {
|
|
||||||
"slug": "hello-world",
|
|
||||||
"file": "document/hello-world.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Document(models.Model):
|
|
||||||
"""A sample model with a FileField."""
|
|
||||||
slug = models.SlugField(verbose_name='slug')
|
|
||||||
file = models.FileField(verbose_name='file', upload_to='document')
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
# coding=utf8
|
|
||||||
"""Test suite for demoproject.download."""
|
|
||||||
from os import listdir
|
|
||||||
from os.path import abspath, dirname, join
|
|
||||||
|
|
||||||
from django.core.files import File
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from django_downloadview.test import temporary_media_root
|
|
||||||
|
|
||||||
from demoproject.download.models import Document
|
|
||||||
|
|
||||||
|
|
||||||
app_dir = dirname(abspath(__file__))
|
|
||||||
fixtures_dir = join(app_dir, 'fixtures')
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadTestCase(TestCase):
|
|
||||||
"""Base class for download tests."""
|
|
||||||
def setUp(self):
|
|
||||||
"""Common setup."""
|
|
||||||
super(DownloadTestCase, self).setUp()
|
|
||||||
self.files = {}
|
|
||||||
for f in listdir(fixtures_dir):
|
|
||||||
self.files[f] = abspath(join(fixtures_dir, f))
|
|
||||||
|
|
||||||
def assertDownloadHelloWorld(self, response, is_attachment=True):
|
|
||||||
"""Assert response is 'hello-world.txt' download."""
|
|
||||||
self.assertEquals(response.status_code, 200)
|
|
||||||
self.assertEquals(response['Content-Type'],
|
|
||||||
'text/plain; charset=utf-8')
|
|
||||||
self.assertFalse('ContentEncoding' in response)
|
|
||||||
if is_attachment:
|
|
||||||
self.assertEquals(response['Content-Disposition'],
|
|
||||||
'attachment; filename=hello-world.txt')
|
|
||||||
else:
|
|
||||||
self.assertFalse('Content-Disposition' in response)
|
|
||||||
self.assertEqual(open(self.files['hello-world.txt']).read(),
|
|
||||||
''.join(response.streaming_content))
|
|
||||||
|
|
||||||
|
|
||||||
class PathDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test "hello_world" and "hello_world_inline" views."""
|
|
||||||
def test_download_hello_world(self):
|
|
||||||
"""hello_world view returns hello-world.txt as attachement."""
|
|
||||||
download_url = reverse('hello_world')
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
||||||
def test_download_hello_world_inline(self):
|
|
||||||
"""hello_world view returns hello-world.txt as attachement."""
|
|
||||||
download_url = reverse('hello_world_inline')
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response, is_attachment=False)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPathDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test "fixture_from_path" view."""
|
|
||||||
def test_download_hello_world(self):
|
|
||||||
"""fixture_from_path view returns hello-world.txt as attachement."""
|
|
||||||
download_url = reverse('fixture_from_path', args=['hello-world.txt'])
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
||||||
|
|
||||||
class StorageDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test "fixture_from_storage" view."""
|
|
||||||
def test_download_hello_world(self):
|
|
||||||
"""fixture_from_storage view returns hello-world.txt as attachement."""
|
|
||||||
download_url = reverse('fixture_from_storage',
|
|
||||||
args=['hello-world.txt'])
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test generic ObjectDownloadView."""
|
|
||||||
@temporary_media_root()
|
|
||||||
def test_download_hello_world(self):
|
|
||||||
"""'download_document' view returns hello-world.txt as attachement."""
|
|
||||||
slug = 'hello-world'
|
|
||||||
download_url = reverse('document', kwargs={'slug': slug})
|
|
||||||
Document.objects.create(slug=slug,
|
|
||||||
file=File(open(self.files['hello-world.txt'])))
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
||||||
|
|
||||||
class GeneratedDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test "generated_hello_world" view."""
|
|
||||||
def test_download_hello_world(self):
|
|
||||||
"""generated_hello_world view returns hello-world.txt as attachement.
|
|
||||||
|
|
||||||
"""
|
|
||||||
download_url = reverse('generated_hello_world')
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
||||||
|
|
||||||
class ProxiedDownloadViewTestCase(DownloadTestCase):
|
|
||||||
"""Test "http_hello_world" view."""
|
|
||||||
def test_download_readme(self):
|
|
||||||
"""http_hello_world view proxies file from URL."""
|
|
||||||
download_url = reverse('http_hello_world')
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertDownloadHelloWorld(response)
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
# coding=utf8
|
|
||||||
"""URL mapping."""
|
|
||||||
from django.conf.urls import patterns, url
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
|
||||||
'demoproject.download.views',
|
|
||||||
# Model-based downloads.
|
|
||||||
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
|
||||||
'download_document',
|
|
||||||
name='document'),
|
|
||||||
# Storage-based downloads.
|
|
||||||
url(r'^storage/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
|
||||||
'download_fixture_from_storage',
|
|
||||||
name='fixture_from_storage'),
|
|
||||||
# Path-based downloads.
|
|
||||||
url(r'^hello-world\.txt$',
|
|
||||||
'download_hello_world',
|
|
||||||
name='hello_world'),
|
|
||||||
url(r'^hello-world-inline\.txt$',
|
|
||||||
'download_hello_world_inline',
|
|
||||||
name='hello_world_inline'),
|
|
||||||
url(r'^path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
|
||||||
'download_fixture_from_path',
|
|
||||||
name='fixture_from_path'),
|
|
||||||
# URL-based downloads.
|
|
||||||
url(r'^http/readme\.txt$',
|
|
||||||
'download_http_hello_world',
|
|
||||||
name='http_hello_world'),
|
|
||||||
# Generated downloads.
|
|
||||||
url(r'^generated/hello-world\.txt$',
|
|
||||||
'download_generated_hello_world',
|
|
||||||
name='generated_hello_world'),
|
|
||||||
)
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
# coding=utf8
|
|
||||||
"""Demo download views."""
|
|
||||||
from cStringIO import StringIO
|
|
||||||
from os.path import abspath, dirname, join
|
|
||||||
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
|
|
||||||
from django_downloadview.files import VirtualFile
|
|
||||||
from django_downloadview import views
|
|
||||||
from demoproject.download.models import Document
|
|
||||||
|
|
||||||
|
|
||||||
# Some initializations.
|
|
||||||
|
|
||||||
#: Directory containing code of :py:module:`demoproject.download.views`.
|
|
||||||
app_dir = dirname(abspath(__file__))
|
|
||||||
|
|
||||||
#: Directory containing files fixtures.
|
|
||||||
fixtures_dir = join(app_dir, 'fixtures')
|
|
||||||
|
|
||||||
#: Path to a text file that says 'Hello world!'.
|
|
||||||
hello_world_path = join(fixtures_dir, 'hello-world.txt')
|
|
||||||
|
|
||||||
#: Storage for fixtures.
|
|
||||||
fixtures_storage = FileSystemStorage(location=fixtures_dir)
|
|
||||||
|
|
||||||
|
|
||||||
# Here are the views.
|
|
||||||
|
|
||||||
#: Pre-configured download view for :py:class:`Document` model.
|
|
||||||
download_document = views.ObjectDownloadView.as_view(model=Document)
|
|
||||||
|
|
||||||
|
|
||||||
#: Same as download_document, but streamed inline, i.e. not as attachments.
|
|
||||||
download_document_inline = views.ObjectDownloadView.as_view(model=Document,
|
|
||||||
attachment=False)
|
|
||||||
|
|
||||||
|
|
||||||
#: Pre-configured view using a storage.
|
|
||||||
download_fixture_from_storage = views.StorageDownloadView.as_view(
|
|
||||||
storage=fixtures_storage)
|
|
||||||
|
|
||||||
|
|
||||||
#: Direct download of one file, based on an absolute path.
|
|
||||||
#:
|
|
||||||
#: You could use this example as a shortcut, inside other views.
|
|
||||||
download_hello_world = views.PathDownloadView.as_view(path=hello_world_path)
|
|
||||||
|
|
||||||
|
|
||||||
#: Direct download of one file, based on an absolute path, not as attachment.
|
|
||||||
download_hello_world_inline = views.PathDownloadView.as_view(
|
|
||||||
path=hello_world_path,
|
|
||||||
attachment=False)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPathDownloadView(views.PathDownloadView):
|
|
||||||
"""Example of customized PathDownloadView."""
|
|
||||||
def get_path(self):
|
|
||||||
"""Convert relative path (provided in URL) into absolute path.
|
|
||||||
|
|
||||||
Notice that this particularly simple use case is covered by
|
|
||||||
:py:class:`django_downloadview.views.StorageDownloadView`.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
If you are doing such things, make the path secure! Prevent users
|
|
||||||
to download files anywhere in the filesystem.
|
|
||||||
|
|
||||||
"""
|
|
||||||
path = super(CustomPathDownloadView, self).get_path()
|
|
||||||
return join(fixtures_dir, path)
|
|
||||||
|
|
||||||
#: Pre-configured :py:class:`CustomPathDownloadView`.
|
|
||||||
download_fixture_from_path = CustomPathDownloadView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class StringIODownloadView(views.VirtualDownloadView):
|
|
||||||
"""Sample download view using StringIO object."""
|
|
||||||
def get_file(self):
|
|
||||||
"""Return wrapper on StringIO object."""
|
|
||||||
file_obj = StringIO(u"Hello world!\n".encode('utf-8'))
|
|
||||||
return VirtualFile(file_obj, name='hello-world.txt')
|
|
||||||
|
|
||||||
#: Pre-configured view that serves "Hello world!" via a StringIO.
|
|
||||||
download_generated_hello_world = StringIODownloadView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
download_http_hello_world = views.HTTPDownloadView.as_view(
|
|
||||||
url=u'https://raw.github.com/benoitbryon/django-downloadview/master/demo/demoproject/download/fixtures/hello-world.txt',
|
|
||||||
basename=u'hello-world.txt')
|
|
||||||
10
demo/demoproject/fixtures/demo.json
Normal file
10
demo/demoproject/fixtures/demo.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "object.document",
|
||||||
|
"fields": {
|
||||||
|
"slug": "hello-world",
|
||||||
|
"file": "object/hello-world.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
7
demo/demoproject/http/__init__.py
Normal file
7
demo/demoproject/http/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Demo for :class:`django_downloadview.HTTPDownloadView`.
|
||||||
|
|
||||||
|
Code in this package is included in documentation's :doc:`/views/http`.
|
||||||
|
Make sure to maintain both together.
|
||||||
|
|
||||||
|
"""
|
||||||
1
demo/demoproject/http/models.py
Normal file
1
demo/demoproject/http/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
26
demo/demoproject/http/tests.py
Normal file
26
demo/demoproject/http/tests.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleURLTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'simple_url' serves 'hello-world.txt' from Github."""
|
||||||
|
url = reverse("http:simple_url")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""HTTPDownloadView proxies Content-Type header."""
|
||||||
|
url = reverse("http:avatar_url")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(self, response, mime_type="image/png")
|
||||||
9
demo/demoproject/http/urls.py
Normal file
9
demo/demoproject/http/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from demoproject.http import views
|
||||||
|
|
||||||
|
app_name = "http"
|
||||||
|
urlpatterns = [
|
||||||
|
path("simple_url/", views.simple_url, name="simple_url"),
|
||||||
|
path("avatar_url/", views.avatar_url, name="avatar_url"),
|
||||||
|
]
|
||||||
21
demo/demoproject/http/views.py
Normal file
21
demo/demoproject/http/views.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from django_downloadview import HTTPDownloadView
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleURLDownloadView(HTTPDownloadView):
|
||||||
|
def get_url(self):
|
||||||
|
"""Return URL of hello-world.txt file on GitHub."""
|
||||||
|
return (
|
||||||
|
"https://raw.githubusercontent.com"
|
||||||
|
"/jazzband/django-downloadview"
|
||||||
|
"/b7f660c5e3f37d918b106b02c5af7a887acc0111"
|
||||||
|
"/demo/demoproject/download/fixtures/hello-world.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubAvatarDownloadView(HTTPDownloadView):
|
||||||
|
def get_url(self):
|
||||||
|
return "https://avatars0.githubusercontent.com/u/235204"
|
||||||
|
|
||||||
|
|
||||||
|
simple_url = SimpleURLDownloadView.as_view()
|
||||||
|
avatar_url = GithubAvatarDownloadView.as_view()
|
||||||
1
demo/demoproject/lighttpd/__init__.py
Normal file
1
demo/demoproject/lighttpd/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Lighttpd optimizations."""
|
||||||
1
demo/demoproject/lighttpd/models.py
Normal file
1
demo/demoproject/lighttpd/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
61
demo/demoproject/lighttpd/tests.py
Normal file
61
demo/demoproject/lighttpd/tests.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview.lighttpd import assert_x_sendfile
|
||||||
|
|
||||||
|
from demoproject.lighttpd.views import storage, storage_dir
|
||||||
|
|
||||||
|
|
||||||
|
def setup_file():
|
||||||
|
if not os.path.exists(storage_dir):
|
||||||
|
os.makedirs(storage_dir)
|
||||||
|
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizedByMiddlewareTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'lighttpd:optimized_by_middleware' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("lighttpd:optimized_by_middleware")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/lighttpd-optimized-by-middleware/hello-world.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'lighttpd:optimized_by_decorator' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("lighttpd:optimized_by_decorator")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/lighttpd-optimized-by-decorator/hello-world.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'lighttpd:modified_headers' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("lighttpd:modified_headers")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/lighttpd-modified-headers/hello-world.txt",
|
||||||
|
)
|
||||||
|
self.assertEqual(response["X-Test"], "header")
|
||||||
24
demo/demoproject/lighttpd/urls.py
Normal file
24
demo/demoproject/lighttpd/urls.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""URL mapping."""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from demoproject.lighttpd import views
|
||||||
|
|
||||||
|
app_name = "lighttpd"
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"optimized-by-middleware/",
|
||||||
|
views.optimized_by_middleware,
|
||||||
|
name="optimized_by_middleware",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"optimized-by-decorator/",
|
||||||
|
views.optimized_by_decorator,
|
||||||
|
name="optimized_by_decorator",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"modified_headers/",
|
||||||
|
views.modified_headers,
|
||||||
|
name="modified_headers",
|
||||||
|
),
|
||||||
|
]
|
||||||
38
demo/demoproject/lighttpd/views.py
Normal file
38
demo/demoproject/lighttpd/views.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
from django_downloadview import StorageDownloadView
|
||||||
|
from django_downloadview.lighttpd import x_sendfile
|
||||||
|
|
||||||
|
storage_dir = os.path.join(settings.MEDIA_ROOT, "lighttpd")
|
||||||
|
storage = FileSystemStorage(
|
||||||
|
location=storage_dir, base_url="".join([settings.MEDIA_URL, "lighttpd/"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
optimized_by_middleware = StorageDownloadView.as_view(
|
||||||
|
storage=storage, path="hello-world.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
optimized_by_decorator = x_sendfile(
|
||||||
|
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/lighttpd-optimized-by-decorator/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _modified_headers(request):
|
||||||
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
|
response = view(request)
|
||||||
|
response["X-Test"] = "header"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
modified_headers = x_sendfile(
|
||||||
|
_modified_headers,
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/lighttpd-modified-headers/",
|
||||||
|
)
|
||||||
|
|
@ -4,8 +4,9 @@ import sys
|
||||||
|
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__)
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{__package__}.settings")
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
"""Nginx optimizations applied to demoproject.download."""
|
"""Nginx optimizations."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
|
|
@ -1,62 +1,73 @@
|
||||||
"""Test suite for demoproject.nginx."""
|
import os
|
||||||
from django.core.files import File
|
|
||||||
from django.core.urlresolvers import reverse_lazy as reverse
|
from django.core.files.base import ContentFile
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview.nginx import assert_x_accel_redirect
|
from django_downloadview.nginx import assert_x_accel_redirect
|
||||||
from django_downloadview.test import temporary_media_root
|
|
||||||
|
|
||||||
from demoproject.download.models import Document
|
from demoproject.nginx.views import storage, storage_dir
|
||||||
from demoproject.download.tests import DownloadTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class XAccelRedirectDecoratorTestCase(DownloadTestCase):
|
def setup_file():
|
||||||
@temporary_media_root()
|
if not os.path.exists(storage_dir):
|
||||||
|
os.makedirs(storage_dir)
|
||||||
|
storage.save("hello-world.txt", ContentFile("Hello world!\n"))
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizedByMiddlewareTestCase(django.test.TestCase):
|
||||||
def test_response(self):
|
def test_response(self):
|
||||||
"""'download_document_nginx' view returns a valid X-Accel response."""
|
"""'nginx:optimized_by_middleware' returns X-Accel response."""
|
||||||
document = Document.objects.create(
|
setup_file()
|
||||||
slug='hello-world',
|
url = reverse("nginx:optimized_by_middleware")
|
||||||
file=File(open(self.files['hello-world.txt'])),
|
response = self.client.get(url)
|
||||||
)
|
|
||||||
download_url = reverse('download_document_nginx',
|
|
||||||
kwargs={'slug': 'hello-world'})
|
|
||||||
response = self.client.get(download_url)
|
|
||||||
self.assertEquals(response.status_code, 200)
|
|
||||||
# Validation shortcut: assert_x_accel_redirect.
|
|
||||||
assert_x_accel_redirect(
|
assert_x_accel_redirect(
|
||||||
self,
|
self,
|
||||||
response,
|
response,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
charset="utf-8",
|
charset="utf-8",
|
||||||
basename="hello-world.txt",
|
basename="hello-world.txt",
|
||||||
redirect_url="/download-optimized/document/hello-world.txt",
|
redirect_url="/nginx-optimized-by-middleware/hello-world.txt",
|
||||||
expires=None,
|
expires=None,
|
||||||
with_buffering=None,
|
with_buffering=None,
|
||||||
limit_rate=None)
|
limit_rate=None,
|
||||||
# Check some more items, because this test is part of
|
|
||||||
# django-downloadview tests.
|
|
||||||
self.assertFalse('ContentEncoding' in response)
|
|
||||||
self.assertEquals(response['Content-Disposition'],
|
|
||||||
'attachment; filename=hello-world.txt')
|
|
||||||
|
|
||||||
|
|
||||||
class InlineXAccelRedirectTestCase(DownloadTestCase):
|
|
||||||
@temporary_media_root()
|
|
||||||
def test_response(self):
|
|
||||||
"""X-Accel optimization respects ``attachment`` attribute."""
|
|
||||||
document = Document.objects.create(
|
|
||||||
slug='hello-world',
|
|
||||||
file=File(open(self.files['hello-world.txt'])),
|
|
||||||
)
|
)
|
||||||
download_url = reverse('download_document_nginx_inline',
|
|
||||||
kwargs={'slug': 'hello-world'})
|
|
||||||
response = self.client.get(download_url)
|
class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'nginx:optimized_by_decorator' returns X-Accel response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("nginx:optimized_by_decorator")
|
||||||
|
response = self.client.get(url)
|
||||||
assert_x_accel_redirect(
|
assert_x_accel_redirect(
|
||||||
self,
|
self,
|
||||||
response,
|
response,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
charset="utf-8",
|
charset="utf-8",
|
||||||
attachment=False,
|
basename="hello-world.txt",
|
||||||
redirect_url="/download-optimized/document/hello-world.txt",
|
redirect_url="/nginx-optimized-by-decorator/hello-world.txt",
|
||||||
expires=None,
|
expires=None,
|
||||||
with_buffering=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,12 +1,24 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
from django.conf.urls import patterns, url
|
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
urlpatterns = patterns(
|
from demoproject.nginx import views
|
||||||
'demoproject.nginx.views',
|
|
||||||
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
app_name = "nginx"
|
||||||
'download_document_nginx', name='download_document_nginx'),
|
urlpatterns = [
|
||||||
url(r'^document-nginx-inline/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
path(
|
||||||
'download_document_nginx_inline',
|
"optimized-by-middleware/",
|
||||||
name='download_document_nginx_inline'),
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,38 @@
|
||||||
"""Views."""
|
import os
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
from django_downloadview import StorageDownloadView
|
||||||
from django_downloadview.nginx import x_accel_redirect
|
from django_downloadview.nginx import x_accel_redirect
|
||||||
|
|
||||||
from demoproject.download import views
|
storage_dir = os.path.join(settings.MEDIA_ROOT, "nginx")
|
||||||
|
storage = FileSystemStorage(
|
||||||
|
location=storage_dir, base_url="".join([settings.MEDIA_URL, "nginx/"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
download_document_nginx = x_accel_redirect(
|
optimized_by_middleware = StorageDownloadView.as_view(
|
||||||
views.download_document,
|
storage=storage, path="hello-world.txt"
|
||||||
source_dir='/var/www/files',
|
)
|
||||||
destination_url='/download-optimized')
|
|
||||||
|
|
||||||
|
|
||||||
download_document_nginx_inline = x_accel_redirect(
|
optimized_by_decorator = x_accel_redirect(
|
||||||
views.download_document_inline,
|
StorageDownloadView.as_view(storage=storage, path="hello-world.txt"),
|
||||||
source_dir=settings.MEDIA_ROOT,
|
source_url=storage.base_url,
|
||||||
destination_url='/download-optimized')
|
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/",
|
||||||
|
)
|
||||||
|
|
|
||||||
7
demo/demoproject/object/__init__.py
Normal file
7
demo/demoproject/object/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Demo for :class:`django_downloadview.ObjectDownloadView`.
|
||||||
|
|
||||||
|
Code in this package is included in documentation's :doc:`/views/object`.
|
||||||
|
Make sure to maintain both together.
|
||||||
|
|
||||||
|
"""
|
||||||
8
demo/demoproject/object/models.py
Normal file
8
demo/demoproject/object/models.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Document(models.Model):
|
||||||
|
slug = models.SlugField()
|
||||||
|
file = models.FileField(upload_to="object")
|
||||||
|
another_file = models.FileField(upload_to="object-other")
|
||||||
|
basename = models.CharField(max_length=100)
|
||||||
87
demo/demoproject/object/tests.py
Normal file
87
demo/demoproject/object/tests.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview import assert_download_response, temporary_media_root
|
||||||
|
|
||||||
|
from demoproject.object.models import Document
|
||||||
|
|
||||||
|
# Fixtures.
|
||||||
|
slug = "hello-world"
|
||||||
|
basename = "hello-world.txt"
|
||||||
|
file_name = "file.txt"
|
||||||
|
another_name = "another_file.txt"
|
||||||
|
file_content = "Hello world!\n"
|
||||||
|
another_content = "Goodbye world!\n"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_document():
|
||||||
|
document = Document(slug=slug, basename=basename)
|
||||||
|
document.file.save(file_name, ContentFile(file_content), save=False)
|
||||||
|
document.another_file.save(another_name, ContentFile(another_content), save=False)
|
||||||
|
document.save()
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultFileTestCase(django.test.TestCase):
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'default_file' streams Document.file."""
|
||||||
|
setup_document()
|
||||||
|
url = reverse("object:default_file", kwargs={"slug": slug})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
basename=file_name,
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnotherFileTestCase(django.test.TestCase):
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'another_file' streams Document.another_file."""
|
||||||
|
setup_document()
|
||||||
|
url = reverse("object:another_file", kwargs={"slug": slug})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=another_content,
|
||||||
|
basename=another_name,
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeserializedBasenameTestCase(django.test.TestCase):
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"'deserialized_basename' streams Document.file with custom basename."
|
||||||
|
setup_document()
|
||||||
|
url = reverse("object:deserialized_basename", kwargs={"slug": slug})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
basename=basename,
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InlineFileTestCase(django.test.TestCase):
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"'inline_file_view' streams Document.file inline."
|
||||||
|
setup_document()
|
||||||
|
url = reverse("object:inline_file", kwargs={"slug": slug})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
mime_type="text/plain",
|
||||||
|
attachment=False,
|
||||||
|
)
|
||||||
27
demo/demoproject/object/urls.py
Normal file
27
demo/demoproject/object/urls.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from demoproject.object import views
|
||||||
|
|
||||||
|
app_name = "object"
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(
|
||||||
|
r"^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
|
views.default_file_view,
|
||||||
|
name="default_file",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
|
views.another_file_view,
|
||||||
|
name="another_file",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
|
views.deserialized_basename_view,
|
||||||
|
name="deserialized_basename",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
|
views.inline_file_view,
|
||||||
|
name="inline_file",
|
||||||
|
),
|
||||||
|
]
|
||||||
20
demo/demoproject/object/views.py
Normal file
20
demo/demoproject/object/views.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from django_downloadview import ObjectDownloadView
|
||||||
|
|
||||||
|
from demoproject.object.models import Document
|
||||||
|
|
||||||
|
#: Serve ``file`` attribute of ``Document`` model.
|
||||||
|
default_file_view = ObjectDownloadView.as_view(model=Document)
|
||||||
|
|
||||||
|
#: Serve ``another_file`` attribute of ``Document`` model.
|
||||||
|
another_file_view = ObjectDownloadView.as_view(
|
||||||
|
model=Document, file_field="another_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Serve ``file`` attribute of ``Document`` model, using client-side filename
|
||||||
|
#: from model.
|
||||||
|
deserialized_basename_view = ObjectDownloadView.as_view(
|
||||||
|
model=Document, basename_field="basename"
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Serve ``file`` attribute of ``Document`` model, inline (not as attachment).
|
||||||
|
inline_file_view = ObjectDownloadView.as_view(model=Document, attachment=False)
|
||||||
7
demo/demoproject/path/__init__.py
Normal file
7
demo/demoproject/path/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Demo for :class:`django_downloadview.PathDownloadView`.
|
||||||
|
|
||||||
|
Code in this package is included in documentation's :doc:`/views/path`.
|
||||||
|
Make sure to maintain both together.
|
||||||
|
|
||||||
|
"""
|
||||||
1
demo/demoproject/path/models.py
Normal file
1
demo/demoproject/path/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
32
demo/demoproject/path/tests.py
Normal file
32
demo/demoproject/path/tests.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
|
|
||||||
|
class StaticPathTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'static_path' serves 'fixtures/hello-world.txt'."""
|
||||||
|
url = reverse("path:static_path")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicPathTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'dynamic_path' serves 'fixtures/{path}'."""
|
||||||
|
url = reverse("path:dynamic_path", kwargs={"path": "hello-world.txt"})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
13
demo/demoproject/path/urls.py
Normal file
13
demo/demoproject/path/urls.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.urls import path, re_path
|
||||||
|
|
||||||
|
from demoproject.path import views
|
||||||
|
|
||||||
|
app_name = "path"
|
||||||
|
urlpatterns = [
|
||||||
|
path("static-path/", views.static_path, name="static_path"),
|
||||||
|
re_path(
|
||||||
|
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
|
views.dynamic_path,
|
||||||
|
name="dynamic_path",
|
||||||
|
),
|
||||||
|
]
|
||||||
39
demo/demoproject/path/views.py
Normal file
39
demo/demoproject/path/views.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django_downloadview import PathDownloadView
|
||||||
|
|
||||||
|
# Let's initialize some fixtures.
|
||||||
|
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_dir = os.path.dirname(app_dir)
|
||||||
|
fixtures_dir = os.path.join(project_dir, "fixtures")
|
||||||
|
#: Path to a text file that says 'Hello world!'.
|
||||||
|
hello_world_path = os.path.join(fixtures_dir, "hello-world.txt")
|
||||||
|
|
||||||
|
#: Serve ``fixtures/hello-world.txt`` file.
|
||||||
|
static_path = PathDownloadView.as_view(path=hello_world_path)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicPathDownloadView(PathDownloadView):
|
||||||
|
"""Serve file in ``settings.MEDIA_ROOT``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Make sure to prevent "../" in path via URL patterns.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This particular setup would be easier to perform with
|
||||||
|
:class:`StorageDownloadView`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_path(self):
|
||||||
|
"""Return path inside fixtures directory."""
|
||||||
|
# Get path from URL resolvers or as_view kwarg.
|
||||||
|
relative_path = super().get_path()
|
||||||
|
# Make it absolute.
|
||||||
|
absolute_path = os.path.join(fixtures_dir, relative_path)
|
||||||
|
return absolute_path
|
||||||
|
|
||||||
|
|
||||||
|
dynamic_path = DynamicPathDownloadView.as_view()
|
||||||
162
demo/demoproject/settings.py
Executable file → Normal file
162
demo/demoproject/settings.py
Executable file → Normal file
|
|
@ -1,24 +1,26 @@
|
||||||
"""Django settings for Django-DownloadView demo project."""
|
"""Django settings for django-downloadview demo project."""
|
||||||
from os.path import abspath, dirname, join
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
# Configure some relative directories.
|
# Configure some relative directories.
|
||||||
demoproject_dir = dirname(abspath(__file__))
|
demoproject_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
demo_dir = dirname(demoproject_dir)
|
demo_dir = os.path.dirname(demoproject_dir)
|
||||||
root_dir = dirname(demo_dir)
|
root_dir = os.path.dirname(demo_dir)
|
||||||
data_dir = join(root_dir, 'var')
|
data_dir = os.path.join(root_dir, "var")
|
||||||
|
cfg_dir = os.path.join(root_dir, "etc")
|
||||||
|
|
||||||
|
|
||||||
# Mandatory settings.
|
# Mandatory settings.
|
||||||
ROOT_URLCONF = 'demoproject.urls'
|
ROOT_URLCONF = "demoproject.urls"
|
||||||
WSGI_APPLICATION = 'demoproject.wsgi.application'
|
WSGI_APPLICATION = "demoproject.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database.
|
# Database.
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': join(data_dir, 'db.sqlite'),
|
"NAME": os.path.join(data_dir, "db.sqlite"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,62 +29,106 @@ DATABASES = {
|
||||||
SECRET_KEY = "This is a secret made public on project's repository."
|
SECRET_KEY = "This is a secret made public on project's repository."
|
||||||
|
|
||||||
# Media and static files.
|
# Media and static files.
|
||||||
MEDIA_ROOT = join(data_dir, 'media')
|
MEDIA_ROOT = os.path.join(data_dir, "media")
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = "/media/"
|
||||||
STATIC_ROOT = join(data_dir, 'static')
|
STATIC_ROOT = os.path.join(data_dir, "static")
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
|
|
||||||
# Applications.
|
# Applications.
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
# Standard Django applications.
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.sites',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
# The actual django-downloadview demo.
|
# The actual django-downloadview demo.
|
||||||
'demoproject',
|
"demoproject",
|
||||||
'demoproject.download', # Sample standard download views.
|
"demoproject.object", # Demo around ObjectDownloadView
|
||||||
'demoproject.nginx', # Sample optimizations for Nginx.
|
"demoproject.storage", # Demo around StorageDownloadView
|
||||||
# For test purposes. The demo project is part of django-downloadview
|
"demoproject.path", # Demo around PathDownloadView
|
||||||
# test suite.
|
"demoproject.http", # Demo around HTTPDownloadView
|
||||||
'django_nose',
|
"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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Default middlewares. You may alter the list later.
|
# BEGIN middlewares
|
||||||
MIDDLEWARE_CLASSES = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django_downloadview.SmartDownloadMiddleware",
|
||||||
|
]
|
||||||
|
# END middlewares
|
||||||
|
|
||||||
|
|
||||||
|
# Specific configuration for django_downloadview.SmartDownloadMiddleware.
|
||||||
|
# BEGIN backend
|
||||||
|
DOWNLOADVIEW_BACKEND = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
||||||
|
# END backend
|
||||||
|
"""Could also be:
|
||||||
|
DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware'
|
||||||
|
DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# BEGIN rules
|
||||||
|
DOWNLOADVIEW_RULES = [
|
||||||
|
{
|
||||||
|
"source_url": "/media/nginx/",
|
||||||
|
"destination_url": "/nginx-optimized-by-middleware/",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
# END rules
|
||||||
|
DOWNLOADVIEW_RULES += [
|
||||||
|
{
|
||||||
|
"source_url": "/media/apache/",
|
||||||
|
"destination_dir": "/apache-optimized-by-middleware/",
|
||||||
|
# Bypass global default backend with additional argument "backend".
|
||||||
|
# Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be
|
||||||
|
# enough. Here, the django_downloadview demo project needs to
|
||||||
|
# demonstrate usage of several backends.
|
||||||
|
"backend": "django_downloadview.apache.XSendfileMiddleware",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Uncomment the following lines to enable global Nginx optimizations.
|
# Test/development settings.
|
||||||
#MIDDLEWARE_CLASSES.append('django_downloadview.nginx.XAccelRedirectMiddleware')
|
|
||||||
#NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT
|
|
||||||
#NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = "/proxied-download"
|
|
||||||
|
|
||||||
|
|
||||||
# Development configuration.
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
|
||||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
|
||||||
NOSE_ARGS = ['--verbose',
|
TEMPLATES = [
|
||||||
'--nocapture',
|
{
|
||||||
'--rednose',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'--with-id', # allows --failed which only reruns failed tests
|
"DIRS": [os.path.join(os.path.dirname(__file__), "templates")],
|
||||||
'--id-file=%s' % join(data_dir, 'test', 'noseids'),
|
"OPTIONS": {
|
||||||
'--with-doctest',
|
"debug": DEBUG,
|
||||||
'--with-xunit',
|
"context_processors": [
|
||||||
'--xunit-file=%s' % join(data_dir, 'test', 'nosetests.xml'),
|
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
|
||||||
'--with-coverage',
|
# list if you haven't customized them:
|
||||||
'--cover-erase',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'--cover-package=django_downloadview',
|
"django.template.context_processors.debug",
|
||||||
'--no-path-adjustment',
|
"django.template.context_processors.i18n",
|
||||||
'--all-modules',
|
"django.template.context_processors.media",
|
||||||
]
|
"django.template.context_processors.static",
|
||||||
|
"django.template.context_processors.tz",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
|
||||||
7
demo/demoproject/storage/__init__.py
Normal file
7
demo/demoproject/storage/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Demo for :class:`django_downloadview.StorageDownloadView`.
|
||||||
|
|
||||||
|
Code in this package is included in documentation's :doc:`/views/storage`.
|
||||||
|
Make sure to maintain both together.
|
||||||
|
|
||||||
|
"""
|
||||||
1
demo/demoproject/storage/models.py
Normal file
1
demo/demoproject/storage/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
3
demo/demoproject/storage/storage.py
Normal file
3
demo/demoproject/storage/storage.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
storage = FileSystemStorage()
|
||||||
115
demo/demoproject/storage/tests.py
Normal file
115
demo/demoproject/storage/tests.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import datetime
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.http.response import HttpResponseNotModified
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview import (
|
||||||
|
assert_download_response,
|
||||||
|
setup_view,
|
||||||
|
temporary_media_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
from demoproject.storage import views
|
||||||
|
|
||||||
|
# Fixtures.
|
||||||
|
file_content = "Hello world!\n"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_file(path):
|
||||||
|
views.storage.save(path, ContentFile(file_content))
|
||||||
|
|
||||||
|
|
||||||
|
class StaticPathTestCase(django.test.TestCase):
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'storage:static_path' streams file by path."""
|
||||||
|
setup_file("1.txt")
|
||||||
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
basename="1.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_not_modified_download_response(self):
|
||||||
|
"""'storage:static_path' sends not modified response if unmodified."""
|
||||||
|
setup_file("1.txt")
|
||||||
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
|
year = datetime.date.today().year + 4
|
||||||
|
response = self.client.get(
|
||||||
|
url, headers={"if-modified-since": f"Sat, 29 Oct {year} 19:43:31 GMT"}
|
||||||
|
)
|
||||||
|
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||||
|
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_modified_since_download_response(self):
|
||||||
|
"""'storage:static_path' streams file if modified."""
|
||||||
|
setup_file("1.txt")
|
||||||
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
|
response = self.client.get(
|
||||||
|
url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"}
|
||||||
|
)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
basename="1.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicPathIntegrationTestCase(django.test.TestCase):
|
||||||
|
"""Integration tests around ``storage:dynamic_path`` URL."""
|
||||||
|
|
||||||
|
@temporary_media_root()
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'dynamic_path' streams file by generated path.
|
||||||
|
|
||||||
|
As we use ``self.client``, this test involves the whole Django stack,
|
||||||
|
including settings, middlewares, decorators... So we need to setup a
|
||||||
|
file, the storage, and an URL.
|
||||||
|
|
||||||
|
This test actually asserts the URL ``storage:dynamic_path`` streams a
|
||||||
|
file in storage.
|
||||||
|
|
||||||
|
"""
|
||||||
|
setup_file("1.TXT")
|
||||||
|
url = reverse("storage:dynamic_path", kwargs={"path": "1.txt"})
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content=file_content,
|
||||||
|
basename="1.TXT",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicPathUnitTestCase(unittest.TestCase):
|
||||||
|
"""Unit tests around ``views.DynamicStorageDownloadView``."""
|
||||||
|
|
||||||
|
def test_get_path(self):
|
||||||
|
"""DynamicStorageDownloadView.get_path() returns uppercase path.
|
||||||
|
|
||||||
|
Uses :func:`~django_downloadview.test.setup_view` to target only
|
||||||
|
overriden methods.
|
||||||
|
|
||||||
|
This test does not involve URLconf, middlewares or decorators. It is
|
||||||
|
fast. It has clear scope. It does not assert ``storage:dynamic_path``
|
||||||
|
URL works. It targets only custom ``DynamicStorageDownloadView`` class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
view = setup_view(
|
||||||
|
views.DynamicStorageDownloadView(),
|
||||||
|
django.test.RequestFactory().get("/fake-url"),
|
||||||
|
path="dummy path",
|
||||||
|
)
|
||||||
|
path = view.get_path()
|
||||||
|
self.assertEqual(path, "DUMMY PATH")
|
||||||
17
demo/demoproject/storage/urls.py
Normal file
17
demo/demoproject/storage/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from demoproject.storage import views
|
||||||
|
|
||||||
|
app_name = "storage"
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(
|
||||||
|
r"^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
|
views.static_path,
|
||||||
|
name="static_path",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
|
views.dynamic_path,
|
||||||
|
name="dynamic_path",
|
||||||
|
),
|
||||||
|
]
|
||||||
20
demo/demoproject/storage/views.py
Normal file
20
demo/demoproject/storage/views.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
from django_downloadview import StorageDownloadView
|
||||||
|
|
||||||
|
storage = FileSystemStorage()
|
||||||
|
|
||||||
|
|
||||||
|
#: Serve file using ``path`` argument.
|
||||||
|
static_path = StorageDownloadView.as_view(storage=storage)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicStorageDownloadView(StorageDownloadView):
|
||||||
|
"""Serve file of storage by path.upper()."""
|
||||||
|
|
||||||
|
def get_path(self):
|
||||||
|
"""Return uppercase path."""
|
||||||
|
return super().get_path().upper()
|
||||||
|
|
||||||
|
|
||||||
|
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
{% load url from future %}
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>django-downloadview demo</title>
|
<title>django-downloadview demo</title>
|
||||||
|
|
@ -10,19 +9,10 @@
|
||||||
<h2>Serving files with Django</h2>
|
<h2>Serving files with Django</h2>
|
||||||
<p>In the following views, Django streams the files, no optimization
|
<p>In the following views, Django streams the files, no optimization
|
||||||
has been setup.</p>
|
has been setup.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'hello_world' %}">PathDownloadView</a></li>
|
<li>
|
||||||
<li><a href="{% url 'fixture_from_path' 'hello-world.txt' %}">
|
<a href="{% url 'object:default_file' 'hello-world' %}">object:default_file</a>
|
||||||
PathDownloadView + argument in URL
|
</li>
|
||||||
</a></li>
|
|
||||||
<li><a href="{% url 'fixture_from_storage' 'hello-world.txt' %}">
|
|
||||||
StorageDownloadView + path in URL
|
|
||||||
</a></li>
|
|
||||||
<li><a href="{% url 'document' 'hello-world' %}">
|
|
||||||
ObjectDownloadView
|
|
||||||
</a></li>
|
|
||||||
<li><a href="{% url 'http_hello_world' %}">
|
|
||||||
HTTPDownloadView</a>, a simple HTTP proxy</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Optimized downloads</h2>
|
<h2>Optimized downloads</h2>
|
||||||
|
|
@ -31,9 +21,6 @@
|
||||||
<p>Since nginx and other servers aren't installed on the demo, you
|
<p>Since nginx and other servers aren't installed on the demo, you
|
||||||
will get raw "X-Sendfile" responses. Look at the headers!</p>
|
will get raw "X-Sendfile" responses. Look at the headers!</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">
|
|
||||||
ObjectDownloadView (nginx)
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
# coding=utf8
|
|
||||||
"""Test suite for demoproject.download."""
|
"""Test suite for demoproject.download."""
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class HomeViewTestCase(TestCase):
|
class HomeViewTestCase(TestCase):
|
||||||
"""Test homepage."""
|
"""Test homepage."""
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
"""Homepage returns HTTP 200."""
|
"""Homepage returns HTTP 200."""
|
||||||
home_url = reverse('home')
|
home_url = reverse("home")
|
||||||
response = self.client.get(home_url)
|
response = self.client.get(home_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
||||||
48
demo/demoproject/urls.py
Executable file → Normal file
48
demo/demoproject/urls.py
Executable file → Normal file
|
|
@ -1,16 +1,44 @@
|
||||||
from django.conf.urls import patterns, include, url
|
from django.urls import include, path
|
||||||
from django.views.generic import TemplateView
|
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.
|
||||||
# Standard download views.
|
path(
|
||||||
url(r'^download/', include('demoproject.download.urls')),
|
"object/",
|
||||||
|
include("demoproject.object.urls", namespace="object"),
|
||||||
|
),
|
||||||
|
# StorageDownloadView.
|
||||||
|
path(
|
||||||
|
"storage/",
|
||||||
|
include("demoproject.storage.urls", namespace="storage"),
|
||||||
|
),
|
||||||
|
# PathDownloadView.
|
||||||
|
path("path/", include("demoproject.path.urls", namespace="path")),
|
||||||
|
# HTTPDownloadView.
|
||||||
|
path("http/", include("demoproject.http.urls", namespace="http")),
|
||||||
|
# VirtualDownloadView.
|
||||||
|
path(
|
||||||
|
"virtual/",
|
||||||
|
include("demoproject.virtual.urls", namespace="virtual"),
|
||||||
|
),
|
||||||
# Nginx optimizations.
|
# Nginx optimizations.
|
||||||
url(r'^nginx/', include('demoproject.nginx.urls')),
|
path(
|
||||||
|
"nginx/",
|
||||||
|
include("demoproject.nginx.urls", namespace="nginx"),
|
||||||
|
),
|
||||||
|
# Apache optimizations.
|
||||||
|
path(
|
||||||
|
"apache/",
|
||||||
|
include("demoproject.apache.urls", namespace="apache"),
|
||||||
|
),
|
||||||
|
# Lighttpd optimizations.
|
||||||
|
path(
|
||||||
|
"lighttpd/",
|
||||||
|
include("demoproject.lighttpd.urls", namespace="lighttpd"),
|
||||||
|
),
|
||||||
# An informative homepage.
|
# An informative homepage.
|
||||||
url(r'', home, name='home')
|
path("", home, name="home"),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
7
demo/demoproject/virtual/__init__.py
Normal file
7
demo/demoproject/virtual/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Demo for :class:`django_downloadview.VirtualDownloadView`.
|
||||||
|
|
||||||
|
Code in this package is included in documentation's :doc:`/views/virtual`.
|
||||||
|
Make sure to maintain both together.
|
||||||
|
|
||||||
|
"""
|
||||||
1
demo/demoproject/virtual/models.py
Normal file
1
demo/demoproject/virtual/models.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Required to make a Django application."""
|
||||||
46
demo/demoproject/virtual/tests.py
Normal file
46
demo/demoproject/virtual/tests.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
|
|
||||||
|
class TextTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'virtual:text' serves 'hello-world.txt' from unicode."""
|
||||||
|
url = reverse("virtual:text")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StringIOTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'virtual:stringio' serves 'hello-world.txt' from stringio."""
|
||||||
|
url = reverse("virtual:stringio")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedTestCase(django.test.TestCase):
|
||||||
|
def test_download_response(self):
|
||||||
|
"""'virtual:generated' serves 'hello-world.txt' from generator."""
|
||||||
|
url = reverse("virtual:generated")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_download_response(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content="Hello world!\n",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
10
demo/demoproject/virtual/urls.py
Normal file
10
demo/demoproject/virtual/urls.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from demoproject.virtual import views
|
||||||
|
|
||||||
|
app_name = "virtual"
|
||||||
|
urlpatterns = [
|
||||||
|
path("text/", views.TextDownloadView.as_view(), name="text"),
|
||||||
|
path("stringio/", views.StringIODownloadView.as_view(), name="stringio"),
|
||||||
|
path("gerenated/", views.GeneratedDownloadView.as_view(), name="generated"),
|
||||||
|
]
|
||||||
31
demo/demoproject/virtual/views.py
Normal file
31
demo/demoproject/virtual/views.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
from django_downloadview import TextIteratorIO, VirtualDownloadView, VirtualFile
|
||||||
|
|
||||||
|
|
||||||
|
class TextDownloadView(VirtualDownloadView):
|
||||||
|
def get_file(self):
|
||||||
|
"""Return :class:`django.core.files.base.ContentFile` object."""
|
||||||
|
return ContentFile(b"Hello world!\n", name="hello-world.txt")
|
||||||
|
|
||||||
|
|
||||||
|
class StringIODownloadView(VirtualDownloadView):
|
||||||
|
def get_file(self):
|
||||||
|
"""Return wrapper on ``six.StringIO`` object."""
|
||||||
|
file_obj = StringIO("Hello world!\n")
|
||||||
|
return VirtualFile(file_obj, name="hello-world.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_hello():
|
||||||
|
yield "Hello "
|
||||||
|
yield "world!"
|
||||||
|
yield "\n"
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedDownloadView(VirtualDownloadView):
|
||||||
|
def get_file(self):
|
||||||
|
"""Return wrapper on ``StringIteratorIO`` object."""
|
||||||
|
file_obj = TextIteratorIO(generate_hello())
|
||||||
|
return VirtualFile(file_obj, name="hello-world.txt")
|
||||||
|
|
@ -12,14 +12,16 @@ middleware here, or combine a Django application with an application of another
|
||||||
framework.
|
framework.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__)
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__)
|
||||||
|
|
||||||
# This application object is used by any WSGI server configured to use this
|
# This application object is used by any WSGI server configured to use this
|
||||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||||
# setting points here.
|
# setting points here.
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
||||||
# Apply WSGI middleware here.
|
# Apply WSGI middleware here.
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,26 @@
|
||||||
# coding=utf-8
|
|
||||||
"""Python packaging."""
|
|
||||||
import os
|
import os
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
def read_relative_file(filename):
|
setup(
|
||||||
"""Returns contents of the given file, which path is supposed relative
|
name="django-downloadview-demo",
|
||||||
to this module."""
|
version="1.0",
|
||||||
with open(os.path.join(os.path.dirname(__file__), filename)) as f:
|
description="Serve files with Django and reverse-proxies.",
|
||||||
return f.read()
|
long_description=open(os.path.join(here, "README.rst")).read(),
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
NAME = 'django-downloadview-demo'
|
"License :: OSI Approved :: BSD License",
|
||||||
README = read_relative_file('README')
|
"Programming Language :: Python :: 3",
|
||||||
VERSION = '0.1'
|
"Framework :: Django",
|
||||||
PACKAGES = ['demoproject']
|
],
|
||||||
REQUIRES = ['django-downloadview',
|
author="Benoît Bryon",
|
||||||
'django-nose']
|
author_email="benoit@marmelune.net",
|
||||||
|
url="https://django-downloadview.readthedocs.io/",
|
||||||
|
license="BSD",
|
||||||
setup(name=NAME,
|
packages=["demoproject"],
|
||||||
version=VERSION,
|
include_package_data=True,
|
||||||
description='Demo project for Django-DownloadView.',
|
zip_safe=False,
|
||||||
long_description=README,
|
install_requires=["django-downloadview", "pytest-django"],
|
||||||
classifiers=['Development Status :: 1 - Planning',
|
entry_points={"console_scripts": ["demo = demoproject.manage:main"]},
|
||||||
'License :: OSI Approved :: BSD License',
|
)
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 2.6',
|
|
||||||
'Framework :: Django',
|
|
||||||
],
|
|
||||||
keywords='class-based view, generic view, download',
|
|
||||||
author='Benoit Bryon',
|
|
||||||
author_email='benoit@marmelune.net',
|
|
||||||
url='https://github.com/benoitbryon/%s' % NAME,
|
|
||||||
license='BSD',
|
|
||||||
packages=PACKAGES,
|
|
||||||
include_package_data=True,
|
|
||||||
zip_safe=False,
|
|
||||||
install_requires=REQUIRES,
|
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
'demo = demoproject.manage:main',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
"""django-downloadview provides generic download views for Django."""
|
"""Serve files with Django and reverse proxies."""
|
||||||
# Shortcut import.
|
|
||||||
from django_downloadview.views import (PathDownloadView,
|
|
||||||
ObjectDownloadView,
|
|
||||||
StorageDownloadView,
|
|
||||||
VirtualDownloadView)
|
|
||||||
|
|
||||||
|
from django_downloadview.api import * # NoQA
|
||||||
|
|
||||||
pkg_resources = __import__('pkg_resources')
|
import importlib.metadata
|
||||||
distribution = pkg_resources.get_distribution('django-downloadview')
|
|
||||||
|
|
||||||
#: Module version, as defined in PEP-0396.
|
#: Module version, as defined in PEP-0396.
|
||||||
__version__ = distribution.version
|
__version__ = importlib.metadata.version(__package__.replace("-", "_"))
|
||||||
|
|
|
||||||
13
django_downloadview/apache/__init__.py
Normal file
13
django_downloadview/apache/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Optimizations for Apache.
|
||||||
|
|
||||||
|
See also `documentation of mod_xsendfile for Apache
|
||||||
|
<https://tn123.org/mod_xsendfile/>`_ and :doc:`narrative documentation about
|
||||||
|
Apache optimizations </optimizations/apache>`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# API shortcuts.
|
||||||
|
from django_downloadview.apache.decorators import x_sendfile # NoQA
|
||||||
|
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
from django_downloadview.apache.response import XSendfileResponse # NoQA
|
||||||
|
from django_downloadview.apache.tests import assert_x_sendfile # NoQA
|
||||||
16
django_downloadview/apache/decorators.py
Normal file
16
django_downloadview/apache/decorators.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
||||||
|
|
||||||
|
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
||||||
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
|
|
||||||
|
|
||||||
|
def x_sendfile(view_func, *args, **kwargs):
|
||||||
|
"""Apply
|
||||||
|
:class:`~django_downloadview.apache.middlewares.XSendfileMiddleware` to
|
||||||
|
``view_func``.
|
||||||
|
|
||||||
|
Proxies (``*args``, ``**kwargs``) to middleware constructor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
decorator = DownloadDecorator(XSendfileMiddleware)
|
||||||
|
return decorator(view_func, *args, **kwargs)
|
||||||
36
django_downloadview/apache/middlewares.py
Normal file
36
django_downloadview/apache/middlewares.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
from django_downloadview.apache.response import XSendfileResponse
|
||||||
|
from django_downloadview.middlewares import (
|
||||||
|
NoRedirectionMatch,
|
||||||
|
ProxiedDownloadMiddleware,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
|
"""Configurable middleware, for use in decorators or in global middlewares.
|
||||||
|
|
||||||
|
Standard Django middlewares are configured globally via settings. Instances
|
||||||
|
of this class are to be configured individually. It makes it possible to
|
||||||
|
use this class as the factory in
|
||||||
|
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||||
|
):
|
||||||
|
"""Constructor."""
|
||||||
|
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||||
|
|
||||||
|
def process_download_response(self, request, response):
|
||||||
|
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||||
|
try:
|
||||||
|
redirect_url = self.get_redirect_url(response)
|
||||||
|
except NoRedirectionMatch:
|
||||||
|
return response
|
||||||
|
return XSendfileResponse(
|
||||||
|
file_path=redirect_url,
|
||||||
|
content_type=response["Content-Type"],
|
||||||
|
basename=response.basename,
|
||||||
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
|
)
|
||||||
22
django_downloadview/apache/response.py
Normal file
22
django_downloadview/apache/response.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Apache's specific responses."""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileResponse(ProxiedDownloadResponse):
|
||||||
|
"Delegates serving file to Apache via X-Sendfile header."
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||||
|
):
|
||||||
|
"""Return a HttpResponse with headers for Apache X-Sendfile."""
|
||||||
|
# content-type must be provided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
|
if attachment:
|
||||||
|
self.basename = basename or os.path.basename(file_path)
|
||||||
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
self["X-Sendfile"] = file_path
|
||||||
62
django_downloadview/apache/tests.py
Normal file
62
django_downloadview/apache/tests.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from django_downloadview.apache.response import XSendfileResponse
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileValidator:
|
||||||
|
"""Utility class to validate XSendfileResponse instances.
|
||||||
|
|
||||||
|
See also :py:func:`assert_x_sendfile` shortcut function.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, test_case, response, **assertions):
|
||||||
|
"""Assert that ``response`` is a valid X-Sendfile response.
|
||||||
|
|
||||||
|
Optional ``assertions`` dictionary can be used to check additional
|
||||||
|
items:
|
||||||
|
|
||||||
|
* ``basename``: the basename of the file in the response.
|
||||||
|
|
||||||
|
* ``content_type``: the value of "Content-Type" header.
|
||||||
|
|
||||||
|
* ``file_path``: the value of "X-Sendfile" header.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.assert_x_sendfile_response(test_case, response)
|
||||||
|
for key, value in assertions.items():
|
||||||
|
assert_func = getattr(self, "assert_%s" % key)
|
||||||
|
assert_func(test_case, response, value)
|
||||||
|
|
||||||
|
def assert_x_sendfile_response(self, test_case, response):
|
||||||
|
test_case.assertTrue(isinstance(response, XSendfileResponse))
|
||||||
|
|
||||||
|
def assert_basename(self, test_case, response, value):
|
||||||
|
test_case.assertEqual(response.basename, value)
|
||||||
|
|
||||||
|
def assert_content_type(self, test_case, response, value):
|
||||||
|
test_case.assertEqual(response["Content-Type"], value)
|
||||||
|
|
||||||
|
def assert_file_path(self, test_case, response, value):
|
||||||
|
test_case.assertEqual(response["X-Sendfile"], value)
|
||||||
|
|
||||||
|
def assert_attachment(self, test_case, response, value):
|
||||||
|
header = "Content-Disposition"
|
||||||
|
if value:
|
||||||
|
test_case.assertTrue(response[header].startswith("attachment"))
|
||||||
|
else:
|
||||||
|
test_case.assertFalse(header in response)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_x_sendfile(test_case, response, **assertions):
|
||||||
|
"""Make ``test_case`` assert that ``response`` is a XSendfileResponse.
|
||||||
|
|
||||||
|
Optional ``assertions`` dictionary can be used to check additional items:
|
||||||
|
|
||||||
|
* ``basename``: the basename of the file in the response.
|
||||||
|
|
||||||
|
* ``content_type``: the value of "Content-Type" header.
|
||||||
|
|
||||||
|
* ``file_path``: the value of "X-Sendfile" header.
|
||||||
|
|
||||||
|
"""
|
||||||
|
validator = XSendfileValidator()
|
||||||
|
return validator(test_case, response, **assertions)
|
||||||
29
django_downloadview/api.py
Normal file
29
django_downloadview/api.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# flake8: noqa
|
||||||
|
"""Declaration of API shortcuts."""
|
||||||
|
|
||||||
|
from django_downloadview.files import HTTPFile, StorageFile, VirtualFile
|
||||||
|
from django_downloadview.io import BytesIteratorIO, TextIteratorIO
|
||||||
|
from django_downloadview.middlewares import (
|
||||||
|
BaseDownloadMiddleware,
|
||||||
|
DownloadDispatcherMiddleware,
|
||||||
|
SmartDownloadMiddleware,
|
||||||
|
)
|
||||||
|
from django_downloadview.response import DownloadResponse, ProxiedDownloadResponse
|
||||||
|
from django_downloadview.shortcuts import sendfile
|
||||||
|
from django_downloadview.test import (
|
||||||
|
assert_download_response,
|
||||||
|
setup_view,
|
||||||
|
temporary_media_root,
|
||||||
|
)
|
||||||
|
from django_downloadview.views import (
|
||||||
|
BaseDownloadView,
|
||||||
|
DownloadMixin,
|
||||||
|
HTTPDownloadView,
|
||||||
|
ObjectDownloadView,
|
||||||
|
PathDownloadView,
|
||||||
|
StorageDownloadView,
|
||||||
|
VirtualDownloadView,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backward compatibility.
|
||||||
|
StringIteratorIO = TextIteratorIO
|
||||||
|
|
@ -1,32 +1,76 @@
|
||||||
"""View decorators.
|
"""View decorators.
|
||||||
|
|
||||||
See also decorators provided by server-specific modules, such as
|
See also decorators provided by server-specific modules, such as
|
||||||
:py:func:`django_downloadview.nginx.x_accel_redirect`.
|
:func:`django_downloadview.nginx.x_accel_redirect`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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):
|
class DownloadDecorator(object):
|
||||||
"""View decorator factory to apply middleware to ``view_func`` response.
|
"""View decorator factory to apply middleware to ``view_func``'s response.
|
||||||
|
|
||||||
Middleware instance is built from ``middleware_factory`` with ``*args`` and
|
Middleware instance is built from ``middleware_factory`` with ``*args`` and
|
||||||
``**kwargs``. Middleware factory is typically a class, such as some
|
``**kwargs``. Middleware factory is typically a class, such as some
|
||||||
:py:class:`django_downloadview.middlewares.XAccelMiddleware` subclass.
|
:py:class:`django_downloadview.BaseDownloadMiddleware` subclass.
|
||||||
|
|
||||||
Response is built from view, then the middleware's ``process_response``
|
Response is built from view, then the middleware's ``process_response``
|
||||||
method is applied on response.
|
method is applied on response.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, middleware_factory):
|
def __init__(self, middleware_factory):
|
||||||
"""Create a download view decorator."""
|
"""Create a download view decorator."""
|
||||||
self.middleware_factory = middleware_factory
|
self.middleware_factory = middleware_factory
|
||||||
|
|
||||||
def __call__(self, view_func, *middleware_args, **middleware_kwargs):
|
def __call__(self, view_func, *middleware_args, **middleware_kwargs):
|
||||||
"""Return ``view_func`` decorated with response middleware."""
|
"""Return ``view_func`` decorated with response middleware."""
|
||||||
|
|
||||||
def decorated(request, *view_args, **view_kwargs):
|
def decorated(request, *view_args, **view_kwargs):
|
||||||
"""Return view's response modified by middleware."""
|
"""Return view's response modified by middleware."""
|
||||||
response = view_func(request, *view_args, **view_kwargs)
|
response = view_func(request, *view_args, **view_kwargs)
|
||||||
middleware = self.middleware_factory(*middleware_args,
|
middleware = self.middleware_factory(*middleware_args, **middleware_kwargs)
|
||||||
**middleware_kwargs)
|
|
||||||
return middleware.process_response(request, response)
|
return middleware.process_response(request, response)
|
||||||
|
|
||||||
return decorated
|
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
|
||||||
|
|
|
||||||
10
django_downloadview/exceptions.py
Normal file
10
django_downloadview/exceptions.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Custom exceptions."""
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotFound(IOError):
|
||||||
|
"""Requested file does not exist.
|
||||||
|
|
||||||
|
This exception is to be raised when operations (such as read) fail because
|
||||||
|
file does not exist (whatever the storage or location).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
"""File wrappers for use as exchange data between views and responses."""
|
"""File wrappers for use as exchange data between views and responses."""
|
||||||
from django.core.files import File
|
|
||||||
|
from io import BytesIO
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.core.files.base import File
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
|
||||||
|
from django_downloadview.io import BytesIteratorIO
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
@ -11,6 +18,7 @@ class StorageFile(File):
|
||||||
but unrelated to model instance.
|
but unrelated to model instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, storage, name, file=None):
|
def __init__(self, storage, name, file=None):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
|
|
@ -27,8 +35,8 @@ class StorageFile(File):
|
||||||
|
|
||||||
def _get_file(self):
|
def _get_file(self):
|
||||||
"""Getter for :py:attr:``file`` property."""
|
"""Getter for :py:attr:``file`` property."""
|
||||||
if not hasattr(self, '_file') or self._file is None:
|
if not hasattr(self, "_file") or self._file is None:
|
||||||
self._file = self.storage.open(self.name, 'rb')
|
self._file = self.storage.open(self.name, "rb")
|
||||||
return self._file
|
return self._file
|
||||||
|
|
||||||
def _set_file(self, file):
|
def _set_file(self, file):
|
||||||
|
|
@ -42,7 +50,7 @@ class StorageFile(File):
|
||||||
#: Required by django.core.files.utils.FileProxy.
|
#: Required by django.core.files.utils.FileProxy.
|
||||||
file = property(_get_file, _set_file, _del_file)
|
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.
|
"""Retrieves the specified file from storage and return open() result.
|
||||||
|
|
||||||
Proxy to self.storage.open(self.name, mode).
|
Proxy to self.storage.open(self.name, mode).
|
||||||
|
|
@ -114,7 +122,10 @@ class StorageFile(File):
|
||||||
Proxy to self.storage.accessed_time(self.name).
|
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
|
@property
|
||||||
def created_time(self):
|
def created_time(self):
|
||||||
|
|
@ -123,7 +134,10 @@ class StorageFile(File):
|
||||||
Proxy to self.storage.created_time(self.name).
|
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
|
@property
|
||||||
def modified_time(self):
|
def modified_time(self):
|
||||||
|
|
@ -132,12 +146,16 @@ class StorageFile(File):
|
||||||
Proxy to self.storage.modified_time(self.name).
|
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):
|
class VirtualFile(File):
|
||||||
"""Wrapper for files that live in memory."""
|
"""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.
|
"""Constructor.
|
||||||
|
|
||||||
file:
|
file:
|
||||||
|
|
@ -150,7 +168,7 @@ class VirtualFile(File):
|
||||||
File URL.
|
File URL.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super(VirtualFile, self).__init__(file, name)
|
super().__init__(file, name)
|
||||||
self.url = url
|
self.url = url
|
||||||
if size is not None:
|
if size is not None:
|
||||||
self._size = size
|
self._size = size
|
||||||
|
|
@ -166,10 +184,36 @@ class VirtualFile(File):
|
||||||
return self._size
|
return self._size
|
||||||
|
|
||||||
def _set_size(self, value):
|
def _set_size(self, value):
|
||||||
return super(VirtualFile, self)._set_size(value)
|
return super()._set_size(value)
|
||||||
|
|
||||||
size = property(_get_size, _set_size)
|
size = property(_get_size, _set_size)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Same as ``File.__iter__()`` but using ``force_bytes()``.
|
||||||
|
|
||||||
|
See https://code.djangoproject.com/ticket/21321
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Iterate over this file-like object by newlines
|
||||||
|
buffer_ = None
|
||||||
|
for chunk in self.chunks():
|
||||||
|
chunk_buffer = BytesIO(force_bytes(chunk))
|
||||||
|
|
||||||
|
for line in chunk_buffer:
|
||||||
|
if buffer_:
|
||||||
|
line = buffer_ + line
|
||||||
|
buffer_ = None
|
||||||
|
|
||||||
|
# If this is the end of a line, yield
|
||||||
|
# otherwise, wait for the next round
|
||||||
|
if line[-1] in ("\n", "\r"):
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
buffer_ = line
|
||||||
|
|
||||||
|
if buffer_ is not None:
|
||||||
|
yield buffer_
|
||||||
|
|
||||||
|
|
||||||
class HTTPFile(File):
|
class HTTPFile(File):
|
||||||
"""Wrapper for files that live on remote HTTP servers.
|
"""Wrapper for files that live on remote HTTP servers.
|
||||||
|
|
@ -181,12 +225,19 @@ class HTTPFile(File):
|
||||||
Always sets "stream=True" in requests kwargs.
|
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.request_factory = request_factory
|
||||||
self.url = url
|
self.url = url
|
||||||
self.name = name
|
if name is None:
|
||||||
kwargs['stream'] = True
|
parts = urlparse(url)
|
||||||
|
if parts.path: # Name from path.
|
||||||
|
self.name = parts.path.strip("/").rsplit("/", 1)[-1]
|
||||||
|
else: # Name from domain.
|
||||||
|
self.name = parts.netloc
|
||||||
|
else:
|
||||||
|
self.name = name
|
||||||
|
kwargs["stream"] = True
|
||||||
self.request_kwargs = kwargs
|
self.request_kwargs = kwargs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -194,13 +245,17 @@ class HTTPFile(File):
|
||||||
try:
|
try:
|
||||||
return self._request
|
return self._request
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._request = self.request_factory(self.url,
|
self._request = self.request_factory(self.url, **self.request_kwargs)
|
||||||
**self.request_kwargs)
|
|
||||||
return self._request
|
return self._request
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file(self):
|
def file(self):
|
||||||
return self.request.raw
|
try:
|
||||||
|
return self._file
|
||||||
|
except AttributeError:
|
||||||
|
content = self.request.iter_content(decode_unicode=False)
|
||||||
|
self._file = BytesIteratorIO(content)
|
||||||
|
return self._file
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
|
|
@ -209,4 +264,9 @@ class HTTPFile(File):
|
||||||
Reads response's "content-length" header.
|
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"]
|
||||||
|
|
|
||||||
143
django_downloadview/io.py
Normal file
143
django_downloadview/io.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""Low-level IO operations, for use with file wrappers."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
|
from django.utils.encoding import force_bytes, force_str
|
||||||
|
|
||||||
|
|
||||||
|
class TextIteratorIO(io.TextIOBase):
|
||||||
|
"""A dynamically generated TextIO-like object.
|
||||||
|
|
||||||
|
Original code by Matt Joiner <anacrolix@gmail.com> from:
|
||||||
|
|
||||||
|
* http://stackoverflow.com/questions/12593576/
|
||||||
|
* https://gist.github.com/anacrolix/3788413
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iterator):
|
||||||
|
#: Iterator/generator for content.
|
||||||
|
self._iter = iterator
|
||||||
|
|
||||||
|
#: Internal buffer.
|
||||||
|
self._left = ""
|
||||||
|
|
||||||
|
def readable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _read1(self, n=None):
|
||||||
|
while not self._left:
|
||||||
|
try:
|
||||||
|
self._left = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Make sure we handle text.
|
||||||
|
self._left = force_str(self._left)
|
||||||
|
ret = self._left[:n]
|
||||||
|
self._left = self._left[len(ret) :]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def read(self, n=None):
|
||||||
|
"""Return content up to ``n`` length."""
|
||||||
|
chunks = []
|
||||||
|
if n is None or n < 0:
|
||||||
|
while True:
|
||||||
|
m = self._read1()
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
chunks.append(m)
|
||||||
|
else:
|
||||||
|
while n > 0:
|
||||||
|
m = self._read1(n)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
n -= len(m)
|
||||||
|
chunks.append(m)
|
||||||
|
return "".join(chunks)
|
||||||
|
|
||||||
|
def readline(self):
|
||||||
|
chunks = []
|
||||||
|
while True:
|
||||||
|
i = self._left.find("\n")
|
||||||
|
if i == -1:
|
||||||
|
chunks.append(self._left)
|
||||||
|
try:
|
||||||
|
self._left = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
self._left = ""
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
chunks.append(self._left[: i + 1])
|
||||||
|
self._left = self._left[i + 1 :]
|
||||||
|
break
|
||||||
|
return "".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
class BytesIteratorIO(io.BytesIO):
|
||||||
|
"""A dynamically generated BytesIO-like object.
|
||||||
|
|
||||||
|
Original code by Matt Joiner <anacrolix@gmail.com> from:
|
||||||
|
|
||||||
|
* http://stackoverflow.com/questions/12593576/
|
||||||
|
* https://gist.github.com/anacrolix/3788413
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iterator):
|
||||||
|
#: Iterator/generator for content.
|
||||||
|
self._iter = iterator
|
||||||
|
|
||||||
|
#: Internal buffer.
|
||||||
|
self._left = b""
|
||||||
|
|
||||||
|
def readable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _read1(self, n=None):
|
||||||
|
while not self._left:
|
||||||
|
try:
|
||||||
|
self._left = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Make sure we handle text.
|
||||||
|
self._left = force_bytes(self._left)
|
||||||
|
ret = self._left[:n]
|
||||||
|
self._left = self._left[len(ret) :]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def read(self, n=None):
|
||||||
|
"""Return content up to ``n`` length."""
|
||||||
|
chunks = []
|
||||||
|
if n is None or n < 0:
|
||||||
|
while True:
|
||||||
|
m = self._read1()
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
chunks.append(m)
|
||||||
|
else:
|
||||||
|
while n > 0:
|
||||||
|
m = self._read1(n)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
n -= len(m)
|
||||||
|
chunks.append(m)
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
def readline(self):
|
||||||
|
chunks = []
|
||||||
|
while True:
|
||||||
|
i = self._left.find(b"\n")
|
||||||
|
if i == -1:
|
||||||
|
chunks.append(self._left)
|
||||||
|
try:
|
||||||
|
self._left = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
self._left = b""
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
chunks.append(self._left[: i + 1])
|
||||||
|
self._left = self._left[i + 1 :]
|
||||||
|
break
|
||||||
|
return b"".join(chunks)
|
||||||
14
django_downloadview/lighttpd/__init__.py
Normal file
14
django_downloadview/lighttpd/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"""Optimizations for Lighttpd.
|
||||||
|
|
||||||
|
See also `documentation of X-Sendfile for Lighttpd
|
||||||
|
<http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file>`_ and
|
||||||
|
:doc:`narrative documentation about Lighttpd optimizations
|
||||||
|
</optimizations/lighttpd>`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# API shortcuts.
|
||||||
|
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
|
||||||
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
from django_downloadview.lighttpd.response import XSendfileResponse # NoQA
|
||||||
|
from django_downloadview.lighttpd.tests import assert_x_sendfile # NoQA
|
||||||
16
django_downloadview/lighttpd/decorators.py
Normal file
16
django_downloadview/lighttpd/decorators.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
||||||
|
|
||||||
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
def x_sendfile(view_func, *args, **kwargs):
|
||||||
|
"""Apply
|
||||||
|
:class:`~django_downloadview.lighttpd.middlewares.XSendfileMiddleware` to
|
||||||
|
``view_func``.
|
||||||
|
|
||||||
|
Proxies (``*args``, ``**kwargs``) to middleware constructor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
decorator = DownloadDecorator(XSendfileMiddleware)
|
||||||
|
return decorator(view_func, *args, **kwargs)
|
||||||
36
django_downloadview/lighttpd/middlewares.py
Normal file
36
django_downloadview/lighttpd/middlewares.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
from django_downloadview.lighttpd.response import XSendfileResponse
|
||||||
|
from django_downloadview.middlewares import (
|
||||||
|
NoRedirectionMatch,
|
||||||
|
ProxiedDownloadMiddleware,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
|
"""Configurable middleware, for use in decorators or in global middlewares.
|
||||||
|
|
||||||
|
Standard Django middlewares are configured globally via settings. Instances
|
||||||
|
of this class are to be configured individually. It makes it possible to
|
||||||
|
use this class as the factory in
|
||||||
|
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||||
|
):
|
||||||
|
"""Constructor."""
|
||||||
|
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||||
|
|
||||||
|
def process_download_response(self, request, response):
|
||||||
|
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||||
|
try:
|
||||||
|
redirect_url = self.get_redirect_url(response)
|
||||||
|
except NoRedirectionMatch:
|
||||||
|
return response
|
||||||
|
return XSendfileResponse(
|
||||||
|
file_path=redirect_url,
|
||||||
|
content_type=response["Content-Type"],
|
||||||
|
basename=response.basename,
|
||||||
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
|
)
|
||||||
22
django_downloadview/lighttpd/response.py
Normal file
22
django_downloadview/lighttpd/response.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Lighttpd's specific responses."""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileResponse(ProxiedDownloadResponse):
|
||||||
|
"Delegates serving file to Lighttpd via X-Sendfile header."
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||||
|
):
|
||||||
|
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
|
||||||
|
# content-type must be porvided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
|
if attachment:
|
||||||
|
self.basename = basename or os.path.basename(file_path)
|
||||||
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
self["X-Sendfile"] = file_path
|
||||||
29
django_downloadview/lighttpd/tests.py
Normal file
29
django_downloadview/lighttpd/tests.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import django_downloadview.apache.tests
|
||||||
|
from django_downloadview.lighttpd.response import XSendfileResponse
|
||||||
|
|
||||||
|
|
||||||
|
class XSendfileValidator(django_downloadview.apache.tests.XSendfileValidator):
|
||||||
|
"""Utility class to validate XSendfileResponse instances.
|
||||||
|
|
||||||
|
See also :py:func:`assert_x_sendfile` shortcut function.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def assert_x_sendfile_response(self, test_case, response):
|
||||||
|
test_case.assertTrue(isinstance(response, XSendfileResponse))
|
||||||
|
|
||||||
|
|
||||||
|
def assert_x_sendfile(test_case, response, **assertions):
|
||||||
|
"""Make ``test_case`` assert that ``response`` is a XSendfileResponse.
|
||||||
|
|
||||||
|
Optional ``assertions`` dictionary can be used to check additional items:
|
||||||
|
|
||||||
|
* ``basename``: the basename of the file in the response.
|
||||||
|
|
||||||
|
* ``content_type``: the value of "Content-Type" header.
|
||||||
|
|
||||||
|
* ``file_path``: the value of "X-Sendfile" header.
|
||||||
|
|
||||||
|
"""
|
||||||
|
validator = XSendfileValidator()
|
||||||
|
return validator(test_case, response, **assertions)
|
||||||
|
|
@ -1,25 +1,62 @@
|
||||||
"""Base material for download middlewares."""
|
"""Base material for download middlewares.
|
||||||
from django_downloadview.response import is_download_response
|
|
||||||
|
Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
|
||||||
|
responses and may replace them with optimized download responses.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections.abc
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from django_downloadview.response import DownloadResponse
|
||||||
|
from django_downloadview.utils import import_member
|
||||||
|
|
||||||
|
|
||||||
class BaseDownloadMiddleware(object):
|
#: Sentinel value to detect whether configuration is to be loaded from Django
|
||||||
|
#: settings or not.
|
||||||
|
AUTO_CONFIGURE = object()
|
||||||
|
|
||||||
|
|
||||||
|
def is_download_response(response):
|
||||||
|
"""Return ``True`` if ``response`` is a download response.
|
||||||
|
|
||||||
|
Current implementation returns True if ``response`` is an instance of
|
||||||
|
:py:class:`django_downloadview.response.DownloadResponse`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return isinstance(response, DownloadResponse)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDownloadMiddleware:
|
||||||
"""Base (abstract) Django middleware that handles download responses.
|
"""Base (abstract) Django middleware that handles download responses.
|
||||||
|
|
||||||
Subclasses **must** implement :py:meth:`process_download_response` method.
|
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):
|
def is_download_response(self, response):
|
||||||
"""Return True if ``response`` can be considered as a file download.
|
"""Return True if ``response`` can be considered as a file download.
|
||||||
|
|
||||||
By default, this method uses
|
By default, this method uses
|
||||||
:py:func:`django_downloadview.response.is_download_response`.
|
:py:func:`django_downloadview.middlewares.is_download_response`.
|
||||||
Override this method if you want a different behaviour.
|
Override this method if you want a different behaviour.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return is_download_response(response)
|
return is_download_response(response)
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def process_response(self, request, response):
|
||||||
"""Call :py:meth:`process_download_response` if ``response`` is download."""
|
"""Call `process_download_response()` if ``response`` is download."""
|
||||||
if self.is_download_response(response):
|
if self.is_download_response(response):
|
||||||
return self.process_download_response(request, response)
|
return self.process_download_response(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
@ -27,3 +64,169 @@ class BaseDownloadMiddleware(object):
|
||||||
def process_download_response(self, request, response):
|
def process_download_response(self, request, response):
|
||||||
"""Handle file download response."""
|
"""Handle file download response."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class RealDownloadMiddleware(BaseDownloadMiddleware):
|
||||||
|
"""Download middleware that cannot handle virtual files."""
|
||||||
|
|
||||||
|
def is_download_response(self, response):
|
||||||
|
"""Return True for DownloadResponse, except for "virtual" files.
|
||||||
|
|
||||||
|
This implementation cannot handle files that live in memory or which
|
||||||
|
are to be dynamically iterated over. So, we capture only responses
|
||||||
|
whose file attribute have either an URL or a file name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return super().is_download_response(response) and bool(
|
||||||
|
getattr(response.file, "url", None) or getattr(response.file, "name", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadDispatcher:
|
||||||
|
def __init__(self, middlewares=AUTO_CONFIGURE):
|
||||||
|
#: List of children middlewares.
|
||||||
|
self.middlewares = middlewares
|
||||||
|
if self.middlewares is AUTO_CONFIGURE:
|
||||||
|
self.auto_configure_middlewares()
|
||||||
|
|
||||||
|
def auto_configure_middlewares(self):
|
||||||
|
"""Populate :attr:`middlewares` from
|
||||||
|
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
|
||||||
|
for key, import_string, kwargs in getattr(
|
||||||
|
settings, "DOWNLOADVIEW_MIDDLEWARES", []
|
||||||
|
):
|
||||||
|
factory = import_member(import_string)
|
||||||
|
middleware = factory(**kwargs)
|
||||||
|
self.middlewares.append((key, middleware))
|
||||||
|
|
||||||
|
def dispatch(self, request, response):
|
||||||
|
"""Dispatches job to children middlewares."""
|
||||||
|
for key, middleware in self.middlewares:
|
||||||
|
response = middleware.process_response(request, response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
||||||
|
"Download middleware that dispatches job to several middleware instances."
|
||||||
|
|
||||||
|
def __init__(self, get_response, middlewares=AUTO_CONFIGURE):
|
||||||
|
super().__init__(get_response)
|
||||||
|
self.dispatcher = DownloadDispatcher(middlewares)
|
||||||
|
|
||||||
|
def process_download_response(self, request, response):
|
||||||
|
return self.dispatcher.dispatch(request, response)
|
||||||
|
|
||||||
|
|
||||||
|
class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
|
||||||
|
"""Easy to configure download middleware."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
get_response,
|
||||||
|
backend_factory=AUTO_CONFIGURE,
|
||||||
|
backend_options=AUTO_CONFIGURE,
|
||||||
|
):
|
||||||
|
"""Constructor."""
|
||||||
|
super().__init__(get_response, middlewares=[])
|
||||||
|
#: Callable (typically a class) to instantiate backend (typically a
|
||||||
|
#: :class:`DownloadMiddleware` subclass).
|
||||||
|
self.backend_factory = backend_factory
|
||||||
|
if self.backend_factory is AUTO_CONFIGURE:
|
||||||
|
self.auto_configure_backend_factory()
|
||||||
|
#: List of positional or keyword arguments to instantiate backend
|
||||||
|
#: instances.
|
||||||
|
self.backend_options = backend_options
|
||||||
|
if self.backend_options is AUTO_CONFIGURE:
|
||||||
|
self.auto_configure_backend_options()
|
||||||
|
|
||||||
|
def auto_configure_backend_factory(self):
|
||||||
|
"Assign :attr:`backend_factory` from ``settings.DOWNLOADVIEW_BACKEND``"
|
||||||
|
try:
|
||||||
|
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_BACKEND"
|
||||||
|
)
|
||||||
|
|
||||||
|
def auto_configure_backend_options(self):
|
||||||
|
"""Populate :attr:`dispatcher` using :attr:`factory` and
|
||||||
|
``settings.DOWNLOADVIEW_RULES``."""
|
||||||
|
try:
|
||||||
|
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_RULES"
|
||||||
|
)
|
||||||
|
for key, options in enumerate(options_list):
|
||||||
|
args = []
|
||||||
|
kwargs = {}
|
||||||
|
if isinstance(options, collections.abc.Mapping): # Using kwargs.
|
||||||
|
kwargs = options
|
||||||
|
else:
|
||||||
|
args = options
|
||||||
|
if "backend" in kwargs: # Specific backend for this rule.
|
||||||
|
factory = import_member(kwargs["backend"])
|
||||||
|
del kwargs["backend"]
|
||||||
|
else: # Fallback to global backend.
|
||||||
|
factory = self.backend_factory
|
||||||
|
middleware_instance = factory(*args, **kwargs)
|
||||||
|
self.dispatcher.middlewares.append((key, middleware_instance))
|
||||||
|
|
||||||
|
|
||||||
|
class NoRedirectionMatch(Exception):
|
||||||
|
"""Response object does not match redirection rules."""
|
||||||
|
|
||||||
|
|
||||||
|
class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
||||||
|
"""Base class for middlewares that use optimizations of reverse proxies."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, get_response, source_dir=None, source_url=None, destination_url=None
|
||||||
|
):
|
||||||
|
"""Constructor."""
|
||||||
|
super().__init__(get_response)
|
||||||
|
|
||||||
|
self.source_dir = source_dir
|
||||||
|
self.source_url = source_url
|
||||||
|
self.destination_url = destination_url
|
||||||
|
|
||||||
|
def get_redirect_url(self, response):
|
||||||
|
"""Return redirect URL for file wrapped into response."""
|
||||||
|
url = None
|
||||||
|
file_url = ""
|
||||||
|
if self.source_url:
|
||||||
|
try:
|
||||||
|
file_url = response.file.url
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if file_url.startswith(self.source_url):
|
||||||
|
file_url = file_url[len(self.source_url) :]
|
||||||
|
url = file_url
|
||||||
|
file_name = ""
|
||||||
|
if url is None and self.source_dir:
|
||||||
|
try:
|
||||||
|
file_name = response.file.name
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if file_name.startswith(self.source_dir):
|
||||||
|
file_name = os.path.relpath(file_name, self.source_dir)
|
||||||
|
url = file_name.replace(os.path.sep, "/")
|
||||||
|
if url is None:
|
||||||
|
message = (
|
||||||
|
"""Couldn't capture/convert file attributes into a """
|
||||||
|
"""redirection. """
|
||||||
|
"""``source_url`` is "%(source_url)s", """
|
||||||
|
"""file's URL is "%(file_url)s". """
|
||||||
|
"""``source_dir`` is "%(source_dir)s", """
|
||||||
|
"""file's name is "%(file_name)s". """
|
||||||
|
% {
|
||||||
|
"source_url": self.source_url,
|
||||||
|
"file_url": file_url,
|
||||||
|
"source_dir": self.source_dir,
|
||||||
|
"file_name": file_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise NoRedirectionMatch(message)
|
||||||
|
return "/".join((self.destination_url.rstrip("/"), url.lstrip("/")))
|
||||||
|
|
|
||||||
|
|
@ -1,415 +0,0 @@
|
||||||
"""Optimizations for Nginx.
|
|
||||||
|
|
||||||
See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
|
||||||
:doc:`narrative documentation about Nginx optimizations
|
|
||||||
</optimizations/nginx>`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import os
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.http import HttpResponse
|
|
||||||
|
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
|
||||||
from django_downloadview.middlewares import BaseDownloadMiddleware
|
|
||||||
from django_downloadview.utils import content_type_to_charset
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for X-Accel-Buffering header.
|
|
||||||
#: Also default value for
|
|
||||||
#: ``settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``.
|
|
||||||
#:
|
|
||||||
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Buffering
|
|
||||||
#:
|
|
||||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
|
||||||
#: defaults or specific configuration.
|
|
||||||
#:
|
|
||||||
#: If set to ``False``, Nginx buffering is disabled.
|
|
||||||
#: If set to ``True``, Nginx buffering is enabled.
|
|
||||||
DEFAULT_WITH_BUFFERING = None
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING',
|
|
||||||
DEFAULT_WITH_BUFFERING)
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for X-Accel-Limit-Rate header.
|
|
||||||
#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``.
|
|
||||||
#:
|
|
||||||
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Rate
|
|
||||||
#:
|
|
||||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
|
||||||
#: defaults or specific configuration.
|
|
||||||
#:
|
|
||||||
#: If set to ``False``, Nginx limit rate is disabled.
|
|
||||||
#: Else, it indicates the limit rate in bytes.
|
|
||||||
DEFAULT_LIMIT_RATE = None
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE',
|
|
||||||
DEFAULT_LIMIT_RATE)
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for X-Accel-Limit-Expires header.
|
|
||||||
#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``.
|
|
||||||
#:
|
|
||||||
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Expires
|
|
||||||
#:
|
|
||||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
|
||||||
#: defaults or specific configuration.
|
|
||||||
#:
|
|
||||||
#: If set to ``False``, Nginx buffering is disabled.
|
|
||||||
#: Else, it indicates the expiration delay, in seconds.
|
|
||||||
DEFAULT_EXPIRES = None
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES', DEFAULT_EXPIRES)
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR.
|
|
||||||
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
|
|
||||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT'):
|
|
||||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is "
|
|
||||||
"deprecated, use "
|
|
||||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR instead.",
|
|
||||||
DeprecationWarning)
|
|
||||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR',
|
|
||||||
DEFAULT_SOURCE_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL.
|
|
||||||
DEFAULT_SOURCE_URL = settings.MEDIA_URL
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL',
|
|
||||||
DEFAULT_SOURCE_URL)
|
|
||||||
|
|
||||||
|
|
||||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL.
|
|
||||||
DEFAULT_DESTINATION_URL = None
|
|
||||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL'):
|
|
||||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is "
|
|
||||||
"deprecated, use "
|
|
||||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL "
|
|
||||||
"instead.",
|
|
||||||
DeprecationWarning)
|
|
||||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
|
||||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL'):
|
|
||||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL',
|
|
||||||
DEFAULT_DESTINATION_URL)
|
|
||||||
|
|
||||||
|
|
||||||
class XAccelRedirectResponse(HttpResponse):
|
|
||||||
"""Http response that delegates serving file to Nginx."""
|
|
||||||
def __init__(self, redirect_url, content_type, basename=None, expires=None,
|
|
||||||
with_buffering=None, limit_rate=None, attachment=True):
|
|
||||||
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
|
||||||
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
|
|
||||||
if attachment:
|
|
||||||
self.basename = basename or redirect_url.split('/')[-1]
|
|
||||||
self['Content-Disposition'] = 'attachment; filename={name}'.format(
|
|
||||||
name=self.basename)
|
|
||||||
self['X-Accel-Redirect'] = redirect_url
|
|
||||||
self['X-Accel-Charset'] = content_type_to_charset(content_type)
|
|
||||||
if with_buffering is not None:
|
|
||||||
self['X-Accel-Buffering'] = with_buffering and 'yes' or 'no'
|
|
||||||
if expires:
|
|
||||||
expire_seconds = timedelta(expires - datetime.now()).seconds
|
|
||||||
self['X-Accel-Expires'] = expire_seconds
|
|
||||||
elif expires is not None: # We explicitely want it off.
|
|
||||||
self['X-Accel-Expires'] = 'off'
|
|
||||||
if limit_rate is not None:
|
|
||||||
self['X-Accel-Limit-Rate'] = (limit_rate
|
|
||||||
and '%d' % limit_rate
|
|
||||||
or 'off')
|
|
||||||
|
|
||||||
|
|
||||||
class XAccelRedirectValidator(object):
|
|
||||||
"""Utility class to validate XAccelRedirectResponse instances.
|
|
||||||
|
|
||||||
See also :py:func:`assert_x_accel_redirect` shortcut function.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __call__(self, test_case, response, **assertions):
|
|
||||||
"""Assert that ``response`` is a valid X-Accel-Redirect response.
|
|
||||||
|
|
||||||
Optional ``assertions`` dictionary can be used to check additional
|
|
||||||
items:
|
|
||||||
|
|
||||||
* ``basename``: the basename of the file in the response.
|
|
||||||
|
|
||||||
* ``content_type``: the value of "Content-Type" header.
|
|
||||||
|
|
||||||
* ``redirect_url``: the value of "X-Accel-Redirect" header.
|
|
||||||
|
|
||||||
* ``charset``: the value of ``X-Accel-Charset`` header.
|
|
||||||
|
|
||||||
* ``with_buffering``: the value of ``X-Accel-Buffering`` header.
|
|
||||||
If ``False``, then makes sure that the header disables buffering.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
* ``expires``: the value of ``X-Accel-Expires`` header.
|
|
||||||
If ``False``, then makes sure that the header disables expiration.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
* ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header.
|
|
||||||
If ``False``, then makes sure that the header disables limit rate.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.assert_x_accel_redirect_response(test_case, response)
|
|
||||||
for key, value in assertions.iteritems():
|
|
||||||
assert_func = getattr(self, 'assert_%s' % key)
|
|
||||||
assert_func(test_case, response, value)
|
|
||||||
|
|
||||||
def assert_x_accel_redirect_response(self, test_case, response):
|
|
||||||
test_case.assertTrue(isinstance(response, XAccelRedirectResponse))
|
|
||||||
|
|
||||||
def assert_basename(self, test_case, response, value):
|
|
||||||
test_case.assertEqual(response.basename, value)
|
|
||||||
|
|
||||||
def assert_content_type(self, test_case, response, value):
|
|
||||||
test_case.assertEqual(response['Content-Type'], value)
|
|
||||||
|
|
||||||
def assert_redirect_url(self, test_case, response, value):
|
|
||||||
test_case.assertEqual(response['X-Accel-Redirect'], value)
|
|
||||||
|
|
||||||
def assert_charset(self, test_case, response, value):
|
|
||||||
test_case.assertEqual(response['X-Accel-Charset'], value)
|
|
||||||
|
|
||||||
def assert_with_buffering(self, test_case, response, value):
|
|
||||||
header = 'X-Accel-Buffering'
|
|
||||||
if value is None:
|
|
||||||
test_case.assertFalse(header in response)
|
|
||||||
elif value:
|
|
||||||
test_case.assertEqual(header, 'yes')
|
|
||||||
else:
|
|
||||||
test_case.assertEqual(header, 'no')
|
|
||||||
|
|
||||||
def assert_expires(self, test_case, response, value):
|
|
||||||
header = 'X-Accel-Expires'
|
|
||||||
if value is None:
|
|
||||||
test_case.assertFalse(header in response)
|
|
||||||
elif not value:
|
|
||||||
test_case.assertEqual(header, 'off')
|
|
||||||
else:
|
|
||||||
test_case.assertEqual(header, value)
|
|
||||||
|
|
||||||
def assert_limit_rate(self, test_case, response, value):
|
|
||||||
header = 'X-Accel-Limit-Rate'
|
|
||||||
if value is None:
|
|
||||||
test_case.assertFalse(header in response)
|
|
||||||
elif not value:
|
|
||||||
test_case.assertEqual(header, 'off')
|
|
||||||
else:
|
|
||||||
test_case.assertEqual(header, value)
|
|
||||||
|
|
||||||
def assert_attachment(self, test_case, response, value):
|
|
||||||
header = 'Content-Disposition'
|
|
||||||
if value:
|
|
||||||
test_case.assertTrue(response[header].startswith('attachment'))
|
|
||||||
else:
|
|
||||||
test_case.assertFalse(header in response)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_x_accel_redirect(test_case, response, **assertions):
|
|
||||||
"""Make ``test_case`` assert that ``response`` is a XAccelRedirectResponse.
|
|
||||||
|
|
||||||
Optional ``assertions`` dictionary can be used to check additional items:
|
|
||||||
|
|
||||||
* ``basename``: the basename of the file in the response.
|
|
||||||
|
|
||||||
* ``content_type``: the value of "Content-Type" header.
|
|
||||||
|
|
||||||
* ``redirect_url``: the value of "X-Accel-Redirect" header.
|
|
||||||
|
|
||||||
* ``charset``: the value of ``X-Accel-Charset`` header.
|
|
||||||
|
|
||||||
* ``with_buffering``: the value of ``X-Accel-Buffering`` header.
|
|
||||||
If ``False``, then makes sure that the header disables buffering.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
* ``expires``: the value of ``X-Accel-Expires`` header.
|
|
||||||
If ``False``, then makes sure that the header disables expiration.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
* ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header.
|
|
||||||
If ``False``, then makes sure that the header disables limit rate.
|
|
||||||
If ``None``, then makes sure that the header is not set.
|
|
||||||
|
|
||||||
"""
|
|
||||||
validator = XAccelRedirectValidator()
|
|
||||||
return validator(test_case, response, **assertions)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware):
|
|
||||||
"""Configurable middleware, for use in decorators or in global middlewares.
|
|
||||||
|
|
||||||
Standard Django middlewares are configured globally via settings. Instances
|
|
||||||
of this class are to be configured individually. It makes it possible to
|
|
||||||
use this class as the factory in
|
|
||||||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, source_dir=None, source_url=None, destination_url=None,
|
|
||||||
expires=None, with_buffering=None, limit_rate=None,
|
|
||||||
media_root=None, media_url=None):
|
|
||||||
"""Constructor."""
|
|
||||||
if media_url is not None:
|
|
||||||
warnings.warn("%s ``media_url`` is deprecated. Use "
|
|
||||||
"``destination_url`` instead."
|
|
||||||
% self.__class__.__name__,
|
|
||||||
DeprecationWarning)
|
|
||||||
if destination_url is None:
|
|
||||||
self.destination_url = media_url
|
|
||||||
else:
|
|
||||||
self.destination_url = destination_url
|
|
||||||
else:
|
|
||||||
self.destination_url = destination_url
|
|
||||||
if media_root is not None:
|
|
||||||
warnings.warn("%s ``media_root`` is deprecated. Use "
|
|
||||||
"``source_dir`` instead." % self.__class__.__name__,
|
|
||||||
DeprecationWarning)
|
|
||||||
if source_dir is None:
|
|
||||||
self.source_dir = media_root
|
|
||||||
else:
|
|
||||||
self.source_dir = source_dir
|
|
||||||
else:
|
|
||||||
self.source_dir = source_dir
|
|
||||||
self.source_url = source_url
|
|
||||||
self.expires = expires
|
|
||||||
self.with_buffering = with_buffering
|
|
||||||
self.limit_rate = limit_rate
|
|
||||||
|
|
||||||
def is_download_response(self, response):
|
|
||||||
"""Return True for DownloadResponse, except for "virtual" files.
|
|
||||||
|
|
||||||
This implementation can't handle files that live in memory or which are
|
|
||||||
to be dynamically iterated over. So, we capture only responses whose
|
|
||||||
file attribute have either an URL or a file name.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if super(BaseXAccelRedirectMiddleware,
|
|
||||||
self).is_download_response(response):
|
|
||||||
try:
|
|
||||||
response.file.url
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
response.file.name
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_redirect_url(self, response):
|
|
||||||
"""Return redirect URL for file wrapped into response."""
|
|
||||||
url = None
|
|
||||||
file_url = ''
|
|
||||||
if self.source_url is not None:
|
|
||||||
try:
|
|
||||||
file_url = response.file.url
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if file_url.startswith(self.source_url):
|
|
||||||
file_url = file_url[len(self.source_url):]
|
|
||||||
url = file_url
|
|
||||||
file_name = ''
|
|
||||||
if url is None and self.source_dir is not None:
|
|
||||||
try:
|
|
||||||
file_name = response.file.name
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if file_name.startswith(self.source_dir):
|
|
||||||
file_name = os.path.relpath(file_name, self.source_dir)
|
|
||||||
url = file_name.replace(os.path.sep, '/')
|
|
||||||
if url is None:
|
|
||||||
message = ("""Couldn't capture/convert file attributes into a """
|
|
||||||
"""redirection. """
|
|
||||||
"""``source_url`` is "%(source_url)s", """
|
|
||||||
"""file's URL is "%(file_url)s". """
|
|
||||||
"""``source_dir`` is "%(source_dir)s", """
|
|
||||||
"""file's name is "%(file_name)s". """
|
|
||||||
% {'source_url': self.source_url,
|
|
||||||
'file_url': file_url,
|
|
||||||
'source_dir': self.source_dir,
|
|
||||||
'file_name': file_name})
|
|
||||||
raise ImproperlyConfigured(message)
|
|
||||||
return '/'.join((self.destination_url.rstrip('/'), url.lstrip('/')))
|
|
||||||
|
|
||||||
def process_download_response(self, request, response):
|
|
||||||
"""Replace DownloadResponse instances by NginxDownloadResponse ones."""
|
|
||||||
redirect_url = self.get_redirect_url(response)
|
|
||||||
if self.expires:
|
|
||||||
expires = self.expires
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
expires = response.expires
|
|
||||||
except AttributeError:
|
|
||||||
expires = None
|
|
||||||
return XAccelRedirectResponse(redirect_url=redirect_url,
|
|
||||||
content_type=response['Content-Type'],
|
|
||||||
basename=response.basename,
|
|
||||||
expires=expires,
|
|
||||||
with_buffering=self.with_buffering,
|
|
||||||
limit_rate=self.limit_rate,
|
|
||||||
attachment=response.attachment)
|
|
||||||
|
|
||||||
|
|
||||||
class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware):
|
|
||||||
"""Apply X-Accel-Redirect globally, via Django settings.
|
|
||||||
|
|
||||||
Available settings are:
|
|
||||||
|
|
||||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL:
|
|
||||||
The string at the beginning of URLs to replace with
|
|
||||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
|
||||||
If ``None``, then URLs aren't captured.
|
|
||||||
Defaults to ``settings.MEDIA_URL``.
|
|
||||||
|
|
||||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR:
|
|
||||||
The string at the beginning of filenames (path) to replace with
|
|
||||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
|
||||||
If ``None``, then filenames aren't captured.
|
|
||||||
Defaults to ``settings.MEDIA_ROOT``.
|
|
||||||
|
|
||||||
NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL:
|
|
||||||
The base URL where requests are proxied to.
|
|
||||||
If ``None`` an ImproperlyConfigured exception is raised.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The following settings are deprecated since version 1.1.
|
|
||||||
URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT"
|
|
||||||
and "MEDIA_URL" became too confuse.
|
|
||||||
|
|
||||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT:
|
|
||||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``.
|
|
||||||
|
|
||||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL:
|
|
||||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
"""Use Django settings as configuration."""
|
|
||||||
if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is '
|
|
||||||
'required by %s middleware' % self.__class__.__name__)
|
|
||||||
super(XAccelRedirectMiddleware, self).__init__(
|
|
||||||
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
|
||||||
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
|
||||||
destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL,
|
|
||||||
expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES,
|
|
||||||
with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING,
|
|
||||||
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE)
|
|
||||||
|
|
||||||
|
|
||||||
#: Apply BaseXAccelRedirectMiddleware to ``view_func`` response.
|
|
||||||
#:
|
|
||||||
#: Proxies additional arguments (``*args``, ``**kwargs``) to
|
|
||||||
#: :py:class:`BaseXAccelRedirectMiddleware` constructor (``expires``,
|
|
||||||
#: ``with_buffering``, and ``limit_rate``).
|
|
||||||
x_accel_redirect = DownloadDecorator(BaseXAccelRedirectMiddleware)
|
|
||||||
13
django_downloadview/nginx/__init__.py
Normal file
13
django_downloadview/nginx/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Optimizations for Nginx.
|
||||||
|
|
||||||
|
See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||||
|
:doc:`narrative documentation about Nginx optimizations
|
||||||
|
</optimizations/nginx>`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# API shortcuts.
|
||||||
|
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
||||||
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
|
||||||
|
from django_downloadview.nginx.response import XAccelRedirectResponse # NoQA
|
||||||
|
from django_downloadview.nginx.tests import assert_x_accel_redirect # NoQA
|
||||||
16
django_downloadview/nginx/decorators.py
Normal file
16
django_downloadview/nginx/decorators.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Decorators to apply Nginx X-Accel on a specific view."""
|
||||||
|
|
||||||
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
def x_accel_redirect(view_func, *args, **kwargs):
|
||||||
|
"""Apply
|
||||||
|
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware` to
|
||||||
|
``view_func``.
|
||||||
|
|
||||||
|
Proxies (``*args``, ``**kwargs``) to middleware constructor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
decorator = DownloadDecorator(XAccelRedirectMiddleware)
|
||||||
|
return decorator(view_func, *args, **kwargs)
|
||||||
142
django_downloadview/nginx/middlewares.py
Normal file
142
django_downloadview/nginx/middlewares.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from django_downloadview.middlewares import (
|
||||||
|
NoRedirectionMatch,
|
||||||
|
ProxiedDownloadMiddleware,
|
||||||
|
)
|
||||||
|
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||||
|
|
||||||
|
|
||||||
|
class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
||||||
|
"""Configurable middleware, for use in decorators or in global middlewares.
|
||||||
|
|
||||||
|
Standard Django middlewares are configured globally via settings. Instances
|
||||||
|
of this class are to be configured individually. It makes it possible to
|
||||||
|
use this class as the factory in
|
||||||
|
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
get_response=None,
|
||||||
|
source_dir=None,
|
||||||
|
source_url=None,
|
||||||
|
destination_url=None,
|
||||||
|
expires=None,
|
||||||
|
with_buffering=None,
|
||||||
|
limit_rate=None,
|
||||||
|
media_root=None,
|
||||||
|
media_url=None,
|
||||||
|
):
|
||||||
|
"""Constructor."""
|
||||||
|
if media_url is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"%s ``media_url`` is deprecated. Use "
|
||||||
|
"``destination_url`` instead." % self.__class__.__name__,
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
if destination_url is None:
|
||||||
|
destination_url = media_url
|
||||||
|
else:
|
||||||
|
destination_url = destination_url
|
||||||
|
else:
|
||||||
|
destination_url = destination_url
|
||||||
|
if media_root is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"%s ``media_root`` is deprecated. Use "
|
||||||
|
"``source_dir`` instead." % self.__class__.__name__,
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
if source_dir is None:
|
||||||
|
source_dir = media_root
|
||||||
|
else:
|
||||||
|
source_dir = source_dir
|
||||||
|
else:
|
||||||
|
source_dir = source_dir
|
||||||
|
|
||||||
|
super().__init__(get_response, source_dir, source_url, destination_url)
|
||||||
|
|
||||||
|
self.expires = expires
|
||||||
|
self.with_buffering = with_buffering
|
||||||
|
self.limit_rate = limit_rate
|
||||||
|
|
||||||
|
def process_download_response(self, request, response):
|
||||||
|
"""Replace DownloadResponse instances by NginxDownloadResponse ones."""
|
||||||
|
try:
|
||||||
|
redirect_url = self.get_redirect_url(response)
|
||||||
|
except NoRedirectionMatch:
|
||||||
|
return response
|
||||||
|
if self.expires:
|
||||||
|
expires = self.expires
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
expires = response.expires
|
||||||
|
except AttributeError:
|
||||||
|
expires = None
|
||||||
|
return XAccelRedirectResponse(
|
||||||
|
redirect_url=redirect_url,
|
||||||
|
content_type=response["Content-Type"],
|
||||||
|
basename=response.basename,
|
||||||
|
expires=expires,
|
||||||
|
with_buffering=self.with_buffering,
|
||||||
|
limit_rate=self.limit_rate,
|
||||||
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware):
|
||||||
|
"""Apply X-Accel-Redirect globally, via Django settings.
|
||||||
|
|
||||||
|
Available settings are:
|
||||||
|
|
||||||
|
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL:
|
||||||
|
The string at the beginning of URLs to replace with
|
||||||
|
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||||
|
If ``None``, then URLs aren't captured.
|
||||||
|
Defaults to ``settings.MEDIA_URL``.
|
||||||
|
|
||||||
|
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR:
|
||||||
|
The string at the beginning of filenames (path) to replace with
|
||||||
|
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||||
|
If ``None``, then filenames aren't captured.
|
||||||
|
Defaults to ``settings.MEDIA_ROOT``.
|
||||||
|
|
||||||
|
NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL:
|
||||||
|
The base URL where requests are proxied to.
|
||||||
|
If ``None`` an ImproperlyConfigured exception is raised.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The following settings are deprecated since version 1.1.
|
||||||
|
URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT"
|
||||||
|
and "MEDIA_URL" became too confuse.
|
||||||
|
|
||||||
|
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT:
|
||||||
|
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``.
|
||||||
|
|
||||||
|
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL:
|
||||||
|
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response=None):
|
||||||
|
"""Use Django settings as configuration."""
|
||||||
|
if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is "
|
||||||
|
"required by %s middleware" % self.__class__.__name__
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
get_response=get_response,
|
||||||
|
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
||||||
|
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
||||||
|
destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL,
|
||||||
|
expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES,
|
||||||
|
with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING,
|
||||||
|
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE,
|
||||||
|
)
|
||||||
43
django_downloadview/nginx/response.py
Normal file
43
django_downloadview/nginx/response.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""Nginx's specific responses."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
||||||
|
from django_downloadview.utils import content_type_to_charset, url_basename
|
||||||
|
|
||||||
|
|
||||||
|
class XAccelRedirectResponse(ProxiedDownloadResponse):
|
||||||
|
"Http response that delegates serving file to Nginx via X-Accel headers."
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
redirect_url,
|
||||||
|
content_type,
|
||||||
|
basename=None,
|
||||||
|
expires=None,
|
||||||
|
with_buffering=None,
|
||||||
|
limit_rate=None,
|
||||||
|
attachment=True,
|
||||||
|
headers=None,
|
||||||
|
):
|
||||||
|
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
||||||
|
# content-type must be porvided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
|
if attachment:
|
||||||
|
self.basename = basename or url_basename(redirect_url, content_type)
|
||||||
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
self["X-Accel-Redirect"] = redirect_url
|
||||||
|
self["X-Accel-Charset"] = content_type_to_charset(content_type)
|
||||||
|
if with_buffering is not None:
|
||||||
|
self["X-Accel-Buffering"] = with_buffering and "yes" or "no"
|
||||||
|
if expires:
|
||||||
|
expire_seconds = timedelta(expires - now()).seconds
|
||||||
|
self["X-Accel-Expires"] = expire_seconds
|
||||||
|
elif expires is not None: # We explicitely want it off.
|
||||||
|
self["X-Accel-Expires"] = "off"
|
||||||
|
if limit_rate is not None:
|
||||||
|
self["X-Accel-Limit-Rate"] = limit_rate and "%d" % limit_rate or "off"
|
||||||
124
django_downloadview/nginx/settings.py
Normal file
124
django_downloadview/nginx/settings.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""Django settings around Nginx X-Accel.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
These settings are deprecated since version 1.3. You can now provide custom
|
||||||
|
configuration via `DOWNLOADVIEW_BACKEND` setting. See :doc:`/settings`
|
||||||
|
for details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
# In version 1.3, former XAccelRedirectMiddleware has been renamed to
|
||||||
|
# SingleXAccelRedirectMiddleware. So tell the users.
|
||||||
|
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
||||||
|
|
||||||
|
|
||||||
|
if deprecated_middleware in settings.MIDDLEWARE:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"{deprecated_middleware} middleware has been renamed as of "
|
||||||
|
"django-downloadview version 1.3. You may use "
|
||||||
|
'"django_downloadview.nginx.SingleXAccelRedirectMiddleware" instead, '
|
||||||
|
'or upgrade to "django_downloadview.SmartDownloadDispatcher". '
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
deprecated_msg = (
|
||||||
|
"settings.{deprecated} is deprecated. You should combine "
|
||||||
|
'"django_downloadview.SmartDownloadDispatcher" with '
|
||||||
|
"with DOWNLOADVIEW_BACKEND and DOWNLOADVIEW_RULES instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for X-Accel-Buffering header.
|
||||||
|
#: Also default value for
|
||||||
|
#: ``settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``.
|
||||||
|
#:
|
||||||
|
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Buffering
|
||||||
|
#:
|
||||||
|
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||||
|
#: defaults or specific configuration.
|
||||||
|
#:
|
||||||
|
#: If set to ``False``, Nginx buffering is disabled.
|
||||||
|
#: If set to ``True``, Nginx buffering is enabled.
|
||||||
|
DEFAULT_WITH_BUFFERING = None
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_WITH_BUFFERING)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for X-Accel-Limit-Rate header.
|
||||||
|
#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``.
|
||||||
|
#:
|
||||||
|
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Rate
|
||||||
|
#:
|
||||||
|
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||||
|
#: defaults or specific configuration.
|
||||||
|
#:
|
||||||
|
#: If set to ``False``, Nginx limit rate is disabled.
|
||||||
|
#: Else, it indicates the limit rate in bytes.
|
||||||
|
DEFAULT_LIMIT_RATE = None
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_LIMIT_RATE)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for X-Accel-Limit-Expires header.
|
||||||
|
#: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``.
|
||||||
|
#:
|
||||||
|
#: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Expires
|
||||||
|
#:
|
||||||
|
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||||
|
#: defaults or specific configuration.
|
||||||
|
#:
|
||||||
|
#: If set to ``False``, Nginx buffering is disabled.
|
||||||
|
#: Else, it indicates the expiration delay, in seconds.
|
||||||
|
DEFAULT_EXPIRES = None
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_EXPIRES)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR.
|
||||||
|
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_SOURCE_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL.
|
||||||
|
DEFAULT_SOURCE_URL = settings.MEDIA_URL
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_SOURCE_URL)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL.
|
||||||
|
DEFAULT_DESTINATION_URL = None
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
||||||
|
setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL"
|
||||||
|
if hasattr(settings, setting_name):
|
||||||
|
warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning)
|
||||||
|
if not hasattr(settings, setting_name):
|
||||||
|
setattr(settings, setting_name, DEFAULT_DESTINATION_URL)
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue