mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
Compare commits
134 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969fb7931a | ||
|
|
fe610b9c9f | ||
|
|
187787d083 | ||
|
|
614bc522d3 | ||
|
|
34a83fd09b | ||
|
|
415b76622e | ||
|
|
982432a05b | ||
|
|
055ef690d2 | ||
|
|
94b4a60917 | ||
|
|
9194e6a11c | ||
|
|
140b631d5b | ||
|
|
e3ebf67457 | ||
|
|
5f2ae9a9b0 | ||
|
|
f1a07c226e | ||
|
|
9b90a3326d | ||
|
|
45fb86b52c | ||
|
|
5dc018f17b | ||
|
|
d515804708 | ||
|
|
4e580980fe | ||
|
|
9b77252326 | ||
|
|
f9cc2c5fab | ||
|
|
ec0b34df5b | ||
|
|
9e22eb8d8f | ||
|
|
c239015d1a | ||
|
|
73f9c013cd | ||
|
|
840827da1d | ||
|
|
31e64b6a76 | ||
|
|
71a1670703 | ||
|
|
dff487aca9 | ||
|
|
7aa9b687aa | ||
|
|
b02e2f13f9 | ||
|
|
a7d182f0b4 | ||
|
|
c8486417bb | ||
|
|
ebcd3a0028 | ||
|
|
6c0e0a8c82 | ||
|
|
d3a8f6b725 | ||
|
|
13a502bc4a | ||
|
|
9f42cde8cb | ||
|
|
5c8bbda4b3 | ||
|
|
eab4fa7abf | ||
|
|
711b2e50b5 | ||
|
|
dd35f867e0 | ||
|
|
402c77b332 | ||
|
|
347f0202ab | ||
|
|
18cb41f760 | ||
|
|
1b294f00fa | ||
|
|
51deef0a7e | ||
|
|
e7e25e68dd | ||
|
|
0568c3c559 | ||
|
|
e2b4470c5b | ||
|
|
71488c49c4 | ||
|
|
6c7b7f8a31 | ||
|
|
d385cbba6f | ||
|
|
60c1839bf5 | ||
|
|
ba6be8c3cd | ||
|
|
16b241d9b5 | ||
|
|
c51720296a | ||
|
|
fba10f7b1b | ||
|
|
41caa79f46 | ||
|
|
ff5073d00b | ||
|
|
338e17195f | ||
|
|
df439fbd4f | ||
|
|
dd2e148b05 | ||
|
|
64e36826ff | ||
|
|
d19e4bee50 | ||
|
|
8c74a77ebe | ||
|
|
a6975d9669 | ||
|
|
f715f72032 | ||
|
|
293403b807 | ||
|
|
b64b1ad21a | ||
|
|
3c2951ceac | ||
|
|
aa003ed6bf | ||
|
|
6dbf06c4ea | ||
|
|
7c2af759c8 | ||
|
|
7e9f81d758 | ||
|
|
e9fbb74b2c | ||
|
|
6381dc94f1 | ||
|
|
198f6a3295 | ||
|
|
a64a0e8c33 | ||
|
|
0ab8aa3e8f | ||
|
|
2524668e86 | ||
|
|
cb3ec3a091 | ||
|
|
95b36fc843 | ||
|
|
0578729a66 | ||
|
|
051f6116f8 | ||
|
|
ca8331e2de | ||
|
|
a8d231cbed | ||
|
|
006d2a288d | ||
|
|
cbd53d813e | ||
|
|
3543a3b8fc | ||
|
|
465123b6e5 | ||
|
|
45e1219275 | ||
|
|
a06c511d8f | ||
|
|
a67f3dc41e | ||
|
|
9c01be6cc8 | ||
|
|
563b2a4f7b | ||
|
|
707c392f6e | ||
|
|
9b5fee2687 | ||
|
|
ed3d470908 | ||
|
|
eb223f169b | ||
|
|
700b1246df | ||
|
|
2586cc5a97 | ||
|
|
c602c32d69 | ||
|
|
8fd75de224 | ||
|
|
87daf8e92e | ||
|
|
4dae54179b | ||
|
|
d1972d6460 | ||
|
|
cee7810cfd | ||
|
|
8df2615a29 | ||
|
|
252e6f127c | ||
|
|
cc4636098b | ||
|
|
f225d87b85 | ||
|
|
deddd2fd2d | ||
|
|
6338f61767 | ||
|
|
9f42e65986 | ||
|
|
384e7c5b13 | ||
|
|
0c59d6d261 | ||
|
|
13d2b3ae58 | ||
|
|
f663fa4b03 | ||
|
|
7ec465d74d | ||
|
|
3bb1a73e6b | ||
|
|
c0acf77f73 | ||
|
|
d74e094fe7 | ||
|
|
3da22537df | ||
|
|
91be5c38a8 | ||
|
|
8e439d3485 | ||
|
|
6cd9ec3dd0 | ||
|
|
bd41e6f7bf | ||
|
|
e2da75ec9d | ||
|
|
cd37fd5084 | ||
|
|
bc145254ea | ||
|
|
79da070b14 | ||
|
|
aacb5c7a16 | ||
|
|
965218aafb |
103 changed files with 971 additions and 648 deletions
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: github.repository == 'jazzband/django-downloadview'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install -U pip
|
||||||
|
python -m pip install -U setuptools twine wheel
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
python setup.py --version
|
||||||
|
python setup.py sdist --format=gztar bdist_wheel
|
||||||
|
twine check dist/*
|
||||||
|
|
||||||
|
- name: Upload packages to Jazzband
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
user: jazzband
|
||||||
|
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
|
||||||
|
repository_url: https://jazzband.co/projects/django-downloadview/upload
|
||||||
61
.github/workflows/test.yml
vendored
Normal file
61
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
|
||||||
|
django-version: ['4.2', '5.0', '5.1', '5.2', 'main']
|
||||||
|
exclude:
|
||||||
|
# Django 5.0 dropped support for Python <3.10
|
||||||
|
- django-version: '5.0'
|
||||||
|
python-version: '3.8'
|
||||||
|
- django-version: '5.0'
|
||||||
|
python-version: '3.9'
|
||||||
|
- django-version: 'main'
|
||||||
|
python-version: '3.8'
|
||||||
|
- django-version: 'main'
|
||||||
|
python-version: '3.9'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key:
|
||||||
|
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ matrix.python-version }}-v1-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install --upgrade tox tox-gh-actions
|
||||||
|
|
||||||
|
- name: Tox tests
|
||||||
|
run: |
|
||||||
|
tox -v
|
||||||
|
env:
|
||||||
|
DJANGO: ${{ matrix.django-version }}
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
name: Python ${{ matrix.python-version }}
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
# Data files.
|
# Data files.
|
||||||
/var/
|
/var/
|
||||||
|
coverage.xml
|
||||||
|
.coverage/
|
||||||
|
|
||||||
# Python files.
|
# Python files.
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
@ -14,11 +16,17 @@
|
||||||
|
|
||||||
# Tox files.
|
# Tox files.
|
||||||
/.tox/
|
/.tox/
|
||||||
|
.eggs
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
# Virtualenv files (created by tox).
|
# Virtualenv files (created by tox).
|
||||||
/build/
|
/build/
|
||||||
/dist/
|
/dist/
|
||||||
|
|
||||||
|
# Virtual environments (created by user).
|
||||||
|
/venv/
|
||||||
|
|
||||||
# Editors' temporary buffers.
|
# Editors' temporary buffers.
|
||||||
.*.swp
|
.*.swp
|
||||||
*~
|
*~
|
||||||
|
.idea
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ force_grid_wrap=0
|
||||||
line_length=88
|
line_length=88
|
||||||
combine_as_imports=True
|
combine_as_imports=True
|
||||||
|
|
||||||
# List sections with django and
|
# List sections with django and
|
||||||
known_django=django
|
known_django=django
|
||||||
known_downloadview=django_downloadview
|
known_downloadview=django_downloadview
|
||||||
|
|
||||||
|
|
|
||||||
59
.pre-commit-config.yaml
Normal file
59
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: mixed-line-ending
|
||||||
|
- id: file-contents-sorter
|
||||||
|
files: docs/spelling_wordlist.txt
|
||||||
|
- repo: https://github.com/pycqa/doc8
|
||||||
|
rev: v2.0.0
|
||||||
|
hooks:
|
||||||
|
- id: doc8
|
||||||
|
- repo: https://github.com/adamchainz/django-upgrade
|
||||||
|
rev: 1.29.1
|
||||||
|
hooks:
|
||||||
|
- id: django-upgrade
|
||||||
|
args: [--target-version, "4.2"]
|
||||||
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
|
rev: v1.10.0
|
||||||
|
hooks:
|
||||||
|
- id: rst-backticks
|
||||||
|
- id: rst-directive-colons
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
entry: env PRETTIER_LEGACY_CLI=1 prettier
|
||||||
|
types_or: [javascript, css]
|
||||||
|
args:
|
||||||
|
- --trailing-comma=es5
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v9.39.1
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
additional_dependencies:
|
||||||
|
- "eslint@v9.0.0-beta.1"
|
||||||
|
- "@eslint/js@v9.0.0-beta.1"
|
||||||
|
- "globals"
|
||||||
|
files: \.js?$
|
||||||
|
types: [file]
|
||||||
|
args:
|
||||||
|
- --fix
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: 'v0.14.5'
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
|
rev: v2.11.1
|
||||||
|
hooks:
|
||||||
|
- id: pyproject-fmt
|
||||||
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
|
rev: v0.24.1
|
||||||
|
hooks:
|
||||||
|
- id: validate-pyproject
|
||||||
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.10"
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
11
.travis.yml
11
.travis.yml
|
|
@ -1,11 +0,0 @@
|
||||||
language: python
|
|
||||||
dist: bionic
|
|
||||||
python:
|
|
||||||
- 3.6
|
|
||||||
- 3.7
|
|
||||||
- 3.8
|
|
||||||
install:
|
|
||||||
- pip install tox
|
|
||||||
- pip install -q tox-travis
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
28
AUTHORS
28
AUTHORS
|
|
@ -4,13 +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>
|
|
||||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
|
||||||
* Benoît Bryon <benoit@marmelune.net>
|
* Benoît Bryon <benoit@marmelune.net>
|
||||||
* Aleksi Häkli <https://github.com/aleksihakli>
|
* CJ <cjdreiss@users.noreply.github.com>
|
||||||
|
* David Wolf <68775926+devidw@users.noreply.github.com>
|
||||||
Developers: https://github.com/benoitbryon/django-downloadview/graphs/contributors
|
* 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>
|
||||||
|
|
|
||||||
41
CHANGELOG
41
CHANGELOG
|
|
@ -4,6 +4,45 @@ Changelog
|
||||||
This document describes changes between past releases. For information about
|
This document describes changes between past releases. For information about
|
||||||
future releases, check `milestones`_ and :doc:`/about/vision`.
|
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)
|
2.1 (2020-01-13)
|
||||||
----------------
|
----------------
|
||||||
|
|
@ -227,4 +266,4 @@ Contains **backward incompatible changes.**
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`milestones`: https://github.com/benoitbryon/django-downloadview/milestones
|
.. _`milestones`: https://github.com/jazzband/django-downloadview/milestones
|
||||||
|
|
|
||||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of the Jazzband projects, and in the interest of
|
||||||
|
fostering an open and welcoming community, we pledge to respect all people who
|
||||||
|
contribute through reporting issues, posting feature requests, updating documentation,
|
||||||
|
submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in the Jazzband a harassment-free experience
|
||||||
|
for everyone, regardless of the level of experience, gender, gender identity and
|
||||||
|
expression, sexual orientation, disability, personal appearance, body size, race,
|
||||||
|
ethnicity, age, religion, or nationality.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery
|
||||||
|
- Personal attacks
|
||||||
|
- Trolling or insulting/derogatory comments
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing other's private information, such as physical or electronic addresses,
|
||||||
|
without explicit permission
|
||||||
|
- Other unethical or unprofessional conduct
|
||||||
|
|
||||||
|
The Jazzband roadies have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are not
|
||||||
|
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
|
||||||
|
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
By adopting this Code of Conduct, the roadies commit themselves to fairly and
|
||||||
|
consistently applying these principles to every aspect of managing the jazzband
|
||||||
|
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
|
||||||
|
removed from the Jazzband roadies.
|
||||||
|
|
||||||
|
This code of conduct applies both within project spaces and in public spaces when an
|
||||||
|
individual is representing the project or its community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||||
|
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
|
||||||
|
investigated and will result in a response that is deemed necessary and appropriate to
|
||||||
|
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
|
||||||
|
reporter of an incident.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
|
||||||
|
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
|
||||||
|
|
||||||
|
[homepage]: https://contributor-covenant.org
|
||||||
|
[version]: https://contributor-covenant.org/version/1/3/0/
|
||||||
|
|
@ -2,8 +2,16 @@
|
||||||
Contributing
|
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
|
This document provides guidelines for people who want to contribute to
|
||||||
`django-downloadview`.
|
``django-downloadview``.
|
||||||
|
|
||||||
|
|
||||||
**************
|
**************
|
||||||
|
|
@ -42,11 +50,11 @@ Use topic branches
|
||||||
Fork, clone
|
Fork, clone
|
||||||
***********
|
***********
|
||||||
|
|
||||||
Clone `django-downloadview` repository (adapt to use your own fork):
|
Clone ``django-downloadview`` repository (adapt to use your own fork):
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
git clone git@github.com:benoitbryon/django-downloadview.git
|
git clone git@github.com:jazzband/django-downloadview.git
|
||||||
cd django-downloadview/
|
cd django-downloadview/
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,7 +62,7 @@ Clone `django-downloadview` repository (adapt to use your own fork):
|
||||||
Usual actions
|
Usual actions
|
||||||
*************
|
*************
|
||||||
|
|
||||||
The `Makefile` is the reference card for usual actions in development
|
The ``Makefile`` is the reference card for usual actions in development
|
||||||
environment:
|
environment:
|
||||||
|
|
||||||
* Install development toolkit with `pip`_: ``make develop``.
|
* Install development toolkit with `pip`_: ``make develop``.
|
||||||
|
|
@ -62,7 +70,7 @@ environment:
|
||||||
* Run tests with `tox`_: ``make test``.
|
* Run tests with `tox`_: ``make test``.
|
||||||
|
|
||||||
* Build documentation: ``make documentation``. It builds `Sphinx`_
|
* Build documentation: ``make documentation``. It builds `Sphinx`_
|
||||||
documentation in `var/docs/html/index.html`.
|
documentation in ``var/docs/html/index.html``.
|
||||||
|
|
||||||
* Release project with `zest.releaser`_: ``make release``.
|
* Release project with `zest.releaser`_: ``make release``.
|
||||||
|
|
||||||
|
|
@ -76,7 +84,7 @@ See also ``make help``.
|
||||||
Demo project included
|
Demo project included
|
||||||
*********************
|
*********************
|
||||||
|
|
||||||
The `demo` included in project's repository is part of the tests and
|
The ``demo`` included in project's repository is part of the tests and
|
||||||
documentation. Maintain it along with code and documentation.
|
documentation. Maintain it along with code and documentation.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,8 +92,8 @@ documentation. Maintain it along with code and documentation.
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`bugtracker`:
|
.. _`bugtracker`:
|
||||||
https://github.com/benoitbryon/django-downloadview/issues
|
https://github.com/jazzband/django-downloadview/issues
|
||||||
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
|
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
|
||||||
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html
|
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html
|
||||||
.. _`pip`: https://pypi.python.org/pypi/pip/
|
.. _`pip`: https://pypi.python.org/pypi/pip/
|
||||||
|
|
|
||||||
4
INSTALL
4
INSTALL
|
|
@ -12,8 +12,8 @@ Install
|
||||||
Requirements
|
Requirements
|
||||||
************
|
************
|
||||||
|
|
||||||
`django-downloadview` has been tested with `Python`_ 3.6, 3.7 and 3.8. Other
|
`django-downloadview` has been tested with `Python`_ 3.7, 3.8, 3.9 and 3.10.
|
||||||
versions may work, but they are not part of the test suite at the moment.
|
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
|
Installing `django-downloadview` will automatically trigger the installation of
|
||||||
the following requirements:
|
the following requirements:
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,3 @@ include CONTRIBUTING.rst
|
||||||
include INSTALL
|
include INSTALL
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
include README.rst
|
||||||
include VERSION
|
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -92,12 +92,6 @@ demo:
|
||||||
runserver: demo
|
runserver: demo
|
||||||
demo runserver
|
demo runserver
|
||||||
|
|
||||||
|
|
||||||
#: release - Tag and push to PyPI.
|
|
||||||
.PHONY: release
|
|
||||||
release:
|
|
||||||
$(TOX) -e release
|
|
||||||
|
|
||||||
.PHONY: black
|
.PHONY: black
|
||||||
black:
|
black:
|
||||||
$(BLACK) demo tests django_downloadview
|
$(BLACK) demo tests django_downloadview
|
||||||
|
|
|
||||||
39
README.rst
39
README.rst
|
|
@ -2,16 +2,40 @@
|
||||||
django-downloadview
|
django-downloadview
|
||||||
###################
|
###################
|
||||||
|
|
||||||
`django-downloadview` makes it easy to serve files with `Django`_:
|
.. image:: https://jazzband.co/static/img/badge.svg
|
||||||
|
:target: https://jazzband.co/
|
||||||
|
:alt: Jazzband
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/djversions/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/dm/django-downloadview.svg
|
||||||
|
:target: https://pypi.python.org/pypi/django-downloadview
|
||||||
|
|
||||||
|
.. image:: https://github.com/jazzband/django-downloadview/workflows/Test/badge.svg
|
||||||
|
:target: https://github.com/jazzband/django-downloadview/actions
|
||||||
|
:alt: GitHub Actions
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/gh/jazzband/django-downloadview/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/jazzband/django-downloadview
|
||||||
|
:alt: Coverage
|
||||||
|
|
||||||
|
``django-downloadview`` makes it easy to serve files with `Django`_:
|
||||||
|
|
||||||
* you manage files with Django (permissions, filters, generation, ...);
|
* you manage files with Django (permissions, filters, generation, ...);
|
||||||
|
|
||||||
* files are stored somewhere or generated somehow (local filesystem, remote
|
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||||
storage, memory...);
|
storage, memory...);
|
||||||
|
|
||||||
* `django-downloadview` helps you stream the files with very little code;
|
* ``django-downloadview`` helps you stream the files with very little code;
|
||||||
|
|
||||||
* `django-downloadview` helps you improve performances with reverse proxies,
|
* ``django-downloadview`` helps you improve performances with reverse proxies,
|
||||||
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.
|
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,10 +65,9 @@ Resources
|
||||||
|
|
||||||
* Documentation: https://django-downloadview.readthedocs.io
|
* Documentation: https://django-downloadview.readthedocs.io
|
||||||
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
* PyPI page: http://pypi.python.org/pypi/django-downloadview
|
||||||
* Code repository: https://github.com/benoitbryon/django-downloadview
|
* Code repository: https://github.com/jazzband/django-downloadview
|
||||||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
* Bugtracker: https://github.com/jazzband/django-downloadview/issues
|
||||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
* Continuous integration: https://github.com/jazzband/django-downloadview/actions
|
||||||
* Roadmap: https://github.com/benoitbryon/django-downloadview/milestones
|
* Roadmap: https://github.com/jazzband/django-downloadview/milestones
|
||||||
|
|
||||||
|
|
||||||
.. _`Django`: https://djangoproject.com
|
.. _`Django`: https://djangoproject.com
|
||||||
|
|
|
||||||
1
VERSION
1
VERSION
|
|
@ -1 +0,0 @@
|
||||||
2.1
|
|
||||||
|
|
@ -3,7 +3,7 @@ Demo project
|
||||||
############
|
############
|
||||||
|
|
||||||
`Demo folder in project's repository`_ contains a Django project to illustrate
|
`Demo folder in project's repository`_ contains a Django project to illustrate
|
||||||
`django-downloadview` usage.
|
``django-downloadview`` usage.
|
||||||
|
|
||||||
|
|
||||||
*****************************************
|
*****************************************
|
||||||
|
|
@ -31,8 +31,8 @@ Deploy the demo
|
||||||
|
|
||||||
System requirements:
|
System requirements:
|
||||||
|
|
||||||
* `Python`_ version 2.7, available as ``python`` command.
|
* `Python`_ version 3.7+, available as ``python`` command.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
You may use `Virtualenv`_ to make sure the active ``python`` is the right
|
You may use `Virtualenv`_ to make sure the active ``python`` is the right
|
||||||
|
|
@ -44,7 +44,7 @@ Execute:
|
||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
git clone git@github.com:benoitbryon/django-downloadview.git
|
git clone git@github.com:jazzband/django-downloadview.git
|
||||||
cd django-downloadview/
|
cd django-downloadview/
|
||||||
make runserver
|
make runserver
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ References
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`demo folder in project's repository`:
|
.. _`demo folder in project's repository`:
|
||||||
https://github.com/benoitbryon/django-downloadview/tree/master/demo/demoproject/
|
https://github.com/jazzband/django-downloadview/tree/master/demo/demoproject/
|
||||||
|
|
||||||
.. _`Python`: http://python.org
|
.. _`Python`: http://python.org
|
||||||
.. _`Virtualenv`: http://virtualenv.org
|
.. _`Virtualenv`: http://virtualenv.org
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import os
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview.apache import assert_x_sendfile
|
from django_downloadview.apache import assert_x_sendfile
|
||||||
|
|
||||||
from demoproject.apache.views import storage, storage_dir
|
from demoproject.apache.views import storage, storage_dir
|
||||||
from demoproject.compat import reverse
|
|
||||||
|
|
||||||
|
|
||||||
def setup_file():
|
def setup_file():
|
||||||
|
|
@ -43,3 +43,19 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
basename="hello-world.txt",
|
basename="hello-world.txt",
|
||||||
file_path="/apache-optimized-by-decorator/hello-world.txt",
|
file_path="/apache-optimized-by-decorator/hello-world.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'apache:modified_headers' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("apache:modified_headers")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/apache-modified-headers/hello-world.txt",
|
||||||
|
)
|
||||||
|
self.assertEqual(response["X-Test"], "header")
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
from django.conf.urls import url
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.apache import views
|
from demoproject.apache import views
|
||||||
from demoproject.compat import patterns
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "apache"
|
||||||
"demoproject.apache.views",
|
urlpatterns = [
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-middleware/$",
|
"optimized-by-middleware/",
|
||||||
views.optimized_by_middleware,
|
views.optimized_by_middleware,
|
||||||
name="optimized_by_middleware",
|
name="optimized_by_middleware",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-decorator/$",
|
"optimized-by-decorator/",
|
||||||
views.optimized_by_decorator,
|
views.optimized_by_decorator,
|
||||||
name="optimized_by_decorator",
|
name="optimized_by_decorator",
|
||||||
),
|
),
|
||||||
)
|
path(
|
||||||
|
"modified_headers/",
|
||||||
|
views.modified_headers,
|
||||||
|
name="modified_headers",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,17 @@ optimized_by_decorator = x_sendfile(
|
||||||
source_url=storage.base_url,
|
source_url=storage.base_url,
|
||||||
destination_dir="/apache-optimized-by-decorator/",
|
destination_dir="/apache-optimized-by-decorator/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _modified_headers(request):
|
||||||
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
|
response = view(request)
|
||||||
|
response["X-Test"] = "header"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
modified_headers = x_sendfile(
|
||||||
|
_modified_headers,
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/apache-modified-headers/",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
from distutils.version import StrictVersion
|
|
||||||
|
|
||||||
from django.utils.version import get_version
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.conf.urls import patterns # noqa
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def patterns(prefix, *args):
|
|
||||||
return list(args)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.urls import reverse # noqa
|
|
||||||
except ImportError:
|
|
||||||
from django.core.urlresolvers import reverse # noqa
|
|
||||||
|
|
||||||
if StrictVersion(get_version()) >= StrictVersion("2.0"):
|
|
||||||
from django.conf.urls import include as urlinclude # noqa
|
|
||||||
|
|
||||||
def include(arg, namespace=None, app_name=None):
|
|
||||||
return urlinclude((arg, app_name), namespace=namespace)
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
from django.conf.urls import include # noqa
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
"model": "object.document",
|
"model": "object.document",
|
||||||
"fields": {
|
"fields": {
|
||||||
"slug": "hello-world",
|
"slug": "hello-world",
|
||||||
"file": "object/hello-world.txt"
|
"file": "object/hello-world.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview import assert_download_response
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleURLTestCase(django.test.TestCase):
|
class SimpleURLTestCase(django.test.TestCase):
|
||||||
def test_download_response(self):
|
def test_download_response(self):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.http import views
|
from demoproject.http import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "http"
|
||||||
"",
|
urlpatterns = [
|
||||||
url(r"^simple_url/$", views.simple_url, name="simple_url"),
|
path("simple_url/", views.simple_url, name="simple_url"),
|
||||||
url(r"^avatar_url/$", views.avatar_url, name="avatar_url"),
|
path("avatar_url/", views.avatar_url, name="avatar_url"),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class SimpleURLDownloadView(HTTPDownloadView):
|
||||||
"""Return URL of hello-world.txt file on GitHub."""
|
"""Return URL of hello-world.txt file on GitHub."""
|
||||||
return (
|
return (
|
||||||
"https://raw.githubusercontent.com"
|
"https://raw.githubusercontent.com"
|
||||||
"/benoitbryon/django-downloadview"
|
"/jazzband/django-downloadview"
|
||||||
"/b7f660c5e3f37d918b106b02c5af7a887acc0111"
|
"/b7f660c5e3f37d918b106b02c5af7a887acc0111"
|
||||||
"/demo/demoproject/download/fixtures/hello-world.txt"
|
"/demo/demoproject/download/fixtures/hello-world.txt"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import os
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview.lighttpd import assert_x_sendfile
|
from django_downloadview.lighttpd import assert_x_sendfile
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
from demoproject.lighttpd.views import storage, storage_dir
|
from demoproject.lighttpd.views import storage, storage_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,3 +43,19 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
basename="hello-world.txt",
|
basename="hello-world.txt",
|
||||||
file_path="/lighttpd-optimized-by-decorator/hello-world.txt",
|
file_path="/lighttpd-optimized-by-decorator/hello-world.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
|
def test_response(self):
|
||||||
|
"""'lighttpd:modified_headers' returns X-Sendfile response."""
|
||||||
|
setup_file()
|
||||||
|
url = reverse("lighttpd:modified_headers")
|
||||||
|
response = self.client.get(url)
|
||||||
|
assert_x_sendfile(
|
||||||
|
self,
|
||||||
|
response,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
basename="hello-world.txt",
|
||||||
|
file_path="/lighttpd-modified-headers/hello-world.txt",
|
||||||
|
)
|
||||||
|
self.assertEqual(response["X-Test"], "header")
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.lighttpd import views
|
from demoproject.lighttpd import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "lighttpd"
|
||||||
"demoproject.lighttpd.views",
|
urlpatterns = [
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-middleware/$",
|
"optimized-by-middleware/",
|
||||||
views.optimized_by_middleware,
|
views.optimized_by_middleware,
|
||||||
name="optimized_by_middleware",
|
name="optimized_by_middleware",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-decorator/$",
|
"optimized-by-decorator/",
|
||||||
views.optimized_by_decorator,
|
views.optimized_by_decorator,
|
||||||
name="optimized_by_decorator",
|
name="optimized_by_decorator",
|
||||||
),
|
),
|
||||||
)
|
path(
|
||||||
|
"modified_headers/",
|
||||||
|
views.modified_headers,
|
||||||
|
name="modified_headers",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,17 @@ optimized_by_decorator = x_sendfile(
|
||||||
source_url=storage.base_url,
|
source_url=storage.base_url,
|
||||||
destination_dir="/lighttpd-optimized-by-decorator/",
|
destination_dir="/lighttpd-optimized-by-decorator/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _modified_headers(request):
|
||||||
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
|
response = view(request)
|
||||||
|
response["X-Test"] = "header"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
modified_headers = x_sendfile(
|
||||||
|
_modified_headers,
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_dir="/lighttpd-modified-headers/",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import os
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
import django.test
|
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 demoproject.compat import reverse
|
|
||||||
from demoproject.nginx.views import storage, storage_dir
|
from demoproject.nginx.views import storage, storage_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -51,3 +51,23 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
|
||||||
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,20 +1,24 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.nginx import views
|
from demoproject.nginx import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "nginx"
|
||||||
"demoproject.nginx.views",
|
urlpatterns = [
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-middleware/$",
|
"optimized-by-middleware/",
|
||||||
views.optimized_by_middleware,
|
views.optimized_by_middleware,
|
||||||
name="optimized_by_middleware",
|
name="optimized_by_middleware",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^optimized-by-decorator/$",
|
"optimized-by-decorator/",
|
||||||
views.optimized_by_decorator,
|
views.optimized_by_decorator,
|
||||||
name="optimized_by_decorator",
|
name="optimized_by_decorator",
|
||||||
),
|
),
|
||||||
)
|
path(
|
||||||
|
"modified_headers/",
|
||||||
|
views.modified_headers,
|
||||||
|
name="modified_headers",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,17 @@ optimized_by_decorator = x_accel_redirect(
|
||||||
source_url=storage.base_url,
|
source_url=storage.base_url,
|
||||||
destination_url="/nginx-optimized-by-decorator/",
|
destination_url="/nginx-optimized-by-decorator/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _modified_headers(request):
|
||||||
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
|
response = view(request)
|
||||||
|
response["X-Test"] = "header"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
modified_headers = x_accel_redirect(
|
||||||
|
_modified_headers,
|
||||||
|
source_url=storage.base_url,
|
||||||
|
destination_url="/nginx-modified-headers/",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview import assert_download_response, temporary_media_root
|
from django_downloadview import assert_download_response, temporary_media_root
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
from demoproject.object.models import Document
|
from demoproject.object.models import Document
|
||||||
|
|
||||||
# Fixtures.
|
# Fixtures.
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,27 @@
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.object import views
|
from demoproject.object import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "object"
|
||||||
"",
|
urlpatterns = [
|
||||||
url(
|
re_path(
|
||||||
r"^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
r"^default-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
views.default_file_view,
|
views.default_file_view,
|
||||||
name="default_file",
|
name="default_file",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
r"^another-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
views.another_file_view,
|
views.another_file_view,
|
||||||
name="another_file",
|
name="another_file",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
r"^deserialized_basename/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
views.deserialized_basename_view,
|
views.deserialized_basename_view,
|
||||||
name="deserialized_basename",
|
name="deserialized_basename",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
r"^inline-file/(?P<slug>[a-zA-Z0-9_-]+)/$",
|
||||||
views.inline_file_view,
|
views.inline_file_view,
|
||||||
name="inline_file",
|
name="inline_file",
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview import assert_download_response
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class StaticPathTestCase(django.test.TestCase):
|
class StaticPathTestCase(django.test.TestCase):
|
||||||
def test_download_response(self):
|
def test_download_response(self):
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.path import views
|
from demoproject.path import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "path"
|
||||||
"",
|
urlpatterns = [
|
||||||
url(r"^static-path/$", views.static_path, name="static_path"),
|
path("static-path/", views.static_path, name="static_path"),
|
||||||
url(
|
re_path(
|
||||||
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
views.dynamic_path,
|
views.dynamic_path,
|
||||||
name="dynamic_path",
|
name="dynamic_path",
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class DynamicPathDownloadView(PathDownloadView):
|
||||||
def get_path(self):
|
def get_path(self):
|
||||||
"""Return path inside fixtures directory."""
|
"""Return path inside fixtures directory."""
|
||||||
# Get path from URL resolvers or as_view kwarg.
|
# Get path from URL resolvers or as_view kwarg.
|
||||||
relative_path = super(DynamicPathDownloadView, self).get_path()
|
relative_path = super().get_path()
|
||||||
# Make it absolute.
|
# Make it absolute.
|
||||||
absolute_path = os.path.join(fixtures_dir, relative_path)
|
absolute_path = os.path.join(fixtures_dir, relative_path)
|
||||||
return absolute_path
|
return absolute_path
|
||||||
|
|
|
||||||
83
demo/demoproject/settings.py
Executable file → Normal file
83
demo/demoproject/settings.py
Executable file → Normal file
|
|
@ -1,8 +1,7 @@
|
||||||
"""Django settings for django-downloadview demo project."""
|
"""Django settings for django-downloadview demo project."""
|
||||||
from distutils.version import StrictVersion
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.version import get_version
|
|
||||||
|
|
||||||
# Configure some relative directories.
|
# Configure some relative directories.
|
||||||
demoproject_dir = os.path.dirname(os.path.abspath(__file__))
|
demoproject_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
@ -55,30 +54,18 @@ INSTALLED_APPS = (
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
# Stuff that must be at the end.
|
|
||||||
"django_nose",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# BEGIN middlewares
|
# BEGIN middlewares
|
||||||
if StrictVersion(get_version()) >= StrictVersion("1.10"):
|
MIDDLEWARE = [
|
||||||
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",
|
||||||
"django_downloadview.SmartDownloadMiddleware",
|
]
|
||||||
]
|
|
||||||
else:
|
|
||||||
MIDDLEWARE_CLASSES = [
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
|
||||||
"django_downloadview.SmartDownloadMiddleware",
|
|
||||||
]
|
|
||||||
# END middlewares
|
# END middlewares
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,37 +110,25 @@ DOWNLOADVIEW_RULES += [
|
||||||
|
|
||||||
# Test/development settings.
|
# Test/development settings.
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
TEST_RUNNER = "django_nose.NoseTestSuiteRunner"
|
|
||||||
NOSE_ARGS = [
|
|
||||||
"--verbosity=2",
|
|
||||||
"--no-path-adjustment",
|
|
||||||
"--nocapture",
|
|
||||||
"--all-modules",
|
|
||||||
"--with-coverage",
|
|
||||||
"--with-doctest",
|
|
||||||
]
|
|
||||||
|
|
||||||
if StrictVersion(get_version()) >= StrictVersion("1.8"):
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": [os.path.join(os.path.dirname(__file__), "templates")],
|
"DIRS": [os.path.join(os.path.dirname(__file__), "templates")],
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"debug": DEBUG,
|
"debug": DEBUG,
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
|
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
|
||||||
# list if you haven't customized them:
|
# list if you haven't customized them:
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.template.context_processors.debug",
|
"django.template.context_processors.debug",
|
||||||
"django.template.context_processors.i18n",
|
"django.template.context_processors.i18n",
|
||||||
"django.template.context_processors.media",
|
"django.template.context_processors.media",
|
||||||
"django.template.context_processors.static",
|
"django.template.context_processors.static",
|
||||||
"django.template.context_processors.tz",
|
"django.template.context_processors.tz",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
},
|
||||||
else:
|
]
|
||||||
TEMPLATE_DEBUG = DEBUG
|
|
||||||
TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), "templates"),)
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import unittest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.http.response import HttpResponseNotModified
|
from django.http.response import HttpResponseNotModified
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview import (
|
from django_downloadview import (
|
||||||
assert_download_response,
|
assert_download_response,
|
||||||
|
|
@ -11,7 +12,6 @@ from django_downloadview import (
|
||||||
temporary_media_root,
|
temporary_media_root,
|
||||||
)
|
)
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
from demoproject.storage import views
|
from demoproject.storage import views
|
||||||
|
|
||||||
# Fixtures.
|
# Fixtures.
|
||||||
|
|
@ -44,7 +44,7 @@ class StaticPathTestCase(django.test.TestCase):
|
||||||
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
year = datetime.date.today().year + 4
|
year = datetime.date.today().year + 4
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
url, HTTP_IF_MODIFIED_SINCE=f"Sat, 29 Oct {year} 19:43:31 GMT",
|
url, headers={"if-modified-since": f"Sat, 29 Oct {year} 19:43:31 GMT"}
|
||||||
)
|
)
|
||||||
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ class StaticPathTestCase(django.test.TestCase):
|
||||||
setup_file("1.txt")
|
setup_file("1.txt")
|
||||||
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
url, HTTP_IF_MODIFIED_SINCE="Sat, 29 Oct 1980 19:43:31 GMT"
|
url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"}
|
||||||
)
|
)
|
||||||
assert_download_response(
|
assert_download_response(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.storage import views
|
from demoproject.storage import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "storage"
|
||||||
"",
|
urlpatterns = [
|
||||||
url(
|
re_path(
|
||||||
r"^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
r"^static-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
views.static_path,
|
views.static_path,
|
||||||
name="static_path",
|
name="static_path",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
r"^dynamic-path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$",
|
||||||
views.dynamic_path,
|
views.dynamic_path,
|
||||||
name="dynamic_path",
|
name="dynamic_path",
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class DynamicStorageDownloadView(StorageDownloadView):
|
||||||
|
|
||||||
def get_path(self):
|
def get_path(self):
|
||||||
"""Return uppercase path."""
|
"""Return uppercase path."""
|
||||||
return super(DynamicStorageDownloadView, self).get_path().upper()
|
return super().get_path().upper()
|
||||||
|
|
||||||
|
|
||||||
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)
|
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# coding=utf8
|
|
||||||
"""Test suite for demoproject.download."""
|
"""Test suite for demoproject.download."""
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class HomeViewTestCase(TestCase):
|
class HomeViewTestCase(TestCase):
|
||||||
|
|
|
||||||
51
demo/demoproject/urls.py
Executable file → Normal file
51
demo/demoproject/urls.py
Executable file → Normal file
|
|
@ -1,47 +1,44 @@
|
||||||
from django.conf.urls import url
|
from django.urls import include, path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from demoproject.compat import include, patterns
|
|
||||||
|
|
||||||
home = TemplateView.as_view(template_name="home.html")
|
home = TemplateView.as_view(template_name="home.html")
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
urlpatterns = [
|
||||||
"",
|
|
||||||
# ObjectDownloadView.
|
# ObjectDownloadView.
|
||||||
url(
|
path(
|
||||||
r"^object/",
|
"object/",
|
||||||
include("demoproject.object.urls", app_name="object", namespace="object"),
|
include("demoproject.object.urls", namespace="object"),
|
||||||
),
|
),
|
||||||
# StorageDownloadView.
|
# StorageDownloadView.
|
||||||
url(
|
path(
|
||||||
r"^storage/",
|
"storage/",
|
||||||
include("demoproject.storage.urls", app_name="storage", namespace="storage"),
|
include("demoproject.storage.urls", namespace="storage"),
|
||||||
),
|
),
|
||||||
# PathDownloadView.
|
# PathDownloadView.
|
||||||
url(r"^path/", include("demoproject.path.urls", app_name="path", namespace="path")),
|
path("path/", include("demoproject.path.urls", namespace="path")),
|
||||||
# HTTPDownloadView.
|
# HTTPDownloadView.
|
||||||
url(r"^http/", include("demoproject.http.urls", app_name="http", namespace="http")),
|
path("http/", include("demoproject.http.urls", namespace="http")),
|
||||||
# VirtualDownloadView.
|
# VirtualDownloadView.
|
||||||
url(
|
path(
|
||||||
r"^virtual/",
|
"virtual/",
|
||||||
include("demoproject.virtual.urls", app_name="virtual", namespace="virtual"),
|
include("demoproject.virtual.urls", namespace="virtual"),
|
||||||
),
|
),
|
||||||
# Nginx optimizations.
|
# Nginx optimizations.
|
||||||
url(
|
path(
|
||||||
r"^nginx/",
|
"nginx/",
|
||||||
include("demoproject.nginx.urls", app_name="nginx", namespace="nginx"),
|
include("demoproject.nginx.urls", namespace="nginx"),
|
||||||
),
|
),
|
||||||
# Apache optimizations.
|
# Apache optimizations.
|
||||||
url(
|
path(
|
||||||
r"^apache/",
|
"apache/",
|
||||||
include("demoproject.apache.urls", app_name="apache", namespace="apache"),
|
include("demoproject.apache.urls", namespace="apache"),
|
||||||
),
|
),
|
||||||
# Lighttpd optimizations.
|
# Lighttpd optimizations.
|
||||||
url(
|
path(
|
||||||
r"^lighttpd/",
|
"lighttpd/",
|
||||||
include("demoproject.lighttpd.urls", app_name="lighttpd", namespace="lighttpd"),
|
include("demoproject.lighttpd.urls", namespace="lighttpd"),
|
||||||
),
|
),
|
||||||
# An informative homepage.
|
# An informative homepage.
|
||||||
url(r"$", home, name="home"),
|
path("", home, name="home"),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import django.test
|
import django.test
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from django_downloadview import assert_download_response
|
from django_downloadview import assert_download_response
|
||||||
|
|
||||||
from demoproject.compat import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class TextTestCase(django.test.TestCase):
|
class TextTestCase(django.test.TestCase):
|
||||||
def test_download_response(self):
|
def test_download_response(self):
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.compat import patterns
|
|
||||||
from demoproject.virtual import views
|
from demoproject.virtual import views
|
||||||
|
|
||||||
urlpatterns = patterns(
|
app_name = "virtual"
|
||||||
"",
|
urlpatterns = [
|
||||||
url(r"^text/$", views.TextDownloadView.as_view(), name="text"),
|
path("text/", views.TextDownloadView.as_view(), name="text"),
|
||||||
url(r"^stringio/$", views.StringIODownloadView.as_view(), name="stringio"),
|
path("stringio/", views.StringIODownloadView.as_view(), name="stringio"),
|
||||||
url(r"^gerenated/$", views.GeneratedDownloadView.as_view(), name="generated"),
|
path("gerenated/", views.GeneratedDownloadView.as_view(), name="generated"),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ 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
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,26 @@
|
||||||
"""Python packaging."""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
project_root = os.path.dirname(here)
|
|
||||||
|
|
||||||
|
setup(
|
||||||
NAME = "django-downloadview-demo"
|
name="django-downloadview-demo",
|
||||||
DESCRIPTION = "Serve files with Django and reverse-proxies."
|
version="1.0",
|
||||||
README = open(os.path.join(here, "README.rst")).read()
|
description="Serve files with Django and reverse-proxies.",
|
||||||
VERSION = open(os.path.join(project_root, "VERSION")).read().strip()
|
long_description=open(os.path.join(here, "README.rst")).read(),
|
||||||
AUTHOR = "Benoît Bryon"
|
classifiers=[
|
||||||
EMAIL = "benoit@marmelune.net"
|
"Development Status :: 5 - Production/Stable",
|
||||||
URL = "https://django-downloadview.readthedocs.io/"
|
"License :: OSI Approved :: BSD License",
|
||||||
CLASSIFIERS = [
|
"Programming Language :: Python :: 3",
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Framework :: Django",
|
||||||
"License :: OSI Approved :: BSD License",
|
],
|
||||||
"Programming Language :: Python :: 2.7",
|
author="Benoît Bryon",
|
||||||
"Framework :: Django",
|
author_email="benoit@marmelune.net",
|
||||||
]
|
url="https://django-downloadview.readthedocs.io/",
|
||||||
KEYWORDS = []
|
license="BSD",
|
||||||
PACKAGES = ["demoproject"]
|
packages=["demoproject"],
|
||||||
REQUIREMENTS = ["django-downloadview", "django-nose"]
|
include_package_data=True,
|
||||||
ENTRY_POINTS = {"console_scripts": ["demo = demoproject.manage:main"]}
|
zip_safe=False,
|
||||||
|
install_requires=["django-downloadview", "pytest-django"],
|
||||||
|
entry_points={"console_scripts": ["demo = demoproject.manage:main"]},
|
||||||
if __name__ == "__main__": # Don't run setup() when we import this module.
|
)
|
||||||
setup(
|
|
||||||
name=NAME,
|
|
||||||
version=VERSION,
|
|
||||||
description=DESCRIPTION,
|
|
||||||
long_description=README,
|
|
||||||
classifiers=CLASSIFIERS,
|
|
||||||
keywords=" ".join(KEYWORDS),
|
|
||||||
author=AUTHOR,
|
|
||||||
author_email=EMAIL,
|
|
||||||
url=URL,
|
|
||||||
license="BSD",
|
|
||||||
packages=PACKAGES,
|
|
||||||
include_package_data=True,
|
|
||||||
zip_safe=False,
|
|
||||||
install_requires=REQUIREMENTS,
|
|
||||||
entry_points=ENTRY_POINTS,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"""Serve files with Django and reverse proxies."""
|
"""Serve files with Django and reverse proxies."""
|
||||||
|
|
||||||
from django_downloadview.api import * # NoQA
|
from django_downloadview.api import * # NoQA
|
||||||
|
|
||||||
import pkg_resources
|
import importlib.metadata
|
||||||
|
|
||||||
#: Module version, as defined in PEP-0396.
|
#: Module version, as defined in PEP-0396.
|
||||||
__version__ = pkg_resources.get_distribution(__package__.replace("-", "_")).version
|
__version__ = importlib.metadata.version(__package__.replace("-", "_"))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ See also `documentation of mod_xsendfile for Apache
|
||||||
Apache optimizations </optimizations/apache>`.
|
Apache optimizations </optimizations/apache>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.apache.decorators import x_sendfile # NoQA
|
from django_downloadview.apache.decorators import x_sendfile # NoQA
|
||||||
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||||
):
|
):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
super(XSendfileMiddleware, self).__init__(
|
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||||
get_response, source_dir, source_url, destination_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
def process_download_response(self, request, response):
|
def process_download_response(self, request, response):
|
||||||
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||||
|
|
@ -34,4 +32,5 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
content_type=response["Content-Type"],
|
content_type=response["Content-Type"],
|
||||||
basename=response.basename,
|
basename=response.basename,
|
||||||
attachment=response.attachment,
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Apache's specific responses."""
|
"""Apache's specific responses."""
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
||||||
|
|
@ -7,9 +8,14 @@ from django_downloadview.response import ProxiedDownloadResponse, content_dispos
|
||||||
class XSendfileResponse(ProxiedDownloadResponse):
|
class XSendfileResponse(ProxiedDownloadResponse):
|
||||||
"Delegates serving file to Apache via X-Sendfile header."
|
"Delegates serving file to Apache via X-Sendfile header."
|
||||||
|
|
||||||
def __init__(self, file_path, content_type, basename=None, attachment=True):
|
def __init__(
|
||||||
|
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||||
|
):
|
||||||
"""Return a HttpResponse with headers for Apache X-Sendfile."""
|
"""Return a HttpResponse with headers for Apache X-Sendfile."""
|
||||||
super(XSendfileResponse, self).__init__(content_type=content_type)
|
# content-type must be provided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
if attachment:
|
if attachment:
|
||||||
self.basename = basename or os.path.basename(file_path)
|
self.basename = basename or os.path.basename(file_path)
|
||||||
self["Content-Disposition"] = content_disposition(self.basename)
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django_downloadview.apache.response import XSendfileResponse
|
from django_downloadview.apache.response import XSendfileResponse
|
||||||
|
|
||||||
|
|
||||||
class XSendfileValidator(object):
|
class XSendfileValidator:
|
||||||
"""Utility class to validate XSendfileResponse instances.
|
"""Utility class to validate XSendfileResponse instances.
|
||||||
|
|
||||||
See also :py:func:`assert_x_sendfile` shortcut function.
|
See also :py:func:`assert_x_sendfile` shortcut function.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
"""Declaration of API shortcuts."""
|
"""Declaration of API shortcuts."""
|
||||||
|
|
||||||
from django_downloadview.files import HTTPFile, StorageFile, VirtualFile
|
from django_downloadview.files import HTTPFile, StorageFile, VirtualFile
|
||||||
from django_downloadview.io import BytesIteratorIO, TextIteratorIO
|
from django_downloadview.io import BytesIteratorIO, TextIteratorIO
|
||||||
from django_downloadview.middlewares import (
|
from django_downloadview.middlewares import (
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ def signature_required(function):
|
||||||
|
|
||||||
@wraps(function)
|
@wraps(function)
|
||||||
def decorator(request, *args, **kwargs):
|
def decorator(request, *args, **kwargs):
|
||||||
_signature_is_valid()
|
_signature_is_valid(request)
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""File wrappers for use as exchange data between views and responses."""
|
"""File wrappers for use as exchange data between views and responses."""
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
@ -167,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
|
||||||
|
|
@ -183,7 +184,7 @@ 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"""Low-level IO operations, for use with file wrappers."""
|
"""Low-level IO operations, for use with file wrappers."""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from django.utils.encoding import force_bytes, force_text
|
from django.utils.encoding import force_bytes, force_str
|
||||||
|
|
||||||
|
|
||||||
class TextIteratorIO(io.TextIOBase):
|
class TextIteratorIO(io.TextIOBase):
|
||||||
|
|
@ -32,7 +33,7 @@ class TextIteratorIO(io.TextIOBase):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Make sure we handle text.
|
# Make sure we handle text.
|
||||||
self._left = force_text(self._left)
|
self._left = force_str(self._left)
|
||||||
ret = self._left[:n]
|
ret = self._left[:n]
|
||||||
self._left = self._left[len(ret) :]
|
self._left = self._left[len(ret) :]
|
||||||
return ret
|
return ret
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ See also `documentation of X-Sendfile for Lighttpd
|
||||||
</optimizations/lighttpd>`.
|
</optimizations/lighttpd>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
|
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
|
||||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
|
||||||
):
|
):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
super(XSendfileMiddleware, self).__init__(
|
super().__init__(get_response, source_dir, source_url, destination_dir)
|
||||||
get_response, source_dir, source_url, destination_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
def process_download_response(self, request, response):
|
def process_download_response(self, request, response):
|
||||||
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
"""Replace DownloadResponse instances by XSendfileResponse ones."""
|
||||||
|
|
@ -34,4 +32,5 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
|
||||||
content_type=response["Content-Type"],
|
content_type=response["Content-Type"],
|
||||||
basename=response.basename,
|
basename=response.basename,
|
||||||
attachment=response.attachment,
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Lighttpd's specific responses."""
|
"""Lighttpd's specific responses."""
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
from django_downloadview.response import ProxiedDownloadResponse, content_disposition
|
||||||
|
|
@ -7,9 +8,14 @@ from django_downloadview.response import ProxiedDownloadResponse, content_dispos
|
||||||
class XSendfileResponse(ProxiedDownloadResponse):
|
class XSendfileResponse(ProxiedDownloadResponse):
|
||||||
"Delegates serving file to Lighttpd via X-Sendfile header."
|
"Delegates serving file to Lighttpd via X-Sendfile header."
|
||||||
|
|
||||||
def __init__(self, file_path, content_type, basename=None, attachment=True):
|
def __init__(
|
||||||
|
self, file_path, content_type, basename=None, attachment=True, headers=None
|
||||||
|
):
|
||||||
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
|
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
|
||||||
super(XSendfileResponse, self).__init__(content_type=content_type)
|
# content-type must be porvided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
if attachment:
|
if attachment:
|
||||||
self.basename = basename or os.path.basename(file_path)
|
self.basename = basename or os.path.basename(file_path)
|
||||||
self["Content-Disposition"] = content_disposition(self.basename)
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
|
||||||
responses and may replace them with optimized download responses.
|
responses and may replace them with optimized download responses.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import collections
|
|
||||||
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -14,14 +15,6 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
from django_downloadview.response import DownloadResponse
|
from django_downloadview.response import DownloadResponse
|
||||||
from django_downloadview.utils import import_member
|
from django_downloadview.utils import import_member
|
||||||
|
|
||||||
try:
|
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
class MiddlewareMixin(object):
|
|
||||||
def __init__(self, get_response=None):
|
|
||||||
super(MiddlewareMixin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
#: Sentinel value to detect whether configuration is to be loaded from Django
|
#: Sentinel value to detect whether configuration is to be loaded from Django
|
||||||
#: settings or not.
|
#: settings or not.
|
||||||
|
|
@ -38,13 +31,20 @@ def is_download_response(response):
|
||||||
return isinstance(response, DownloadResponse)
|
return isinstance(response, DownloadResponse)
|
||||||
|
|
||||||
|
|
||||||
class BaseDownloadMiddleware(MiddlewareMixin):
|
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.
|
||||||
|
|
||||||
|
|
@ -77,21 +77,13 @@ class RealDownloadMiddleware(BaseDownloadMiddleware):
|
||||||
whose file attribute have either an URL or a file name.
|
whose file attribute have either an URL or a file name.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if super(RealDownloadMiddleware, self).is_download_response(response):
|
return super().is_download_response(response) and bool(
|
||||||
try:
|
getattr(response.file, "url", None) or getattr(response.file, "name", None)
|
||||||
return response.file.url or response.file.name
|
)
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
class DownloadDispatcher:
|
||||||
"Download middleware that dispatches job to several middleware instances."
|
def __init__(self, middlewares=AUTO_CONFIGURE):
|
||||||
|
|
||||||
def __init__(self, get_response=None, middlewares=AUTO_CONFIGURE):
|
|
||||||
super(DownloadDispatcherMiddleware, self).__init__(get_response)
|
|
||||||
#: List of children middlewares.
|
#: List of children middlewares.
|
||||||
self.middlewares = middlewares
|
self.middlewares = middlewares
|
||||||
if self.middlewares is AUTO_CONFIGURE:
|
if self.middlewares is AUTO_CONFIGURE:
|
||||||
|
|
@ -100,40 +92,48 @@ class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
||||||
def auto_configure_middlewares(self):
|
def auto_configure_middlewares(self):
|
||||||
"""Populate :attr:`middlewares` from
|
"""Populate :attr:`middlewares` from
|
||||||
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
|
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
|
||||||
for (key, import_string, kwargs) in getattr(
|
for key, import_string, kwargs in getattr(
|
||||||
settings, "DOWNLOADVIEW_MIDDLEWARES", []
|
settings, "DOWNLOADVIEW_MIDDLEWARES", []
|
||||||
):
|
):
|
||||||
factory = import_member(import_string)
|
factory = import_member(import_string)
|
||||||
middleware = factory(**kwargs)
|
middleware = factory(**kwargs)
|
||||||
self.middlewares.append((key, middleware))
|
self.middlewares.append((key, middleware))
|
||||||
|
|
||||||
def process_download_response(self, request, response):
|
def dispatch(self, request, response):
|
||||||
"""Dispatches job to children middlewares."""
|
"""Dispatches job to children middlewares."""
|
||||||
for (key, middleware) in self.middlewares:
|
for key, middleware in self.middlewares:
|
||||||
response = middleware.process_response(request, response)
|
response = middleware.process_response(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
||||||
|
"Download middleware that dispatches job to several middleware instances."
|
||||||
|
|
||||||
|
def __init__(self, get_response, middlewares=AUTO_CONFIGURE):
|
||||||
|
super().__init__(get_response)
|
||||||
|
self.dispatcher = DownloadDispatcher(middlewares)
|
||||||
|
|
||||||
|
def process_download_response(self, request, response):
|
||||||
|
return self.dispatcher.dispatch(request, response)
|
||||||
|
|
||||||
|
|
||||||
|
class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
|
||||||
"""Easy to configure download middleware."""
|
"""Easy to configure download middleware."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
get_response=None,
|
get_response,
|
||||||
backend_factory=AUTO_CONFIGURE,
|
backend_factory=AUTO_CONFIGURE,
|
||||||
backend_options=AUTO_CONFIGURE,
|
backend_options=AUTO_CONFIGURE,
|
||||||
):
|
):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
super(SmartDownloadMiddleware, self).__init__(get_response)
|
super().__init__(get_response, middlewares=[])
|
||||||
#: :class:`DownloadDispatcher` instance that can hold multiple
|
#: Callable (typically a class) to instantiate backend (typically a
|
||||||
#: backend instances.
|
|
||||||
self.dispatcher = DownloadDispatcherMiddleware(middlewares=[])
|
|
||||||
#: Callable (typically a class) to instanciate backend (typically a
|
|
||||||
#: :class:`DownloadMiddleware` subclass).
|
#: :class:`DownloadMiddleware` subclass).
|
||||||
self.backend_factory = backend_factory
|
self.backend_factory = backend_factory
|
||||||
if self.backend_factory is AUTO_CONFIGURE:
|
if self.backend_factory is AUTO_CONFIGURE:
|
||||||
self.auto_configure_backend_factory()
|
self.auto_configure_backend_factory()
|
||||||
#: List of positional or keyword arguments to instanciate backend
|
#: List of positional or keyword arguments to instantiate backend
|
||||||
#: instances.
|
#: instances.
|
||||||
self.backend_options = backend_options
|
self.backend_options = backend_options
|
||||||
if self.backend_options is AUTO_CONFIGURE:
|
if self.backend_options is AUTO_CONFIGURE:
|
||||||
|
|
@ -145,7 +145,7 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
||||||
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
|
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_BACKEND"
|
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_BACKEND"
|
||||||
)
|
)
|
||||||
|
|
||||||
def auto_configure_backend_options(self):
|
def auto_configure_backend_options(self):
|
||||||
|
|
@ -155,12 +155,12 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
||||||
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
|
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_RULES"
|
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_RULES"
|
||||||
)
|
)
|
||||||
for key, options in enumerate(options_list):
|
for key, options in enumerate(options_list):
|
||||||
args = []
|
args = []
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if isinstance(options, collections.Mapping): # Using kwargs.
|
if isinstance(options, collections.abc.Mapping): # Using kwargs.
|
||||||
kwargs = options
|
kwargs = options
|
||||||
else:
|
else:
|
||||||
args = options
|
args = options
|
||||||
|
|
@ -172,10 +172,6 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware):
|
||||||
middleware_instance = factory(*args, **kwargs)
|
middleware_instance = factory(*args, **kwargs)
|
||||||
self.dispatcher.middlewares.append((key, middleware_instance))
|
self.dispatcher.middlewares.append((key, middleware_instance))
|
||||||
|
|
||||||
def process_download_response(self, request, response):
|
|
||||||
"""Use :attr:`dispatcher` to process download response."""
|
|
||||||
return self.dispatcher.process_download_response(request, response)
|
|
||||||
|
|
||||||
|
|
||||||
class NoRedirectionMatch(Exception):
|
class NoRedirectionMatch(Exception):
|
||||||
"""Response object does not match redirection rules."""
|
"""Response object does not match redirection rules."""
|
||||||
|
|
@ -185,10 +181,10 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
||||||
"""Base class for middlewares that use optimizations of reverse proxies."""
|
"""Base class for middlewares that use optimizations of reverse proxies."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, get_response=None, source_dir=None, source_url=None, destination_url=None
|
self, get_response, source_dir=None, source_url=None, destination_url=None
|
||||||
):
|
):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
super(ProxiedDownloadMiddleware, self).__init__(get_response)
|
super().__init__(get_response)
|
||||||
|
|
||||||
self.source_dir = source_dir
|
self.source_dir = source_dir
|
||||||
self.source_url = source_url
|
self.source_url = source_url
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||||
</optimizations/nginx>`.
|
</optimizations/nginx>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
||||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Nginx X-Accel on a specific view."""
|
"""Decorators to apply Nginx X-Accel on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
||||||
else:
|
else:
|
||||||
source_dir = source_dir
|
source_dir = source_dir
|
||||||
|
|
||||||
super(XAccelRedirectMiddleware, self).__init__(
|
super().__init__(get_response, source_dir, source_url, destination_url)
|
||||||
get_response, source_dir, source_url, destination_url
|
|
||||||
)
|
|
||||||
|
|
||||||
self.expires = expires
|
self.expires = expires
|
||||||
self.with_buffering = with_buffering
|
self.with_buffering = with_buffering
|
||||||
|
|
@ -87,6 +85,7 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
|
||||||
with_buffering=self.with_buffering,
|
with_buffering=self.with_buffering,
|
||||||
limit_rate=self.limit_rate,
|
limit_rate=self.limit_rate,
|
||||||
attachment=response.attachment,
|
attachment=response.attachment,
|
||||||
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -132,7 +131,7 @@ class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware):
|
||||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is "
|
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is "
|
||||||
"required by %s middleware" % self.__class__.__name__
|
"required by %s middleware" % self.__class__.__name__
|
||||||
)
|
)
|
||||||
super(SingleXAccelRedirectMiddleware, self).__init__(
|
super().__init__(
|
||||||
get_response=get_response,
|
get_response=get_response,
|
||||||
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
||||||
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Nginx's specific responses."""
|
"""Nginx's specific responses."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
@ -19,9 +20,13 @@ class XAccelRedirectResponse(ProxiedDownloadResponse):
|
||||||
with_buffering=None,
|
with_buffering=None,
|
||||||
limit_rate=None,
|
limit_rate=None,
|
||||||
attachment=True,
|
attachment=True,
|
||||||
|
headers=None,
|
||||||
):
|
):
|
||||||
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
||||||
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
|
# content-type must be porvided only as keyword argument to response
|
||||||
|
if headers and content_type:
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
super().__init__(content_type=content_type, headers=headers)
|
||||||
if attachment:
|
if attachment:
|
||||||
self.basename = basename or url_basename(redirect_url, content_type)
|
self.basename = basename or url_basename(redirect_url, content_type)
|
||||||
self["Content-Disposition"] = content_disposition(self.basename)
|
self["Content-Disposition"] = content_disposition(self.basename)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
for details.
|
for details.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -17,14 +18,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
|
||||||
|
|
||||||
|
|
||||||
def get_middlewares():
|
if deprecated_middleware in settings.MIDDLEWARE:
|
||||||
try:
|
|
||||||
return settings.MIDDLEWARE
|
|
||||||
except AttributeError:
|
|
||||||
return settings.MIDDLEWARE_CLASSES
|
|
||||||
|
|
||||||
|
|
||||||
if deprecated_middleware in get_middlewares():
|
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"{deprecated_middleware} middleware has been renamed as of "
|
"{deprecated_middleware} middleware has been renamed as of "
|
||||||
"django-downloadview version 1.3. You may use "
|
"django-downloadview version 1.3. You may use "
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django_downloadview.nginx.response import XAccelRedirectResponse
|
from django_downloadview.nginx.response import XAccelRedirectResponse
|
||||||
|
|
||||||
|
|
||||||
class XAccelRedirectValidator(object):
|
class XAccelRedirectValidator:
|
||||||
"""Utility class to validate XAccelRedirectResponse instances.
|
"""Utility class to validate XAccelRedirectResponse instances.
|
||||||
|
|
||||||
See also :py:func:`assert_x_accel_redirect` shortcut function.
|
See also :py:func:`assert_x_accel_redirect` shortcut function.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
""":py:class:`django.http.HttpResponse` subclasses."""
|
""":py:class:`django.http.HttpResponse` subclasses."""
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
@ -72,7 +73,13 @@ def content_disposition(filename):
|
||||||
"""
|
"""
|
||||||
if not filename:
|
if not filename:
|
||||||
return "attachment"
|
return "attachment"
|
||||||
ascii_filename = encode_basename_ascii(filename)
|
# ASCII filenames are quoted and must ensure escape sequences
|
||||||
|
# in the filename won't break out of the quoted header value
|
||||||
|
# which can permit a reflected file download attack. The UTF-8
|
||||||
|
# version is immune because it's not quoted.
|
||||||
|
ascii_filename = (
|
||||||
|
encode_basename_ascii(filename).replace("\\", "\\\\").replace('"', r"\"")
|
||||||
|
)
|
||||||
utf8_filename = encode_basename_utf8(filename)
|
utf8_filename = encode_basename_utf8(filename)
|
||||||
if ascii_filename == utf8_filename: # ASCII only.
|
if ascii_filename == utf8_filename: # ASCII only.
|
||||||
return f'attachment; filename="{ascii_filename}"'
|
return f'attachment; filename="{ascii_filename}"'
|
||||||
|
|
@ -138,7 +145,7 @@ class DownloadResponse(StreamingHttpResponse):
|
||||||
#: A :doc:`file wrapper instance </files>`, such as
|
#: A :doc:`file wrapper instance </files>`, such as
|
||||||
#: :class:`~django.core.files.base.File`.
|
#: :class:`~django.core.files.base.File`.
|
||||||
self.file = file_instance
|
self.file = file_instance
|
||||||
super(DownloadResponse, self).__init__(
|
super().__init__(
|
||||||
streaming_content=self.file, status=status, content_type=content_type
|
streaming_content=self.file, status=status, content_type=content_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -195,16 +202,6 @@ class DownloadResponse(StreamingHttpResponse):
|
||||||
self._default_headers = headers
|
self._default_headers = headers
|
||||||
return self._default_headers
|
return self._default_headers
|
||||||
|
|
||||||
def items(self):
|
|
||||||
"""Return iterable of (header, value).
|
|
||||||
|
|
||||||
This method is called by http handlers just before WSGI's
|
|
||||||
start_response() is called... but it is not called by
|
|
||||||
django.test.ClientHandler! :'(
|
|
||||||
|
|
||||||
"""
|
|
||||||
return super(DownloadResponse, self).items()
|
|
||||||
|
|
||||||
def get_basename(self):
|
def get_basename(self):
|
||||||
"""Return basename."""
|
"""Return basename."""
|
||||||
if self.basename:
|
if self.basename:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Port of django-sendfile in django-downloadview."""
|
"""Port of django-sendfile in django-downloadview."""
|
||||||
|
|
||||||
from django_downloadview.views.path import PathDownloadView
|
from django_downloadview.views.path import PathDownloadView
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class SignedURLMixin:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def url(self, name):
|
def url(self, name):
|
||||||
path = super(SignedURLMixin, self).url(name)
|
path = super().url(name)
|
||||||
signer = TimestampSigner()
|
signer = TimestampSigner()
|
||||||
signature = signer.sign(path)
|
signature = signer.sign(path)
|
||||||
return "{}?X-Signature={}".format(path, signature)
|
return "{}?X-Signature={}".format(path, signature)
|
||||||
|
|
|
||||||
|
|
@ -71,13 +71,13 @@ class temporary_media_root(override_settings):
|
||||||
settings.MEDIA_ROOT."""
|
settings.MEDIA_ROOT."""
|
||||||
tmp_dir = tempfile.mkdtemp()
|
tmp_dir = tempfile.mkdtemp()
|
||||||
self.options["MEDIA_ROOT"] = tmp_dir
|
self.options["MEDIA_ROOT"] = tmp_dir
|
||||||
super(temporary_media_root, self).enable()
|
super().enable()
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
"""Remove directory settings.MEDIA_ROOT then restore original
|
"""Remove directory settings.MEDIA_ROOT then restore original
|
||||||
setting."""
|
setting."""
|
||||||
shutil.rmtree(settings.MEDIA_ROOT)
|
shutil.rmtree(settings.MEDIA_ROOT)
|
||||||
super(temporary_media_root, self).disable()
|
super().disable()
|
||||||
|
|
||||||
|
|
||||||
class DownloadResponseValidator(object):
|
class DownloadResponseValidator(object):
|
||||||
|
|
@ -125,11 +125,13 @@ class DownloadResponseValidator(object):
|
||||||
check_ascii = True
|
check_ascii = True
|
||||||
if check_ascii:
|
if check_ascii:
|
||||||
test_case.assertIn(
|
test_case.assertIn(
|
||||||
f'filename="{ascii_name}"', response["Content-Disposition"],
|
f'filename="{ascii_name}"',
|
||||||
|
response["Content-Disposition"],
|
||||||
)
|
)
|
||||||
if check_utf8:
|
if check_utf8:
|
||||||
test_case.assertIn(
|
test_case.assertIn(
|
||||||
f"filename*=UTF-8''{utf8_name}", response["Content-Disposition"],
|
f"filename*=UTF-8''{utf8_name}",
|
||||||
|
response["Content-Disposition"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def assert_content_type(self, test_case, response, value):
|
def assert_content_type(self, test_case, response, value):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Utility functions that may be implemented in external packages."""
|
"""Utility functions that may be implemented in external packages."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)
|
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Views to stream files."""
|
"""Views to stream files."""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
|
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
|
||||||
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Base material for download views: :class:`DownloadMixin` and
|
"""Base material for download views: :class:`DownloadMixin` and
|
||||||
:class:`BaseDownloadView`"""
|
:class:`BaseDownloadView`"""
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from django.http import Http404, HttpResponseNotModified
|
from django.http import Http404, HttpResponseNotModified
|
||||||
|
|
@ -102,9 +103,9 @@ class DownloadMixin(object):
|
||||||
Else, fallbacks to default implementation, which uses
|
Else, fallbacks to default implementation, which uses
|
||||||
:py:func:`django.views.static.was_modified_since`.
|
:py:func:`django.views.static.was_modified_since`.
|
||||||
|
|
||||||
Django's ``was_modified_since`` function needs a datetime and a size.
|
Django's ``was_modified_since`` function needs a datetime.
|
||||||
It is passed ``modified_time`` and ``size`` attributes from file
|
It is passed the ``modified_time`` attribute from file
|
||||||
wrapper. If file wrapper does not support these attributes
|
wrapper. If file wrapper does not support this attribute
|
||||||
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
||||||
the file is considered as modified and ``True`` is returned.
|
the file is considered as modified and ``True`` is returned.
|
||||||
|
|
||||||
|
|
@ -116,12 +117,11 @@ class DownloadMixin(object):
|
||||||
modification_time = calendar.timegm(
|
modification_time = calendar.timegm(
|
||||||
file_instance.modified_time.utctimetuple()
|
file_instance.modified_time.utctimetuple()
|
||||||
)
|
)
|
||||||
size = file_instance.size
|
|
||||||
except (AttributeError, NotImplementedError) as e:
|
except (AttributeError, NotImplementedError) as e:
|
||||||
print("!=======!", e)
|
print("!=======!", e)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return was_modified_since(since, modification_time, size)
|
return was_modified_since(since, modification_time)
|
||||||
|
|
||||||
def not_modified_response(self, *response_args, **response_kwargs):
|
def not_modified_response(self, *response_args, **response_kwargs):
|
||||||
"""Return :class:`django.http.HttpResponseNotModified` instance."""
|
"""Return :class:`django.http.HttpResponseNotModified` instance."""
|
||||||
|
|
@ -157,7 +157,7 @@ class DownloadMixin(object):
|
||||||
except exceptions.FileNotFound:
|
except exceptions.FileNotFound:
|
||||||
return self.file_not_found_response()
|
return self.file_not_found_response()
|
||||||
# Respect the If-Modified-Since header.
|
# Respect the If-Modified-Since header.
|
||||||
since = self.request.META.get("HTTP_IF_MODIFIED_SINCE", None)
|
since = self.request.headers.get("if-modified-since", None)
|
||||||
if since is not None:
|
if since is not None:
|
||||||
if not self.was_modified_since(self.file_instance, since):
|
if not self.was_modified_since(self.file_instance, since):
|
||||||
return self.not_modified_response(**response_kwargs)
|
return self.not_modified_response(**response_kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files given an URL, i.e. files you want to proxy."""
|
"""Stream files given an URL, i.e. files you want to proxy."""
|
||||||
|
|
||||||
from django_downloadview.files import HTTPFile
|
from django_downloadview.files import HTTPFile
|
||||||
from django_downloadview.views.base import BaseDownloadView
|
from django_downloadview.views.base import BaseDownloadView
|
||||||
|
|
||||||
|
|
@ -44,5 +45,5 @@ class HTTPDownloadView(BaseDownloadView):
|
||||||
request_factory=self.get_request_factory(),
|
request_factory=self.get_request_factory(),
|
||||||
name=self.get_basename(),
|
name=self.get_basename(),
|
||||||
url=self.get_url(),
|
url=self.get_url(),
|
||||||
**self.get_request_kwargs()
|
**self.get_request_kwargs(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files that live in models."""
|
"""Stream files that live in models."""
|
||||||
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from django_downloadview.exceptions import FileNotFound
|
from django_downloadview.exceptions import FileNotFound
|
||||||
|
|
@ -83,7 +84,7 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
||||||
|
|
||||||
def get_basename(self):
|
def get_basename(self):
|
||||||
"""Return client-side filename."""
|
"""Return client-side filename."""
|
||||||
basename = super(ObjectDownloadView, self).get_basename()
|
basename = super().get_basename()
|
||||||
if basename is None:
|
if basename is None:
|
||||||
field = "basename"
|
field = "basename"
|
||||||
model_field = getattr(self, "%s_field" % field, False)
|
model_field = getattr(self, "%s_field" % field, False)
|
||||||
|
|
@ -93,4 +94,4 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
return super(ObjectDownloadView, self).get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
""":class:`PathDownloadView`."""
|
""":class:`PathDownloadView`."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files from storage."""
|
"""Stream files from storage."""
|
||||||
|
|
||||||
from django.core.files.storage import DefaultStorage
|
from django.core.files.storage import DefaultStorage
|
||||||
|
|
||||||
from django_downloadview.files import StorageFile
|
from django_downloadview.files import StorageFile
|
||||||
|
|
@ -14,16 +15,6 @@ class StorageDownloadView(PathDownloadView):
|
||||||
#: Path to the file to serve relative to storage.
|
#: Path to the file to serve relative to storage.
|
||||||
path = None # Override docstring.
|
path = None # Override docstring.
|
||||||
|
|
||||||
def get_path(self):
|
|
||||||
"""Return path of the file to serve, relative to storage.
|
|
||||||
|
|
||||||
Default implementation simply returns view's :py:attr:`path` attribute.
|
|
||||||
|
|
||||||
Override this method if you want custom implementation.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return super(StorageDownloadView, self).get_path()
|
|
||||||
|
|
||||||
def get_file(self):
|
def get_file(self):
|
||||||
"""Return :class:`~django_downloadview.files.StorageFile` instance."""
|
"""Return :class:`~django_downloadview.files.StorageFile` instance."""
|
||||||
return StorageFile(self.storage, self.get_path())
|
return StorageFile(self.storage, self.get_path())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files that you generate or that live in memory."""
|
"""Stream files that you generate or that live in memory."""
|
||||||
|
|
||||||
from django_downloadview.views.base import BaseDownloadView
|
from django_downloadview.views.base import BaseDownloadView
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ optimizations.
|
||||||
|
|
||||||
* :doc:`/about/alternatives`
|
* :doc:`/about/alternatives`
|
||||||
* `roadmap
|
* `roadmap
|
||||||
<https://github.com/benoitbryon/django-downloadview/milestones>`_
|
<https://github.com/jazzband/django-downloadview/milestones>`_
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`Django`: https://djangoproject.com
|
.. _`Django`: https://www.djangoproject.com
|
||||||
|
|
|
||||||
100
docs/conf.py
100
docs/conf.py
|
|
@ -1,14 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""django-downloadview documentation build configuration file."""
|
"""django-downloadview documentation build configuration file."""
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
|
|
||||||
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
|
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
|
||||||
# django-downloadview depends on Django...
|
# django-downloadview depends on Django...
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
settings.configure(
|
settings.configure(
|
||||||
DATABASES={}, # Required to load ``django.views.generic``.
|
DATABASES={}, # Required to load ``django.views.generic``.
|
||||||
)
|
)
|
||||||
|
|
@ -18,63 +18,58 @@ settings.configure(
|
||||||
|
|
||||||
# Extensions.
|
# Extensions.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.autosummary',
|
"sphinx.ext.autosummary",
|
||||||
'sphinx.ext.doctest',
|
"sphinx.ext.doctest",
|
||||||
'sphinx.ext.coverage',
|
"sphinx.ext.coverage",
|
||||||
'sphinx.ext.intersphinx',
|
"sphinx.ext.intersphinx",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
# The suffix of source filenames.
|
# The suffix of source filenames.
|
||||||
source_suffix = '.txt'
|
source_suffix = ".txt"
|
||||||
|
|
||||||
# The encoding of source files.
|
# The encoding of source files.
|
||||||
source_encoding = 'utf-8'
|
source_encoding = "utf-8"
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'django-downloadview'
|
project = "django-downloadview"
|
||||||
project_slug = re.sub(r'([\w_.-]+)', u'-', project)
|
project_slug = re.sub(r"([\w_.-]+)", "-", project)
|
||||||
copyright = u'2012-2015, Benoît Bryon'
|
copyright = "2012-2015, Benoît Bryon"
|
||||||
author = u'Benoît Bryon'
|
author = "Benoît Bryon"
|
||||||
author_slug = re.sub(r'([\w_.-]+)', u'-', author)
|
author_slug = re.sub(r"([\w_.-]+)", "-", author)
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
configuration_dir = os.path.dirname(__file__)
|
|
||||||
documentation_dir = configuration_dir
|
|
||||||
version_file = os.path.normpath(os.path.join(
|
|
||||||
documentation_dir,
|
|
||||||
'../VERSION'))
|
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = open(version_file).read().strip()
|
release = importlib.metadata.version("django-downloadview")
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '.'.join(release.split('.')[0:1])
|
version = ".".join(release.split(".")[:2])
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
language = 'en'
|
language = "en"
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
exclude_patterns = ['_build']
|
exclude_patterns = ["_build"]
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output --------------------------------------------------
|
# -- Options for HTML output --------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'alabaster'
|
html_theme = "alabaster"
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
|
@ -83,23 +78,22 @@ html_static_path = []
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
# Custom sidebar templates, maps document names to template names.
|
||||||
html_sidebars = {
|
html_sidebars = {
|
||||||
'**': ['globaltoc.html',
|
"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"],
|
||||||
'relations.html',
|
|
||||||
'sourcelink.html',
|
|
||||||
'searchbox.html'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = u'{project}doc'.format(project=project_slug)
|
htmlhelp_basename = "{project}doc".format(project=project_slug)
|
||||||
|
|
||||||
|
|
||||||
# -- Options for sphinx.ext.intersphinx ---------------------------------------
|
# -- Options for sphinx.ext.intersphinx ---------------------------------------
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'python': ('http://docs.python.org/2.7', None),
|
"python": ("https://docs.python.org/3", None),
|
||||||
'django': ('http://docs.djangoproject.com/en/1.8/',
|
"django": (
|
||||||
'http://docs.djangoproject.com/en/1.8/_objects/'),
|
"https://docs.djangoproject.com/en/3.1/",
|
||||||
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
"https://docs.djangoproject.com/en/3.1/_objects/",
|
||||||
|
),
|
||||||
|
"requests": ("https://requests.readthedocs.io/en/master/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -111,11 +105,13 @@ latex_elements = {}
|
||||||
# (source start file, target name, title, author, documentclass
|
# (source start file, target name, title, author, documentclass
|
||||||
# [howto/manual]).
|
# [howto/manual]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('index',
|
(
|
||||||
u'{project}.tex'.format(project=project_slug),
|
"index",
|
||||||
u'{project} Documentation'.format(project=project),
|
"{project}.tex".format(project=project_slug),
|
||||||
author,
|
"{project} Documentation".format(project=project),
|
||||||
'manual'),
|
author,
|
||||||
|
"manual",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -124,11 +120,7 @@ latex_documents = [
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
('index',
|
("index", project, "{project} Documentation".format(project=project), [author], 1)
|
||||||
project,
|
|
||||||
u'{project} Documentation'.format(project=project),
|
|
||||||
[author],
|
|
||||||
1)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -138,11 +130,13 @@ man_pages = [
|
||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
('index',
|
(
|
||||||
project_slug,
|
"index",
|
||||||
u'{project} Documentation'.format(project=project),
|
project_slug,
|
||||||
author,
|
"{project} Documentation".format(project=project),
|
||||||
project,
|
author,
|
||||||
'One line description of project.',
|
project,
|
||||||
'Miscellaneous'),
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Here are tips to migrate from `django-sendfile` to `django-downloadview`...
|
||||||
* setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do
|
* setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do
|
||||||
more.
|
more.
|
||||||
* register ``django_downloadview.SmartDownloadMiddleware`` in
|
* register ``django_downloadview.SmartDownloadMiddleware`` in
|
||||||
``MIDDLEWARE_CLASSES``.
|
``MIDDLEWARE``.
|
||||||
|
|
||||||
4. Change your tests if any. You can no longer use `django-senfile`'s
|
4. Change your tests if any. You can no longer use `django-senfile`'s
|
||||||
``development`` backend. See :doc:`/testing` for `django-downloadview`'s
|
``development`` backend. See :doc:`/testing` for `django-downloadview`'s
|
||||||
|
|
@ -54,4 +54,4 @@ API reference
|
||||||
|
|
||||||
.. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile
|
.. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile
|
||||||
.. _`django-downloadview's bugtracker`:
|
.. _`django-downloadview's bugtracker`:
|
||||||
https://github.com/benoitbryon/django-downloadview/issues
|
https://github.com/jazzband/django-downloadview/issues
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Django's builtins
|
||||||
wrapper in :doc:`/views/path`.
|
wrapper in :doc:`/views/path`.
|
||||||
|
|
||||||
* :class:`django.db.models.fields.files.FieldFile` wraps a file that is
|
* :class:`django.db.models.fields.files.FieldFile` wraps a file that is
|
||||||
managed in a model. ``django-downloadview`` uses this wrapper in
|
managed in a model. ``django-downloadview`` uses this wrapper in
|
||||||
:doc:`/views/object`.
|
:doc:`/views/object`.
|
||||||
|
|
||||||
* :class:`django.core.files.base.ContentFile` wraps a bytes, string or
|
* :class:`django.core.files.base.ContentFile` wraps a bytes, string or
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ Setup XSendfile middlewares
|
||||||
***************************
|
***************************
|
||||||
|
|
||||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
``MIDDLEWARE`` of your `Django` settings.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
@ -128,4 +128,4 @@ setup.
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/
|
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,6 @@ Here are optimizations builtin `django_downloadview`:
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`tell us`:
|
.. _`tell us`:
|
||||||
https://github.com/benoitbryon/django-downloadview/issues?labels=optimizations
|
https://github.com/jazzband/django-downloadview/issues?labels=optimizations
|
||||||
.. _`a feature request about "local cache" for streamed files`:
|
.. _`a feature request about "local cache" for streamed files`:
|
||||||
https://github.com/benoitbryon/django-downloadview/issues/70
|
https://github.com/jazzband/django-downloadview/issues/70
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ Setup XSendfile middlewares
|
||||||
***************************
|
***************************
|
||||||
|
|
||||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
``MIDDLEWARE`` of your `Django` settings.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
@ -137,4 +137,4 @@ setup.
|
||||||
.. _`Lighttpd X-Sendfile documentation`:
|
.. _`Lighttpd X-Sendfile documentation`:
|
||||||
http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file
|
http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file
|
||||||
.. _`X-Sendfile2 feature request on django_downloadview's bugtracker`:
|
.. _`X-Sendfile2 feature request on django_downloadview's bugtracker`:
|
||||||
https://github.com/benoitbryon/django-downloadview/issues/67
|
https://github.com/jazzband/django-downloadview/issues/67
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ Let's consider the following view:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/nginx/views.py
|
.. literalinclude:: /../demo/demoproject/nginx/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-6, 8-16
|
:lines: 1-6, 8-17
|
||||||
|
|
||||||
What is important here is that the files will have an ``url`` property
|
What is important here is that the files will have an ``url`` property
|
||||||
implemented by storage. Let's setup an optimization rule based on that URL.
|
implemented by storage. Let's setup an optimization rule based on that URL.
|
||||||
|
|
@ -46,26 +46,26 @@ Setup XAccelRedirect middlewares
|
||||||
********************************
|
********************************
|
||||||
|
|
||||||
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
|
||||||
``MIDDLEWARE_CLASSES`` of your `Django` settings.
|
``MIDDLEWARE`` of your `Django` settings.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/settings.py
|
.. literalinclude:: /../demo/demoproject/settings.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 63-70
|
:lines: 62-69
|
||||||
|
|
||||||
Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as
|
Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as
|
||||||
``DOWNLOADVIEW_BACKEND``:
|
``DOWNLOADVIEW_BACKEND``:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/settings.py
|
.. literalinclude:: /../demo/demoproject/settings.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 76
|
:lines: 75
|
||||||
|
|
||||||
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
|
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/settings.py
|
.. literalinclude:: /../demo/demoproject/settings.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 84-89
|
:lines: 83-88
|
||||||
|
|
||||||
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
|
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
|
||||||
to the middleware factory. In the example above, we capture responses by
|
to the middleware factory. In the example above, we capture responses by
|
||||||
|
|
@ -142,7 +142,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
|
||||||
internal;
|
internal;
|
||||||
# Location to files on disk.
|
# Location to files on disk.
|
||||||
alias /var/www/files/;
|
alias /var/www/files/;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Proxy to Django-powered frontend.
|
# Proxy to Django-powered frontend.
|
||||||
location / {
|
location / {
|
||||||
|
|
@ -154,7 +154,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
|
||||||
}
|
}
|
||||||
|
|
||||||
... where specific configuration is the ``location /optimized-download``
|
... where specific configuration is the ``location /optimized-download``
|
||||||
section.
|
section.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
@ -183,7 +183,7 @@ Add ``charset utf-8;`` in your nginx configuration file.
|
||||||
``open() "path/to/something" failed (2: No such file or directory)``
|
``open() "path/to/something" failed (2: No such file or directory)``
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|
||||||
Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT`` in Django
|
Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR`` in Django
|
||||||
configuration VS ``alias`` in nginx configuration: in a standard configuration,
|
configuration VS ``alias`` in nginx configuration: in a standard configuration,
|
||||||
they should be equal.
|
they should be equal.
|
||||||
|
|
||||||
|
|
@ -192,4 +192,4 @@ they should be equal.
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ possible, file wrappers do not embed file data, in order to save memory.
|
||||||
|
|
||||||
Learn more about available file wrappers in :doc:`files`.
|
Learn more about available file wrappers in :doc:`files`.
|
||||||
|
|
||||||
|
|
||||||
*****************************************************************
|
*****************************************************************
|
||||||
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
||||||
*****************************************************************
|
*****************************************************************
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ There is no need to register this application in ``INSTALLED_APPS``.
|
||||||
|
|
||||||
|
|
||||||
******************
|
******************
|
||||||
MIDDLEWARE_CLASSES
|
MIDDLEWARE
|
||||||
******************
|
******************
|
||||||
|
|
||||||
If you plan to setup :doc:`reverse-proxy optimizations </optimizations/index>`,
|
If you plan to setup :doc:`reverse-proxy optimizations </optimizations/index>`,
|
||||||
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
|
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE``.
|
||||||
It is a response middleware. Move it after middlewares that compute the
|
It is a response middleware. Move it after middlewares that compute the
|
||||||
response content such as gzip middleware.
|
response content such as gzip middleware.
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ URLs, and they can then be verified with the supplied ``signature_required`` wra
|
||||||
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
download = ObjectDownloadView.as_view(model=Document, file_field='file')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('download/<str:slug>/', signature_required(download),
|
path('download/<str:slug>/', signature_required(download)),
|
||||||
]
|
]
|
||||||
|
|
||||||
Make sure to test the desired functionality after configuration.
|
Make sure to test the desired functionality after configuration.
|
||||||
|
|
@ -91,7 +91,7 @@ Example:
|
||||||
See :doc:`/optimizations/index` for a list of available backends (middlewares).
|
See :doc:`/optimizations/index` for a list of available backends (middlewares).
|
||||||
|
|
||||||
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
||||||
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
|
``MIDDLEWARE``, this setting must be explicitely configured (no default
|
||||||
value). Else, you can ignore this setting.
|
value). Else, you can ignore this setting.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,5 +119,5 @@ See :doc:`/optimizations/index` for details about builtin backends
|
||||||
(middlewares) and their options.
|
(middlewares) and their options.
|
||||||
|
|
||||||
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
When ``django_downloadview.SmartDownloadMiddleware`` is in your
|
||||||
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
|
``MIDDLEWARE``, this setting must be explicitely configured (no default
|
||||||
value). Else, you can ignore this setting.
|
value). Else, you can ignore this setting.
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,3 @@ Example, related to :doc:`StorageDownloadView demo </views/storage>`:
|
||||||
.. literalinclude:: /../demo/demoproject/storage/tests.py
|
.. literalinclude:: /../demo/demoproject/storage/tests.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-2, 8-12, 59-
|
:lines: 1-2, 8-12, 59-
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ Modified" response:
|
||||||
class TextDownloadView(VirtualDownloadView):
|
class TextDownloadView(VirtualDownloadView):
|
||||||
def get_file(self):
|
def get_file(self):
|
||||||
"""Return :class:`django.core.files.base.ContentFile` object."""
|
"""Return :class:`django.core.files.base.ContentFile` object."""
|
||||||
return ContentFile(u"Hello world!", name='hello-world.txt')
|
return ContentFile("Hello world!", name='hello-world.txt')
|
||||||
|
|
||||||
def was_modified_since(self, file_instance, since):
|
def was_modified_since(self, file_instance, since):
|
||||||
return False # Never modified, always u"Hello world!".
|
return False # Never modified, always "Hello world!".
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Setup a view to stream the ``file`` attribute:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-5, 7
|
:lines: 1-6
|
||||||
|
|
||||||
:class:`~django_downloadview.views.object.ObjectDownloadView` inherits from
|
:class:`~django_downloadview.views.object.ObjectDownloadView` inherits from
|
||||||
:class:`~django.views.generic.detail.BaseDetailView`, i.e. it expects either
|
:class:`~django.views.generic.detail.BaseDetailView`, i.e. it expects either
|
||||||
|
|
@ -38,7 +38,7 @@ Setup a view to stream the ``file`` attribute:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/object/urls.py
|
.. literalinclude:: /../demo/demoproject/object/urls.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-7, 8-10, 20
|
:lines: 1-7, 8-11, 27
|
||||||
|
|
||||||
|
|
||||||
************
|
************
|
||||||
|
|
@ -69,7 +69,7 @@ Then here is the code to serve "another_file" instead of the default "file":
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-5, 10-12
|
:lines: 1-4, 8-11
|
||||||
|
|
||||||
|
|
||||||
**********************************
|
**********************************
|
||||||
|
|
@ -90,7 +90,7 @@ Then you can configure the :attr:`ObjectDownloadView.basename_field` option:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/object/views.py
|
.. literalinclude:: /../demo/demoproject/object/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-5, 16-18
|
:lines: 1-4, 13-17
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ Setup a view to stream files given path:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/path/views.py
|
.. literalinclude:: /../demo/demoproject/path/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-14
|
:lines: 1-13
|
||||||
:emphasize-lines: 14
|
:emphasize-lines: 13
|
||||||
|
|
||||||
|
|
||||||
************
|
************
|
||||||
|
|
@ -54,7 +54,7 @@ via URLconfs:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/path/urls.py
|
.. literalinclude:: /../demo/demoproject/path/urls.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-7, 11-13, 14
|
:lines: 1-13
|
||||||
|
|
||||||
|
|
||||||
*************
|
*************
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,20 @@ Given a storage:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/storage/views.py
|
.. literalinclude:: /../demo/demoproject/storage/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1, 4-6
|
:lines: 1, 4-5
|
||||||
|
|
||||||
Setup a view to stream files in storage:
|
Setup a view to stream files in storage:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/storage/views.py
|
.. literalinclude:: /../demo/demoproject/storage/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 3-5, 10
|
:lines: 3-6, 8-9
|
||||||
|
|
||||||
The view accepts a ``path`` argument you can setup either in ``as_view`` or
|
The view accepts a ``path`` argument you can setup either in ``as_view`` or
|
||||||
via URLconfs:
|
via URLconfs:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/storage/urls.py
|
.. literalinclude:: /../demo/demoproject/storage/urls.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 1-7, 8-10, 14
|
:lines: 1-6, 7-11, 17
|
||||||
|
|
||||||
|
|
||||||
************
|
************
|
||||||
|
|
@ -56,7 +56,7 @@ uppercase:
|
||||||
|
|
||||||
.. literalinclude:: /../demo/demoproject/storage/views.py
|
.. literalinclude:: /../demo/demoproject/storage/views.py
|
||||||
:language: python
|
:language: python
|
||||||
:lines: 3-5, 13-20
|
:lines: 3-5, 11-20
|
||||||
|
|
||||||
|
|
||||||
*************
|
*************
|
||||||
|
|
|
||||||
149
setup.py
149
setup.py
|
|
@ -1,94 +1,67 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""Python packaging."""
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from setuptools.command.test import test as TestCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Tox(TestCommand):
|
|
||||||
"""Test command that runs tox."""
|
|
||||||
def finalize_options(self):
|
|
||||||
TestCommand.finalize_options(self)
|
|
||||||
self.test_args = []
|
|
||||||
self.test_suite = True
|
|
||||||
|
|
||||||
def run_tests(self):
|
|
||||||
import tox # import here, cause outside the eggs aren't loaded.
|
|
||||||
|
|
||||||
errno = tox.cmdline(self.test_args)
|
|
||||||
sys.exit(errno)
|
|
||||||
|
|
||||||
|
|
||||||
#: Absolute path to directory containing setup.py file.
|
#: Absolute path to directory containing setup.py file.
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
setup(
|
||||||
NAME = 'django-downloadview'
|
name="django-downloadview",
|
||||||
DESCRIPTION = 'Serve files with Django and reverse-proxies.'
|
use_scm_version={"version_scheme": "post-release"},
|
||||||
README = open(os.path.join(here, 'README.rst')).read()
|
setup_requires=["setuptools_scm"],
|
||||||
VERSION = open(os.path.join(here, 'VERSION')).read().strip()
|
description="Serve files with Django and reverse-proxies.",
|
||||||
AUTHOR = u'Benoît Bryon'
|
long_description=open(os.path.join(here, "README.rst")).read(),
|
||||||
EMAIL = 'benoit@marmelune.net'
|
long_description_content_type="text/x-rst",
|
||||||
LICENSE = 'BSD'
|
classifiers=[
|
||||||
URL = 'https://{name}.readthedocs.io/'.format(name=NAME)
|
"Development Status :: 5 - Production/Stable",
|
||||||
CLASSIFIERS = [
|
"License :: OSI Approved :: BSD License",
|
||||||
'Development Status :: 5 - Production/Stable',
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
'Framework :: Django',
|
"Programming Language :: Python :: 3.8",
|
||||||
'License :: OSI Approved :: BSD License',
|
"Programming Language :: Python :: 3.9",
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
"Programming Language :: Python :: 3.10",
|
||||||
'Programming Language :: Python :: 3.6',
|
"Programming Language :: Python :: 3.11",
|
||||||
'Programming Language :: Python :: 3.7',
|
"Programming Language :: Python :: 3.12",
|
||||||
'Programming Language :: Python :: 3.8',
|
"Programming Language :: Python :: 3.13",
|
||||||
]
|
"Programming Language :: Python :: 3.14",
|
||||||
KEYWORDS = ['file',
|
"Framework :: Django",
|
||||||
'stream',
|
"Framework :: Django :: 4.2",
|
||||||
'download',
|
"Framework :: Django :: 5.0",
|
||||||
'FileField',
|
],
|
||||||
'ImageField',
|
keywords=" ".join(
|
||||||
'x-accel',
|
[
|
||||||
'x-accel-redirect',
|
"file",
|
||||||
'x-sendfile',
|
"stream",
|
||||||
'sendfile',
|
"download",
|
||||||
'mod_xsendfile',
|
"FileField",
|
||||||
'offload']
|
"ImageField",
|
||||||
PACKAGES = [NAME.replace('-', '_')]
|
"x-accel",
|
||||||
REQUIREMENTS = [
|
"x-accel-redirect",
|
||||||
# BEGIN requirements
|
"x-sendfile",
|
||||||
'Django>=1.11',
|
"sendfile",
|
||||||
'requests',
|
"mod_xsendfile",
|
||||||
'setuptools',
|
"offload",
|
||||||
# END requirements
|
]
|
||||||
]
|
),
|
||||||
ENTRY_POINTS = {}
|
author="Benoît Bryon",
|
||||||
SETUP_REQUIREMENTS = ['setuptools']
|
author_email="benoit@marmelune.net",
|
||||||
TEST_REQUIREMENTS = ['tox']
|
url="https://django-downloadview.readthedocs.io/",
|
||||||
CMDCLASS = {'test': Tox}
|
license="BSD",
|
||||||
EXTRA_REQUIREMENTS = {
|
packages=[
|
||||||
'test': TEST_REQUIREMENTS,
|
"django_downloadview",
|
||||||
}
|
"django_downloadview.apache",
|
||||||
|
"django_downloadview.lighttpd",
|
||||||
|
"django_downloadview.nginx",
|
||||||
if __name__ == '__main__': # Don't run setup() when we import this module.
|
"django_downloadview.views",
|
||||||
setup(
|
],
|
||||||
name=NAME,
|
include_package_data=True,
|
||||||
version=VERSION,
|
zip_safe=False,
|
||||||
description=DESCRIPTION,
|
python_requires=">=3.8",
|
||||||
long_description=README,
|
install_requires=[
|
||||||
classifiers=CLASSIFIERS,
|
# BEGIN requirements
|
||||||
keywords=' '.join(KEYWORDS),
|
"Django>=4.2",
|
||||||
author=AUTHOR,
|
"requests",
|
||||||
author_email=EMAIL,
|
# END requirements
|
||||||
url=URL,
|
],
|
||||||
license=LICENSE,
|
extras_require={
|
||||||
packages=PACKAGES,
|
"test": ["tox"],
|
||||||
include_package_data=True,
|
},
|
||||||
zip_safe=False,
|
)
|
||||||
install_requires=REQUIREMENTS,
|
|
||||||
entry_points=ENTRY_POINTS,
|
|
||||||
tests_require=TEST_REQUIREMENTS,
|
|
||||||
cmdclass=CMDCLASS,
|
|
||||||
setup_requires=SETUP_REQUIREMENTS,
|
|
||||||
extras_require=EXTRA_REQUIREMENTS,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Test suite around :mod:`django_downloadview.api` and deprecation plan."""
|
"""Test suite around :mod:`django_downloadview.api` and deprecation plan."""
|
||||||
|
|
||||||
from importlib import import_module, reload
|
from importlib import import_module, reload
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
@ -100,7 +101,6 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
|
||||||
def test_nginx_x_accel_redirect_middleware(self):
|
def test_nginx_x_accel_redirect_middleware(self):
|
||||||
"XAccelRedirectMiddleware in settings triggers ImproperlyConfigured."
|
"XAccelRedirectMiddleware in settings triggers ImproperlyConfigured."
|
||||||
with override_settings(
|
with override_settings(
|
||||||
MIDDLEWARE_CLASSES=["django_downloadview.nginx.XAccelRedirectMiddleware"],
|
|
||||||
MIDDLEWARE=["django_downloadview.nginx.XAccelRedirectMiddleware"],
|
MIDDLEWARE=["django_downloadview.nginx.XAccelRedirectMiddleware"],
|
||||||
):
|
):
|
||||||
with self.assertRaises(ImproperlyConfigured):
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
|
|
@ -131,7 +131,7 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
|
||||||
reload(django_downloadview.nginx.settings)
|
reload(django_downloadview.nginx.settings)
|
||||||
caught = False
|
caught = False
|
||||||
for warning_item in warning_list:
|
for warning_item in warning_list:
|
||||||
if warning_item.category == DeprecationWarning:
|
if warning_item.category is DeprecationWarning:
|
||||||
if "deprecated" in str(warning_item.message):
|
if "deprecated" in str(warning_item.message):
|
||||||
if setting_name in str(warning_item.message):
|
if setting_name in str(warning_item.message):
|
||||||
caught = True
|
caught = True
|
||||||
|
|
@ -141,5 +141,5 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
|
||||||
if missed_warnings:
|
if missed_warnings:
|
||||||
self.fail(
|
self.fail(
|
||||||
f"No DeprecationWarning raised about following settings: "
|
f"No DeprecationWarning raised about following settings: "
|
||||||
f'{", ".join(missed_warnings)}.'
|
f"{', '.join(missed_warnings)}."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests around :mod:`django_downloadview.io`."""
|
"""Tests around :mod:`django_downloadview.io`."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django_downloadview import BytesIteratorIO, TextIteratorIO
|
from django_downloadview import BytesIteratorIO, TextIteratorIO
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
"""Tests around project's distribution and packaging."""
|
"""Tests around project's distribution and packaging."""
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
@ -24,37 +26,15 @@ class VersionTestCase(unittest.TestCase):
|
||||||
self.fail("django_downloadview package has no __version__.")
|
self.fail("django_downloadview package has no __version__.")
|
||||||
|
|
||||||
def test_version_match(self):
|
def test_version_match(self):
|
||||||
"""django_downloadview.__version__ matches pkg_resources info."""
|
"""django_downloadview.__version__ matches importlib metadata."""
|
||||||
try:
|
distribution = importlib.metadata.distribution("django-downloadview")
|
||||||
import pkg_resources
|
|
||||||
except ImportError:
|
|
||||||
self.fail(
|
|
||||||
"Cannot import pkg_resources module. It is part of "
|
|
||||||
"setuptools, which is a dependency of "
|
|
||||||
"django_downloadview."
|
|
||||||
)
|
|
||||||
distribution = pkg_resources.get_distribution("django-downloadview")
|
|
||||||
installed_version = distribution.version
|
installed_version = distribution.version
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
installed_version,
|
installed_version,
|
||||||
self.get_version(),
|
self.get_version(),
|
||||||
"Version mismatch: django_downloadview.__version__ "
|
"Version mismatch: django_downloadview.__version__ "
|
||||||
'is "%s" whereas pkg_resources tells "%s". '
|
'is "%s" whereas importlib.metadata tells "%s". '
|
||||||
"You may need to run ``make develop`` to update the "
|
"You may need to run ``make develop`` to update the "
|
||||||
"installed version in development environment."
|
"installed version in development environment."
|
||||||
% (self.get_version(), installed_version),
|
% (self.get_version(), installed_version),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_version_file(self):
|
|
||||||
"""django_downloadview.__version__ matches VERSION file info."""
|
|
||||||
version_file = os.path.join(project_dir, "VERSION")
|
|
||||||
file_version = open(version_file).read().strip()
|
|
||||||
self.assertEqual(
|
|
||||||
file_version,
|
|
||||||
self.get_version(),
|
|
||||||
"Version mismatch: django_downloadview.__version__ "
|
|
||||||
'is "%s" whereas VERSION file tells "%s". '
|
|
||||||
"You may need to run ``make develop`` to update the "
|
|
||||||
"installed version in development environment."
|
|
||||||
% (self.get_version(), file_version),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Unit tests around responses."""
|
"""Unit tests around responses."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django_downloadview.response import DownloadResponse
|
from django_downloadview.response import DownloadResponse
|
||||||
|
|
@ -10,10 +11,22 @@ class DownloadResponseTestCase(unittest.TestCase):
|
||||||
def test_content_disposition_encoding(self):
|
def test_content_disposition_encoding(self):
|
||||||
"""Content-Disposition header is encoded."""
|
"""Content-Disposition header is encoded."""
|
||||||
response = DownloadResponse(
|
response = DownloadResponse(
|
||||||
"fake file", attachment=True, basename="espacé .txt",
|
"fake file",
|
||||||
|
attachment=True,
|
||||||
|
basename="espacé .txt",
|
||||||
)
|
)
|
||||||
headers = response.default_headers
|
headers = response.default_headers
|
||||||
self.assertIn('filename="espace_.txt"', headers["Content-Disposition"])
|
self.assertIn('filename="espace_.txt"', headers["Content-Disposition"])
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"filename*=UTF-8''espac%C3%A9%20.txt", headers["Content-Disposition"]
|
"filename*=UTF-8''espac%C3%A9%20.txt", headers["Content-Disposition"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_content_disposition_escaping(self):
|
||||||
|
"""Content-Disposition headers escape special characters."""
|
||||||
|
response = DownloadResponse(
|
||||||
|
"fake file", attachment=True, basename=r'"malicious\file.exe'
|
||||||
|
)
|
||||||
|
headers = response.default_headers
|
||||||
|
self.assertIn(
|
||||||
|
r'filename="\"malicious\\file.exe"', headers["Content-Disposition"]
|
||||||
|
)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue