Compare commits

..

70 commits

Author SHA1 Message Date
Rémy HUBSCHER
969fb7931a
Merge pull request #228 from jazzband/pre-commit-ci-update-config 2025-11-17 21:38:55 +01:00
Rémy HUBSCHER
fe610b9c9f
Apply suggestion from @Natim 2025-11-17 21:38:36 +01:00
pre-commit-ci[bot]
187787d083
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-eslint: v9.39.1 → v10.0.0-alpha.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.39.1...v10.0.0-alpha.0)
- [github.com/astral-sh/ruff-pre-commit: v0.14.4 → v0.14.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.4...v0.14.5)
2025-11-17 17:30:11 +00:00
Rémy HUBSCHER
614bc522d3
Merge pull request #227 from jazzband/pre-commit-ci-update-config 2025-11-12 11:29:07 +01:00
pre-commit-ci[bot]
34a83fd09b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-eslint: v9.38.0 → v9.39.1](https://github.com/pre-commit/mirrors-eslint/compare/v9.38.0...v9.39.1)
- [github.com/astral-sh/ruff-pre-commit: v0.14.2 → v0.14.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.2...v0.14.4)
- [github.com/tox-dev/pyproject-fmt: v2.11.0 → v2.11.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.0...v2.11.1)
2025-11-10 17:38:43 +00:00
Rémy Hubscher
415b76622e
Post release 2025-10-28 11:34:20 +01:00
Rémy HUBSCHER
982432a05b
Merge pull request #226 from jazzband/release/2.5.0 2025-10-28 11:31:52 +01:00
Rémy Hubscher
055ef690d2
Release 2.5.0 2025-10-28 11:23:58 +01:00
Rémy HUBSCHER
94b4a60917
Merge pull request #224 from jazzband/fix/upgrade-to-django-52 2025-10-28 11:19:59 +01:00
Rémy HUBSCHER
9194e6a11c
Merge pull request #225 from jazzband/pre-commit-ci-update-config 2025-10-28 11:19:37 +01:00
pre-commit-ci[bot]
140b631d5b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/adamchainz/django-upgrade: 1.29.0 → 1.29.1](https://github.com/adamchainz/django-upgrade/compare/1.29.0...1.29.1)
- [github.com/astral-sh/ruff-pre-commit: v0.14.1 → v0.14.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.1...v0.14.2)
2025-10-27 17:25:13 +00:00
Rémy Hubscher
e3ebf67457
Upgrade test matrix 2025-10-21 10:36:39 +02:00
Rémy Hubscher
5f2ae9a9b0
Add test matrix 2025-10-21 09:53:13 +02:00
Rémy Hubscher
f1a07c226e
Upgrade to Django 5.2 and Python 3.14 2025-10-21 09:52:06 +02:00
Rémy HUBSCHER
9b90a3326d
Merge pull request #223 from jazzband/dependabot/github_actions/codecov/codecov-action-5 2025-10-21 09:51:50 +02:00
Rémy HUBSCHER
45fb86b52c
Merge pull request #222 from jazzband/dependabot/github_actions/actions/setup-python-6 2025-10-21 09:51:40 +02:00
Rémy HUBSCHER
5dc018f17b
Merge pull request #221 from jazzband/dependabot/github_actions/actions/cache-4 2025-10-21 09:51:28 +02:00
Rémy HUBSCHER
d515804708
Merge pull request #220 from jazzband/dependabot/github_actions/actions/checkout-5 2025-10-21 09:51:13 +02:00
dependabot[bot]
4e580980fe
Bump codecov/codecov-action from 1 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 07:46:40 +00:00
dependabot[bot]
9b77252326
Bump actions/setup-python from 2 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 07:46:37 +00:00
dependabot[bot]
f9cc2c5fab
Bump actions/cache from 2 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 07:46:33 +00:00
dependabot[bot]
ec0b34df5b
Bump actions/checkout from 2 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 07:46:30 +00:00
Rémy HUBSCHER
9e22eb8d8f
Merge pull request #219 from jazzband/pre-commit-ci-update-config 2025-10-21 09:45:49 +02:00
Rémy Hubscher
c239015d1a
Update github actions version. 2025-10-21 09:45:36 +02:00
pre-commit-ci[bot]
73f9c013cd [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-10-20 19:50:02 +00:00
pre-commit-ci[bot]
840827da1d
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/pycqa/doc8: v1.1.2 → v2.0.0](https://github.com/pycqa/doc8/compare/v1.1.2...v2.0.0)
- [github.com/adamchainz/django-upgrade: 1.22.2 → 1.29.0](https://github.com/adamchainz/django-upgrade/compare/1.22.2...1.29.0)
- [github.com/pre-commit/mirrors-eslint: v9.17.0 → v9.38.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.17.0...v9.38.0)
- [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.14.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.14.1)
- [github.com/tox-dev/pyproject-fmt: v2.5.0 → v2.11.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.5.0...v2.11.0)
- [github.com/abravalheri/validate-pyproject: v0.23 → v0.24.1](https://github.com/abravalheri/validate-pyproject/compare/v0.23...v0.24.1)
2025-10-20 17:24:32 +00:00
Rémy HUBSCHER
31e64b6a76
Merge pull request #218 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-01-07 08:50:48 +01:00
pre-commit-ci[bot]
71a1670703
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6)
2025-01-06 17:54:28 +00:00
Rémy HUBSCHER
dff487aca9
Merge pull request #217 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-12-24 14:42:55 +01:00
pre-commit-ci[bot]
7aa9b687aa
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
2024-12-23 17:46:59 +00:00
Rémy HUBSCHER
b02e2f13f9
Merge pull request #216 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-12-16 21:37:50 +01:00
pre-commit-ci[bot]
a7d182f0b4
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-eslint: v9.16.0 → v9.17.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.16.0...v9.17.0)
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)
2024-12-16 17:45:43 +00:00
Rémy HUBSCHER
c8486417bb
Merge pull request #215 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-12-13 09:38:59 +01:00
pre-commit-ci[bot]
ebcd3a0028
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/pycqa/doc8: v1.1.1 → v1.1.2](https://github.com/pycqa/doc8/compare/v1.1.1...v1.1.2)
- [github.com/adamchainz/django-upgrade: 1.20.0 → 1.22.2](https://github.com/adamchainz/django-upgrade/compare/1.20.0...1.22.2)
- [github.com/pre-commit/mirrors-eslint: v9.8.0 → v9.16.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.8.0...v9.16.0)
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.8.2)
- [github.com/tox-dev/pyproject-fmt: 2.2.1 → v2.5.0](https://github.com/tox-dev/pyproject-fmt/compare/2.2.1...v2.5.0)
- [github.com/abravalheri/validate-pyproject: v0.18 → v0.23](https://github.com/abravalheri/validate-pyproject/compare/v0.18...v0.23)
2024-12-09 17:44:37 +00:00
Rémy HUBSCHER
6c0e0a8c82
Merge pull request #214 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-06 09:26:57 +02:00
pre-commit-ci[bot]
d3a8f6b725
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6)
- [github.com/tox-dev/pyproject-fmt: 2.1.4 → 2.2.1](https://github.com/tox-dev/pyproject-fmt/compare/2.1.4...2.2.1)
2024-08-05 17:54:35 +00:00
Rémy HUBSCHER
13a502bc4a
Post release 2.4.0 2024-08-05 14:51:35 +02:00
Rémy HUBSCHER
9f42cde8cb
Merge pull request #213 from jazzband/prepare-2.4
Prepare 2.4 release
2024-08-05 14:49:32 +02:00
Rémy HUBSCHER
5c8bbda4b3
Update AUTHORS 2024-08-05 14:03:55 +02:00
Rémy HUBSCHER
eab4fa7abf
Deduplicate names 2024-08-05 11:00:37 +02:00
Rémy HUBSCHER
711b2e50b5
Run pre-commit on all files 2024-08-05 10:53:19 +02:00
Rémy HUBSCHER
dd35f867e0
Add .pre-commit-config.yaml file 2024-08-05 10:51:09 +02:00
Rémy HUBSCHER
402c77b332
Add readthedocs config 2024-08-05 10:50:24 +02:00
Rémy HUBSCHER
347f0202ab
Prepare 2.4 release 2024-08-05 10:45:59 +02:00
Rémy HUBSCHER
18cb41f760
Merge pull request #211 from tari/196-rfd
Guard against reflected file download
2024-08-05 10:26:15 +02:00
Rémy HUBSCHER
1b294f00fa
Merge pull request #212 from sevdog/xaccel-headers
Allow XResponses to keep original headers provided to base response
2024-08-05 10:23:48 +02:00
Davide
51deef0a7e
Allow XResponses to keep original headers provided to base response 2024-08-01 08:35:53 +02:00
Peter Marheine
e7e25e68dd Add missing import in packaging test
This test was broken when changed to begin using importlib,
but that wasn't evident because the tests directory
wasn't being automatically tested.
2024-08-01 06:28:06 +00:00
Peter Marheine
0568c3c559 Prevent reflected file downloads on specially-named files
This fixes #196, where it was observed that django_downloadview
was vulnerable to reflected file download attacks with
specially-named files, similar to CVE-2022-36359 in Django.
This change adopts the same replacement rules as used in Django's fix
in commit b3e4494d759202a3b6bf247fd34455bf13be5b80.
2024-08-01 06:24:00 +00:00
Peter Marheine
e2b4470c5b Ensure tests are actually run
pytest by default only discovers tests in files named test_*.py,
so none of the tests were actually being executed. Set the appropriate
pytest option
to discover the tests so they are automatically run.
2024-08-01 06:24:00 +00:00
Peter Marheine
71488c49c4
Merge pull request #204 from sevdog/fix-realdownload-check
Use safer check in RealDownloadMiddleware
2024-08-01 09:53:07 +10:00
Peter Marheine
6c7b7f8a31
Merge pull request #209 from tari/pkg-resources
Replace use of pkg_resources (setuptools)
2024-08-01 09:50:07 +10:00
Davide
d385cbba6f
Use hasattr to check if any of required attribute is present 2024-07-31 16:18:20 +02:00
Peter Marheine
60c1839bf5 Replace use of pkg_resources (setuptools)
Since Python 3.12, setuptools isn't included with Python
and importlib is the recommended replacement, available
since Python 3.8.
2024-07-31 11:41:25 +00:00
Peter Marheine
ba6be8c3cd
Merge pull request #210 from tari/django-4.2
Upgrade support matrix to maintained versions of Django
2024-07-31 21:05:14 +10:00
Peter Marheine
16b241d9b5
Merge pull request #206 from sevdog/upgrade-middleware-doc
Update usage of middleware settings
2024-07-30 22:07:22 +10:00
Davide
c51720296a
Update references to middleware settings 2024-07-30 13:01:56 +02:00
Peter Marheine
fba10f7b1b
Merge pull request #207 from sevdog/improve-py3-code
Improve codebase to python3
2024-07-30 20:20:14 +10:00
Peter Marheine
41caa79f46 Upgrade support matrix to maintained versions of Django
Currently, only Django versions 4.2 and 5.0 are maintained and
collectively only support Python down to version 3.8. This change updates the support matrix
for this package to match.
2024-07-30 10:03:32 +00:00
Davide
ff5073d00b
Use python3 super and remove useless method re-definitions 2023-09-26 11:26:43 +02:00
Rémy HUBSCHER
338e17195f
Merge pull request #194 from felixxm/was_modified_since_size
Removed passing unused size parameter to was_modified_since().
2022-08-05 09:38:56 +02:00
Rémy HUBSCHER
df439fbd4f
Merge pull request #199 from jazzband/johnthagen-patch-1
Enforce minimum Python version
2022-08-05 09:37:00 +02:00
johnthagen
dd2e148b05
Enforce minimum Python version 2022-08-04 13:23:07 -04:00
Rémy HUBSCHER
64e36826ff
Merge pull request #197 from johnthagen/drop-py3.6
Drop Python 3.6 support
2022-08-04 16:10:02 +02:00
johnthagen
d19e4bee50
Update GitHub Actions 2022-08-04 09:23:23 -04:00
johnthagen
8c74a77ebe Drop Python 3.6 support 2022-08-04 09:22:28 -04:00
Rémy HUBSCHER
a6975d9669
Merge pull request #195 from devidw/patch-1
Add missing `)` to settings docs sample
2022-08-04 14:26:22 +02:00
David Wolf
f715f72032
Add missing ) to settings docs sample 2022-04-14 17:02:49 +02:00
Mariusz Felisiak
293403b807 Removed passing unused size parameter to was_modified_since().
The size parameter is unused because we pass timestamp and not the
If-Modified-Since HTML header.
2022-03-14 20:38:28 +01:00
jazzband-bot
b64b1ad21a Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2022-02-17 11:46:17 +01:00
77 changed files with 476 additions and 198 deletions

8
.github/dependabot.yml vendored Normal file
View file

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

View file

@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: 3.8

View file

@ -9,29 +9,24 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
django-version: ['2.2', '3.1', '3.2', '4.0', 'main']
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 prior to 3.2 does not support Python 3.10
- django-version: '2.2'
python-version: '3.10'
- django-version: '3.1'
python-version: '3.10'
# Django after 3.2 dropped support for Python prior to 3.8
- django-version: '4.0'
python-version: '3.6'
# 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.6'
- django-version: '4.0'
python-version: '3.7'
python-version: '3.8'
- django-version: 'main'
python-version: '3.7'
python-version: '3.9'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@ -41,7 +36,7 @@ jobs:
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
@ -61,6 +56,6 @@ jobs:
DJANGO: ${{ matrix.django-version }}
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

59
.pre-commit-config.yaml Normal file
View 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
View 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: .

21
AUTHORS
View file

@ -6,22 +6,23 @@ Maintainer: Benoît Bryon <benoit@marmelune.net>
Original code by `PeopleDoc <https://www.people-doc.com/>`_ team:
* Nicolas Tobo <https://github.com/nicolastobo>
* Lauréline Guérin <https://github.com/zebuline>
* Gregory Tappero <https://github.com/gregtap>
* Rémy Hubscher <https://github.com/natim>
* Adam Chainz <adam@adamj.eu>
* Aleksi Häkli <aleksi.hakli@iki.fi>
* Benoît Bryon <benoit@marmelune.net>
* Aleksi Häkli <https://github.com/aleksihakli>
* Johnt Hagen <johnthagen@gmail.com>
* CJ <cjdreiss@users.noreply.github.com>
* David Wolf <68775926+devidw@users.noreply.github.com>
* Davide Setti <setti.davide89@gmail.com>
* Erik Dykema <dykema@gmail.com>
* Fabre Florian <ffabre@hybird.org>
* Peter Marheine <peter@taricorp.net>
* Hasan Ramezani <hasan.r67@gmail.com>
* Jannis Leidel <jannis@leidel.info>
* Erik Dykema <dykema@gmail.com>
* 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>
* Adam Chainz <adam@adamj.eu>
* Martin Bächtold <martin@baechtold.me>
* Rémy HUBSCHER <hubscher.remy@gmail.com>
* Tim Gates <tim.gates@iress.com>
* zero13cool <zero13cool@yandex.ru>

View file

@ -4,18 +4,33 @@ Changelog
This document describes changes between past releases. For information about
future releases, check `milestones`_ and :doc:`/about/vision`.
2.3 (unreleased)
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
2.2 (unreleased)
----------------
- Remove support for Python 3.5 and Django 1.11
- Add support for Python 3.9 and Django 3.1
- Remove old urls syntax and adopt the new one

46
CODE_OF_CONDUCT.md Normal file
View 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/

View file

@ -11,7 +11,7 @@ This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree t
This document provides guidelines for people who want to contribute to
`django-downloadview`.
``django-downloadview``.
**************
@ -50,7 +50,7 @@ Use topic branches
Fork, clone
***********
Clone `django-downloadview` repository (adapt to use your own fork):
Clone ``django-downloadview`` repository (adapt to use your own fork):
.. code:: sh
@ -62,7 +62,7 @@ Clone `django-downloadview` repository (adapt to use your own fork):
Usual actions
*************
The `Makefile` is the reference card for usual actions in development
The ``Makefile`` is the reference card for usual actions in development
environment:
* Install development toolkit with `pip`_: ``make develop``.
@ -70,7 +70,7 @@ environment:
* Run tests with `tox`_: ``make test``.
* Build documentation: ``make documentation``. It builds `Sphinx`_
documentation in `var/docs/html/index.html`.
documentation in ``var/docs/html/index.html``.
* Release project with `zest.releaser`_: ``make release``.
@ -84,7 +84,7 @@ See also ``make help``.
Demo project included
*********************
The `demo` included in project's repository is part of the tests and
The ``demo`` included in project's repository is part of the tests and
documentation. Maintain it along with code and documentation.
@ -92,7 +92,7 @@ documentation. Maintain it along with code and documentation.
.. target-notes::
.. _`bugtracker`:
.. _`bugtracker`:
https://github.com/jazzband/django-downloadview/issues
.. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing
.. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html

View file

@ -12,8 +12,8 @@ Install
Requirements
************
`django-downloadview` has been tested with `Python`_ 3.6, 3.7 and 3.8. Other
versions may work, but they are not part of the test suite at the moment.
`django-downloadview` has been tested with `Python`_ 3.7, 3.8, 3.9 and 3.10.
Other versions may work, but they are not part of the test suite at the moment.
Installing `django-downloadview` will automatically trigger the installation of
the following requirements:

View file

@ -26,16 +26,16 @@ django-downloadview
:target: https://codecov.io/gh/jazzband/django-downloadview
:alt: Coverage
`django-downloadview` makes it easy to serve files with `Django`_:
``django-downloadview`` makes it easy to serve files with `Django`_:
* you manage files with Django (permissions, filters, generation, ...);
* files are stored somewhere or generated somehow (local filesystem, remote
storage, memory...);
* `django-downloadview` helps you stream the files with very little code;
* ``django-downloadview`` helps you stream the files with very little code;
* `django-downloadview` helps you improve performances with reverse proxies,
* ``django-downloadview`` helps you improve performances with reverse proxies,
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.

View file

@ -3,7 +3,7 @@ Demo project
############
`Demo folder in project's repository`_ contains a Django project to illustrate
`django-downloadview` usage.
``django-downloadview`` usage.
*****************************************
@ -31,8 +31,8 @@ Deploy the demo
System requirements:
* `Python`_ version 3.6+, available as ``python`` command.
* `Python`_ version 3.7+, available as ``python`` command.
.. note::
You may use `Virtualenv`_ to make sure the active ``python`` is the right

View file

@ -43,3 +43,19 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
basename="hello-world.txt",
file_path="/apache-optimized-by-decorator/hello-world.txt",
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'apache:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("apache:modified_headers")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/apache-modified-headers/hello-world.txt",
)
self.assertEqual(response["X-Test"], "header")

View file

@ -1,4 +1,5 @@
"""URL mapping."""
from django.urls import path
from demoproject.apache import views
@ -15,4 +16,9 @@ urlpatterns = [
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -22,3 +22,17 @@ optimized_by_decorator = x_sendfile(
source_url=storage.base_url,
destination_dir="/apache-optimized-by-decorator/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_sendfile(
_modified_headers,
source_url=storage.base_url,
destination_dir="/apache-modified-headers/",
)

View file

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

View file

@ -43,3 +43,19 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
basename="hello-world.txt",
file_path="/lighttpd-optimized-by-decorator/hello-world.txt",
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("lighttpd:modified_headers")
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-modified-headers/hello-world.txt",
)
self.assertEqual(response["X-Test"], "header")

View file

@ -1,4 +1,5 @@
"""URL mapping."""
from django.urls import path
from demoproject.lighttpd import views
@ -15,4 +16,9 @@ urlpatterns = [
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -22,3 +22,17 @@ optimized_by_decorator = x_sendfile(
source_url=storage.base_url,
destination_dir="/lighttpd-optimized-by-decorator/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_sendfile(
_modified_headers,
source_url=storage.base_url,
destination_dir="/lighttpd-modified-headers/",
)

View file

@ -51,3 +51,23 @@ class OptimizedByDecoratorTestCase(django.test.TestCase):
with_buffering=None,
limit_rate=None,
)
class ModifiedHeadersTestCase(django.test.TestCase):
def test_response(self):
"""'nginx:modified_headers' returns X-Sendfile response."""
setup_file()
url = reverse("nginx:modified_headers")
response = self.client.get(url)
assert_x_accel_redirect(
self,
response,
content_type="text/plain; charset=utf-8",
charset="utf-8",
basename="hello-world.txt",
redirect_url="/nginx-modified-headers/hello-world.txt",
expires=None,
with_buffering=None,
limit_rate=None,
)
self.assertEqual(response["X-Test"], "header")

View file

@ -16,4 +16,9 @@ urlpatterns = [
views.optimized_by_decorator,
name="optimized_by_decorator",
),
path(
"modified_headers/",
views.modified_headers,
name="modified_headers",
),
]

View file

@ -22,3 +22,17 @@ optimized_by_decorator = x_accel_redirect(
source_url=storage.base_url,
destination_url="/nginx-optimized-by-decorator/",
)
def _modified_headers(request):
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
response = view(request)
response["X-Test"] = "header"
return response
modified_headers = x_accel_redirect(
_modified_headers,
source_url=storage.base_url,
destination_url="/nginx-modified-headers/",
)

View file

@ -30,7 +30,7 @@ class DynamicPathDownloadView(PathDownloadView):
def get_path(self):
"""Return path inside fixtures directory."""
# Get path from URL resolvers or as_view kwarg.
relative_path = super(DynamicPathDownloadView, self).get_path()
relative_path = super().get_path()
# Make it absolute.
absolute_path = os.path.join(fixtures_dir, relative_path)
return absolute_path

View file

@ -1,4 +1,5 @@
"""Django settings for django-downloadview demo project."""
import os

View file

@ -44,8 +44,7 @@ class StaticPathTestCase(django.test.TestCase):
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
year = datetime.date.today().year + 4
response = self.client.get(
url,
HTTP_IF_MODIFIED_SINCE=f"Sat, 29 Oct {year} 19:43:31 GMT",
url, headers={"if-modified-since": f"Sat, 29 Oct {year} 19:43:31 GMT"}
)
self.assertTrue(isinstance(response, HttpResponseNotModified))
@ -55,7 +54,7 @@ class StaticPathTestCase(django.test.TestCase):
setup_file("1.txt")
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
response = self.client.get(
url, HTTP_IF_MODIFIED_SINCE="Sat, 29 Oct 1980 19:43:31 GMT"
url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"}
)
assert_download_response(
self,

View file

@ -14,7 +14,7 @@ class DynamicStorageDownloadView(StorageDownloadView):
def get_path(self):
"""Return uppercase path."""
return super(DynamicStorageDownloadView, self).get_path().upper()
return super().get_path().upper()
dynamic_path = DynamicStorageDownloadView.as_view(storage=storage)

View file

@ -1,4 +1,5 @@
"""Test suite for demoproject.download."""
from django.test import TestCase
from django.urls import reverse

View file

@ -12,6 +12,7 @@ middleware here, or combine a Django application with an application of another
framework.
"""
import os
from django.core.wsgi import get_wsgi_application

View file

@ -1,7 +1,8 @@
"""Serve files with Django and reverse proxies."""
from django_downloadview.api import * # NoQA
import pkg_resources
import importlib.metadata
#: Module version, as defined in PEP-0396.
__version__ = pkg_resources.get_distribution(__package__.replace("-", "_")).version
__version__ = importlib.metadata.version(__package__.replace("-", "_"))

View file

@ -5,6 +5,7 @@ See also `documentation of mod_xsendfile for Apache
Apache optimizations </optimizations/apache>`.
"""
# API shortcuts.
from django_downloadview.apache.decorators import x_sendfile # NoQA
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA

View file

@ -1,4 +1,5 @@
"""Decorators to apply Apache X-Sendfile on a specific view."""
from django_downloadview.apache.middlewares import XSendfileMiddleware
from django_downloadview.decorators import DownloadDecorator

View file

@ -19,9 +19,7 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
):
"""Constructor."""
super(XSendfileMiddleware, self).__init__(
get_response, source_dir, source_url, destination_dir
)
super().__init__(get_response, source_dir, source_url, destination_dir)
def process_download_response(self, request, response):
"""Replace DownloadResponse instances by XSendfileResponse ones."""
@ -34,4 +32,5 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
content_type=response["Content-Type"],
basename=response.basename,
attachment=response.attachment,
headers=response.headers,
)

View file

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

View file

@ -1,7 +1,7 @@
from django_downloadview.apache.response import XSendfileResponse
class XSendfileValidator(object):
class XSendfileValidator:
"""Utility class to validate XSendfileResponse instances.
See also :py:func:`assert_x_sendfile` shortcut function.

View file

@ -1,5 +1,6 @@
# flake8: noqa
"""Declaration of API shortcuts."""
from django_downloadview.files import HTTPFile, StorageFile, VirtualFile
from django_downloadview.io import BytesIteratorIO, TextIteratorIO
from django_downloadview.middlewares import (

View file

@ -1,4 +1,5 @@
"""File wrappers for use as exchange data between views and responses."""
from io import BytesIO
from urllib.parse import urlparse
@ -167,7 +168,7 @@ class VirtualFile(File):
File URL.
"""
super(VirtualFile, self).__init__(file, name)
super().__init__(file, name)
self.url = url
if size is not None:
self._size = size
@ -183,7 +184,7 @@ class VirtualFile(File):
return self._size
def _set_size(self, value):
return super(VirtualFile, self)._set_size(value)
return super()._set_size(value)
size = property(_get_size, _set_size)

View file

@ -1,4 +1,5 @@
"""Low-level IO operations, for use with file wrappers."""
import io
from django.utils.encoding import force_bytes, force_str

View file

@ -6,6 +6,7 @@ See also `documentation of X-Sendfile for Lighttpd
</optimizations/lighttpd>`.
"""
# API shortcuts.
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA

View file

@ -1,4 +1,5 @@
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
from django_downloadview.decorators import DownloadDecorator
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware

View file

@ -19,9 +19,7 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
self, get_response=None, source_dir=None, source_url=None, destination_dir=None
):
"""Constructor."""
super(XSendfileMiddleware, self).__init__(
get_response, source_dir, source_url, destination_dir
)
super().__init__(get_response, source_dir, source_url, destination_dir)
def process_download_response(self, request, response):
"""Replace DownloadResponse instances by XSendfileResponse ones."""
@ -34,4 +32,5 @@ class XSendfileMiddleware(ProxiedDownloadMiddleware):
content_type=response["Content-Type"],
basename=response.basename,
attachment=response.attachment,
headers=response.headers,
)

View file

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

View file

@ -4,6 +4,7 @@ Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
responses and may replace them with optimized download responses.
"""
import collections.abc
import copy
import os
@ -36,6 +37,7 @@ class BaseDownloadMiddleware:
Subclasses **must** implement :py:meth:`process_download_response` method.
"""
def __init__(self, get_response):
self.get_response = get_response
@ -75,14 +77,9 @@ class RealDownloadMiddleware(BaseDownloadMiddleware):
whose file attribute have either an URL or a file name.
"""
if super(RealDownloadMiddleware, self).is_download_response(response):
try:
return response.file.url or response.file.name
except AttributeError:
return False
else:
return True
return False
return super().is_download_response(response) and bool(
getattr(response.file, "url", None) or getattr(response.file, "name", None)
)
class DownloadDispatcher:
@ -95,7 +92,7 @@ class DownloadDispatcher:
def auto_configure_middlewares(self):
"""Populate :attr:`middlewares` from
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
for (key, import_string, kwargs) in getattr(
for key, import_string, kwargs in getattr(
settings, "DOWNLOADVIEW_MIDDLEWARES", []
):
factory = import_member(import_string)
@ -104,7 +101,7 @@ class DownloadDispatcher:
def dispatch(self, request, response):
"""Dispatches job to children middlewares."""
for (key, middleware) in self.middlewares:
for key, middleware in self.middlewares:
response = middleware.process_response(request, response)
return response
@ -113,7 +110,7 @@ class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
"Download middleware that dispatches job to several middleware instances."
def __init__(self, get_response, middlewares=AUTO_CONFIGURE):
super(DownloadDispatcherMiddleware, self).__init__(get_response)
super().__init__(get_response)
self.dispatcher = DownloadDispatcher(middlewares)
def process_download_response(self, request, response):
@ -130,7 +127,7 @@ class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
backend_options=AUTO_CONFIGURE,
):
"""Constructor."""
super(SmartDownloadMiddleware, self).__init__(get_response, middlewares=[])
super().__init__(get_response, middlewares=[])
#: Callable (typically a class) to instantiate backend (typically a
#: :class:`DownloadMiddleware` subclass).
self.backend_factory = backend_factory
@ -148,7 +145,7 @@ class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
except AttributeError:
raise ImproperlyConfigured(
"SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_BACKEND"
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_BACKEND"
)
def auto_configure_backend_options(self):
@ -158,7 +155,7 @@ class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES)
except AttributeError:
raise ImproperlyConfigured(
"SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_RULES"
"SmartDownloadMiddleware requires settings.DOWNLOADVIEW_RULES"
)
for key, options in enumerate(options_list):
args = []
@ -187,7 +184,7 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware):
self, get_response, source_dir=None, source_url=None, destination_url=None
):
"""Constructor."""
super(ProxiedDownloadMiddleware, self).__init__(get_response)
super().__init__(get_response)
self.source_dir = source_dir
self.source_url = source_url

View file

@ -5,6 +5,7 @@ See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
</optimizations/nginx>`.
"""
# API shortcuts.
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA

View file

@ -1,4 +1,5 @@
"""Decorators to apply Nginx X-Accel on a specific view."""
from django_downloadview.decorators import DownloadDecorator
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware

View file

@ -58,9 +58,7 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
else:
source_dir = source_dir
super(XAccelRedirectMiddleware, self).__init__(
get_response, source_dir, source_url, destination_url
)
super().__init__(get_response, source_dir, source_url, destination_url)
self.expires = expires
self.with_buffering = with_buffering
@ -87,6 +85,7 @@ class XAccelRedirectMiddleware(ProxiedDownloadMiddleware):
with_buffering=self.with_buffering,
limit_rate=self.limit_rate,
attachment=response.attachment,
headers=response.headers,
)
@ -132,7 +131,7 @@ class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware):
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is "
"required by %s middleware" % self.__class__.__name__
)
super(SingleXAccelRedirectMiddleware, self).__init__(
super().__init__(
get_response=get_response,
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,

View file

@ -1,4 +1,5 @@
"""Nginx's specific responses."""
from datetime import timedelta
from django.utils.timezone import now
@ -19,9 +20,13 @@ class XAccelRedirectResponse(ProxiedDownloadResponse):
with_buffering=None,
limit_rate=None,
attachment=True,
headers=None,
):
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
super(XAccelRedirectResponse, self).__init__(content_type=content_type)
# content-type must be porvided only as keyword argument to response
if headers and content_type:
headers.pop("Content-Type", None)
super().__init__(content_type=content_type, headers=headers)
if attachment:
self.basename = basename or url_basename(redirect_url, content_type)
self["Content-Disposition"] = content_disposition(self.basename)

View file

@ -7,6 +7,7 @@
for details.
"""
import warnings
from django.conf import settings
@ -17,14 +18,7 @@ from django.core.exceptions import ImproperlyConfigured
deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware"
def get_middlewares():
try:
return settings.MIDDLEWARE
except AttributeError:
return settings.MIDDLEWARE_CLASSES
if deprecated_middleware in get_middlewares():
if deprecated_middleware in settings.MIDDLEWARE:
raise ImproperlyConfigured(
"{deprecated_middleware} middleware has been renamed as of "
"django-downloadview version 1.3. You may use "

View file

@ -1,7 +1,7 @@
from django_downloadview.nginx.response import XAccelRedirectResponse
class XAccelRedirectValidator(object):
class XAccelRedirectValidator:
"""Utility class to validate XAccelRedirectResponse instances.
See also :py:func:`assert_x_accel_redirect` shortcut function.

View file

@ -1,4 +1,5 @@
""":py:class:`django.http.HttpResponse` subclasses."""
import mimetypes
import os
import re
@ -72,7 +73,13 @@ def content_disposition(filename):
"""
if not filename:
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)
if ascii_filename == utf8_filename: # ASCII only.
return f'attachment; filename="{ascii_filename}"'
@ -138,7 +145,7 @@ class DownloadResponse(StreamingHttpResponse):
#: A :doc:`file wrapper instance </files>`, such as
#: :class:`~django.core.files.base.File`.
self.file = file_instance
super(DownloadResponse, self).__init__(
super().__init__(
streaming_content=self.file, status=status, content_type=content_type
)
@ -195,16 +202,6 @@ class DownloadResponse(StreamingHttpResponse):
self._default_headers = headers
return self._default_headers
def items(self):
"""Return iterable of (header, value).
This method is called by http handlers just before WSGI's
start_response() is called... but it is not called by
django.test.ClientHandler! :'(
"""
return super(DownloadResponse, self).items()
def get_basename(self):
"""Return basename."""
if self.basename:

View file

@ -1,4 +1,5 @@
"""Port of django-sendfile in django-downloadview."""
from django_downloadview.views.path import PathDownloadView

View file

@ -10,7 +10,7 @@ class SignedURLMixin:
"""
def url(self, name):
path = super(SignedURLMixin, self).url(name)
path = super().url(name)
signer = TimestampSigner()
signature = signer.sign(path)
return "{}?X-Signature={}".format(path, signature)

View file

@ -71,13 +71,13 @@ class temporary_media_root(override_settings):
settings.MEDIA_ROOT."""
tmp_dir = tempfile.mkdtemp()
self.options["MEDIA_ROOT"] = tmp_dir
super(temporary_media_root, self).enable()
super().enable()
def disable(self):
"""Remove directory settings.MEDIA_ROOT then restore original
setting."""
shutil.rmtree(settings.MEDIA_ROOT)
super(temporary_media_root, self).disable()
super().disable()
class DownloadResponseValidator(object):

View file

@ -1,4 +1,5 @@
"""Utility functions that may be implemented in external packages."""
import re
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)

View file

@ -1,4 +1,5 @@
"""Views to stream files."""
# API shortcuts.
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
from django_downloadview.views.http import HTTPDownloadView # NoQA

View file

@ -1,5 +1,6 @@
"""Base material for download views: :class:`DownloadMixin` and
:class:`BaseDownloadView`"""
import calendar
from django.http import Http404, HttpResponseNotModified
@ -102,9 +103,9 @@ class DownloadMixin(object):
Else, fallbacks to default implementation, which uses
:py:func:`django.views.static.was_modified_since`.
Django's ``was_modified_since`` function needs a datetime and a size.
It is passed ``modified_time`` and ``size`` attributes from file
wrapper. If file wrapper does not support these attributes
Django's ``was_modified_since`` function needs a datetime.
It is passed the ``modified_time`` attribute from file
wrapper. If file wrapper does not support this attribute
(``AttributeError`` or ``NotImplementedError`` is raised), then
the file is considered as modified and ``True`` is returned.
@ -116,12 +117,11 @@ class DownloadMixin(object):
modification_time = calendar.timegm(
file_instance.modified_time.utctimetuple()
)
size = file_instance.size
except (AttributeError, NotImplementedError) as e:
print("!=======!", e)
return True
else:
return was_modified_since(since, modification_time, size)
return was_modified_since(since, modification_time)
def not_modified_response(self, *response_args, **response_kwargs):
"""Return :class:`django.http.HttpResponseNotModified` instance."""
@ -157,7 +157,7 @@ class DownloadMixin(object):
except exceptions.FileNotFound:
return self.file_not_found_response()
# Respect the If-Modified-Since header.
since = self.request.META.get("HTTP_IF_MODIFIED_SINCE", None)
since = self.request.headers.get("if-modified-since", None)
if since is not None:
if not self.was_modified_since(self.file_instance, since):
return self.not_modified_response(**response_kwargs)

View file

@ -1,4 +1,5 @@
"""Stream files given an URL, i.e. files you want to proxy."""
from django_downloadview.files import HTTPFile
from django_downloadview.views.base import BaseDownloadView
@ -44,5 +45,5 @@ class HTTPDownloadView(BaseDownloadView):
request_factory=self.get_request_factory(),
name=self.get_basename(),
url=self.get_url(),
**self.get_request_kwargs()
**self.get_request_kwargs(),
)

View file

@ -1,4 +1,5 @@
"""Stream files that live in models."""
from django.views.generic.detail import SingleObjectMixin
from django_downloadview.exceptions import FileNotFound
@ -83,7 +84,7 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
def get_basename(self):
"""Return client-side filename."""
basename = super(ObjectDownloadView, self).get_basename()
basename = super().get_basename()
if basename is None:
field = "basename"
model_field = getattr(self, "%s_field" % field, False)
@ -93,4 +94,4 @@ class ObjectDownloadView(SingleObjectMixin, BaseDownloadView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super(ObjectDownloadView, self).get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)

View file

@ -1,4 +1,5 @@
""":class:`PathDownloadView`."""
import os
from django.core.files import File

View file

@ -1,4 +1,5 @@
"""Stream files from storage."""
from django.core.files.storage import DefaultStorage
from django_downloadview.files import StorageFile
@ -14,16 +15,6 @@ class StorageDownloadView(PathDownloadView):
#: Path to the file to serve relative to storage.
path = None # Override docstring.
def get_path(self):
"""Return path of the file to serve, relative to storage.
Default implementation simply returns view's :py:attr:`path` attribute.
Override this method if you want custom implementation.
"""
return super(StorageDownloadView, self).get_path()
def get_file(self):
"""Return :class:`~django_downloadview.files.StorageFile` instance."""
return StorageFile(self.storage, self.get_path())

View file

@ -1,4 +1,5 @@
"""Stream files that you generate or that live in memory."""
from django_downloadview.views.base import BaseDownloadView

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""django-downloadview documentation build configuration file."""
import os
import re
from pkg_resources import get_distribution
import importlib.metadata
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
# django-downloadview depends on Django...
@ -48,9 +49,9 @@ author_slug = re.sub(r"([\w_.-]+)", "-", author)
# built documents.
# The full version, including alpha/beta/rc tags.
release = get_distribution("django-downloadview").version
release = importlib.metadata.version("django-downloadview")
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
version = ".".join(release.split(".")[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -31,7 +31,7 @@ Here are tips to migrate from `django-sendfile` to `django-downloadview`...
* setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do
more.
* register ``django_downloadview.SmartDownloadMiddleware`` in
``MIDDLEWARE_CLASSES``.
``MIDDLEWARE``.
4. Change your tests if any. You can no longer use `django-senfile`'s
``development`` backend. See :doc:`/testing` for `django-downloadview`'s

View file

@ -21,7 +21,7 @@ Django's builtins
wrapper in :doc:`/views/path`.
* :class:`django.db.models.fields.files.FieldFile` wraps a file that is
managed in a model. ``django-downloadview`` uses this wrapper in
managed in a model. ``django-downloadview`` uses this wrapper in
:doc:`/views/object`.
* :class:`django.core.files.base.ContentFile` wraps a bytes, string or

View file

@ -45,7 +45,7 @@ Setup XSendfile middlewares
***************************
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
``MIDDLEWARE_CLASSES`` of your `Django` settings.
``MIDDLEWARE`` of your `Django` settings.
Example:
@ -128,4 +128,4 @@ setup.
.. target-notes::
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/
.. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/

View file

@ -51,7 +51,7 @@ Setup XSendfile middlewares
***************************
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
``MIDDLEWARE_CLASSES`` of your `Django` settings.
``MIDDLEWARE`` of your `Django` settings.
Example:

View file

@ -142,7 +142,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
internal;
# Location to files on disk.
alias /var/www/files/;
}
}
# Proxy to Django-powered frontend.
location / {
@ -154,7 +154,7 @@ Here is what you could have in :file:`/etc/nginx/sites-available/default`:
}
... where specific configuration is the ``location /optimized-download``
section.
section.
.. note::
@ -192,4 +192,4 @@ they should be equal.
.. target-notes::
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel

View file

@ -61,7 +61,7 @@ possible, file wrappers do not embed file data, in order to save memory.
Learn more about available file wrappers in :doc:`files`.
*****************************************************************
Middlewares convert DownloadResponse into ProxiedDownloadResponse
*****************************************************************

View file

@ -13,11 +13,11 @@ There is no need to register this application in ``INSTALLED_APPS``.
******************
MIDDLEWARE_CLASSES
MIDDLEWARE
******************
If you plan to setup :doc:`reverse-proxy optimizations </optimizations/index>`,
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE``.
It is a response middleware. Move it after middlewares that compute the
response content such as gzip middleware.
@ -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')
urlpatterns = [
path('download/<str:slug>/', signature_required(download),
path('download/<str:slug>/', signature_required(download)),
]
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).
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.
@ -119,5 +119,5 @@ See :doc:`/optimizations/index` for details about builtin backends
(middlewares) and their options.
When ``django_downloadview.SmartDownloadMiddleware`` is in your
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.
``MIDDLEWARE``, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.

View file

@ -41,4 +41,3 @@ Example, related to :doc:`StorageDownloadView demo </views/storage>`:
.. literalinclude:: /../demo/demoproject/storage/tests.py
:language: python
:lines: 1-2, 8-12, 59-

View file

@ -10,21 +10,21 @@ setup(
setup_requires=["setuptools_scm"],
description="Serve files with Django and reverse-proxies.",
long_description=open(os.path.join(here, "README.rst")).read(),
long_description_content_type='text/x-rst',
long_description_content_type="text/x-rst",
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
],
keywords=" ".join(
[
@ -54,9 +54,10 @@ setup(
],
include_package_data=True,
zip_safe=False,
python_requires=">=3.8",
install_requires=[
# BEGIN requirements
"Django>=2.2",
"Django>=4.2",
"requests",
# END requirements
],

View file

@ -1,4 +1,5 @@
"""Test suite around :mod:`django_downloadview.api` and deprecation plan."""
from importlib import import_module, reload
import unittest
import warnings
@ -100,7 +101,6 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
def test_nginx_x_accel_redirect_middleware(self):
"XAccelRedirectMiddleware in settings triggers ImproperlyConfigured."
with override_settings(
MIDDLEWARE_CLASSES=["django_downloadview.nginx.XAccelRedirectMiddleware"],
MIDDLEWARE=["django_downloadview.nginx.XAccelRedirectMiddleware"],
):
with self.assertRaises(ImproperlyConfigured):
@ -131,7 +131,7 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
reload(django_downloadview.nginx.settings)
caught = False
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 setting_name in str(warning_item.message):
caught = True
@ -141,5 +141,5 @@ class DeprecatedAPITestCase(django.test.SimpleTestCase):
if missed_warnings:
self.fail(
f"No DeprecationWarning raised about following settings: "
f'{", ".join(missed_warnings)}.'
f"{', '.join(missed_warnings)}."
)

View file

@ -1,4 +1,5 @@
"""Tests around :mod:`django_downloadview.io`."""
import unittest
from django_downloadview import BytesIteratorIO, TextIteratorIO

View file

@ -1,4 +1,6 @@
"""Tests around project's distribution and packaging."""
import importlib.metadata
import os
import unittest
@ -24,22 +26,14 @@ class VersionTestCase(unittest.TestCase):
self.fail("django_downloadview package has no __version__.")
def test_version_match(self):
"""django_downloadview.__version__ matches pkg_resources info."""
try:
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")
"""django_downloadview.__version__ matches importlib metadata."""
distribution = importlib.metadata.distribution("django-downloadview")
installed_version = distribution.version
self.assertEqual(
installed_version,
self.get_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 "
"installed version in development environment."
% (self.get_version(), installed_version),

View file

@ -1,4 +1,5 @@
"""Unit tests around responses."""
import unittest
from django_downloadview.response import DownloadResponse
@ -19,3 +20,13 @@ class DownloadResponseTestCase(unittest.TestCase):
self.assertIn(
"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"]
)

View file

@ -1,4 +1,5 @@
"""Tests around :py:mod:`django_downloadview.sendfile`."""
from django.http import Http404
import django.test

View file

@ -1,4 +1,5 @@
"""Tests around :mod:`django_downloadview.views`."""
import calendar
from datetime import datetime
import os
@ -85,13 +86,12 @@ class DownloadMixinTestCase(unittest.TestCase):
When calling file wrapper's ``was_modified_since()`` raises
``NotImplementedError`` or ``AttributeError``,
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
tries to pass file wrapper's ``size`` and ``modified_time`` to
tries to pass file wrapper's ``modified_time`` to
:func:`django.views.static import was_modified_since`.
"""
file_wrapper = mock.Mock()
file_wrapper.was_modified_since = mock.Mock(side_effect=AttributeError)
file_wrapper.size = mock.sentinel.size
file_wrapper.modified_time = datetime.now()
was_modified_since_mock = mock.Mock(return_value=mock.sentinel.was_modified)
mixin = views.DownloadMixin()
@ -106,7 +106,6 @@ class DownloadMixinTestCase(unittest.TestCase):
was_modified_since_mock.assert_called_once_with(
mock.sentinel.since,
calendar.timegm(file_wrapper.modified_time.utctimetuple()),
mock.sentinel.size,
)
def test_was_modified_since_fallback(self):
@ -117,7 +116,7 @@ class DownloadMixinTestCase(unittest.TestCase):
* calling file wrapper's ``was_modified_since()`` raises
``NotImplementedError`` or ``AttributeError``;
* and accessing ``size`` and ``modified_time`` from file wrapper raises
* and accessing ``modified_time`` from file wrapper raises
``NotImplementedError`` or ``AttributeError``...
... then

36
tox.ini
View file

@ -1,44 +1,49 @@
[tox]
envlist =
py{36,37,38,39,310}-dj{22,31,32}
py{38,39,310}-dj{40,main}
py{38,39,310,311,312}-dj{42}-{unittest,pytest,checkmigrations}
py{310,311,312,313}-dj{50}-{unittest,pytest,checkmigrations}
py{310,311,312,313}-dj{51}-{unittest,pytest,checkmigrations}
py{310,311,312,313,314}-dj{52}-{unittest,pytest,checkmigrations}
py{312,313,314}-dj{main}-{unittest,pytest,checkmigrations}
lint
sphinx
readme
[gh-actions]
python =
3.6: py36
3.7: py37
3.8: py38, lint, sphinx, readme
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
3.14: py314
[gh-actions:env]
DJANGO =
2.2: dj22
3.1: dj31
3.2: dj32
4.0: dj40
4.2: dj42
5.0: dj50
5.1: dj51
5.2: dj52
main: djmain
[testenv]
deps =
coverage
dj22: Django>=2.2,<3.0
dj31: Django>=3.1,<3.2
dj32: Django>=3.2,<3.3
dj40: Django>=4.0,<4.1
dj42: Django>=4.2,<5.0
dj50: Django>=5.0,<5.1
dj51: django>=5.1,<5.2
dj52: django>=5.2,<5.3
djmain: https://github.com/django/django/archive/main.tar.gz
pytest
pytest-cov
commands =
pip install -e .
pip install -e demo
# doctests
# doctests and unit tests
pytest --cov=django_downloadview --cov=demoproject {posargs}
# all other test cases
coverage run --append {envbindir}/demo test {posargs: tests demoproject}
# demo project integration tests
coverage run --append {envbindir}/demo test {posargs: demoproject}
coverage xml
pip freeze
ignore_outcome =
@ -80,3 +85,4 @@ source = django_downloadview,demo
[pytest]
DJANGO_SETTINGS_MODULE = demoproject.settings
addopts = --doctest-modules --ignore=docs/
python_files = tests/*.py