Compare commits

...

134 commits
2.1 ... master

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
Rémy HUBSCHER
3c2951ceac
Merge pull request #192 from jazzband/prepare-2.3-release
Prepare release of django-downloadview 2.3
2022-01-11 10:41:15 +01:00
Rémy HUBSCHER
aa003ed6bf
Prepare 2.3 release. 2022-01-11 10:37:35 +01:00
Rémy HUBSCHER
6dbf06c4ea
Merge pull request #190 from johnthagen/patch-2
Document Python 3.10 support
2022-01-05 14:11:53 +01:00
johnthagen
7c2af759c8
Document Python 3.10 support 2022-01-05 07:45:22 -05:00
Rémy HUBSCHER
7e9f81d758
Merge pull request #188 from tari/django-4.0-compat
Update compatibility for Django 4.0
2022-01-05 13:38:34 +01:00
Peter Marheine
e9fbb74b2c Uncap Github Actions parallelism
There seems to be no reason to limit tests to 5 jobs at a time; removing
the limit should allow tests to finish faster.
2021-12-23 20:27:29 +11:00
Peter Marheine
6381dc94f1 Include Django 4.0 in the test matrix 2021-12-23 13:05:46 +11:00
Peter Marheine
198f6a3295 Update compatibility for Django 4.0
The only meaningful change is removing use of `force_text` (which was
deprecated in Django 3.0) in favor of `force_str` (which has existed
since before Django 1.11). On Python 3 there is no functional difference
between the two.
2021-12-23 13:05:46 +11:00
Peter Marheine
a64a0e8c33 Split DownloadDispatcherMiddleware into two classes
Instantiating a middleware but not using it as a middleware was a
strange behavior, so this change splits the dispatching out to another
class with a more specialized API and uses that middleware.
2021-12-23 13:05:45 +11:00
Peter Marheine
0ab8aa3e8f Stop using django.util.deprecation.MiddlewareMixin
That class is intended primarily for compatibility with Pre-1.10
middleware, and recently gained a check that get_response is not None.
This package ensures an unexpecified `get_response` function is never
called on its own, so it's simplest to manually implement the middleware
API.
2021-12-23 13:05:45 +11:00
Peter Marheine
2524668e86 Test on Python 3.10 2021-12-23 11:53:26 +11:00
Peter Marheine
cb3ec3a091 Stop using nosetests
Nose is no longer maintained and is incompatible with Python 3.10, so
can no longer be used. This change runs `coverage` manually to collect
coverage and uses `pytest` to run doctests, collectively covering what
was tested using django_nose.
2021-12-23 11:51:53 +11:00
Peter Marheine
95b36fc843 Import ABCs from collections.abc, not collections
The types in collections.abc were moved from just collections in Python
3.3, and Python 3.10 removed the old aliases. We no longer support
Python versions earlier than 3.3 and need to support 3.10, so update the
import.
2021-12-23 10:27:14 +11:00
Rémy HUBSCHER
0578729a66
Merge pull request #183 from johnthagen/patch-1
Remove duplicate trove classifier
2021-04-30 08:09:51 +02:00
johnthagen
051f6116f8
Remove duplicate trove classifier 2021-04-27 19:09:12 -04:00
Hasan Ramezani
ca8331e2de Add Django supported version badge to README.rst. 2021-04-16 14:30:02 +02:00
Hasan Ramezani
a8d231cbed Add Django 3.2 support. 2021-04-16 14:30:02 +02:00
Hasan Ramezani
006d2a288d Drop Django 3.0 support. 2021-04-16 14:30:02 +02:00
Rémy HUBSCHER
cbd53d813e
Merge pull request #179 from johnthagen/storage-download-docs
Fix StorageDownloadView code snippets
2021-04-10 18:53:18 +02:00
Rémy HUBSCHER
3543a3b8fc
Merge pull request #180 from johnthagen/path-doc-fixes
Fix code snippet in PathDownloadView docs
2021-04-10 18:53:04 +02:00
Rémy HUBSCHER
465123b6e5
Merge pull request #178 from johnthagen/nginx-docs
Replace mention of deprecated NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR
2021-04-10 18:52:44 +02:00
Jannis Leidel
45e1219275
Rename Django's dev branch to main. (#181)
* Rename Django's dev branch to main.

More information: https://groups.google.com/g/django-developers/c/tctDuKUGosc/
Refs: https://github.com/django/django/pull/14048

* Don't run Django main test on Python < 3.8.
2021-03-09 13:49:08 +01:00
johnthagen
a06c511d8f Fix code snippet in PathDownloadView docs 2021-02-02 10:30:34 -05:00
johnthagen
a67f3dc41e Fix StorageDownloadView code snippets 2021-02-02 10:27:38 -05:00
johnthagen
9c01be6cc8 Replace mention of deprecated NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR 2021-02-02 10:10:59 -05:00
Rémy HUBSCHER
563b2a4f7b
Merge pull request #176 from johnthagen/fix-nginx-code-snippets
Fix the code snippets in the NGINX documentation page
2021-01-27 10:06:57 +01:00
johnthagen
707c392f6e Fix the code snippets in the NGINX documentation page 2021-01-26 16:00:11 -05:00
Rémy HUBSCHER
9b5fee2687
Merge pull request #174 from johnthagen/doc-code-fix
Fix doc code include in ObjectDownloadView
2021-01-21 19:12:34 +01:00
johnthagen
ed3d470908 Fix url example code 2021-01-21 13:07:36 -05:00
johnthagen
eb223f169b Fix "Mapping file attributes to model's" example code 2021-01-21 12:59:36 -05:00
johnthagen
700b1246df Fix "serving a specific file field" example code 2021-01-21 12:56:36 -05:00
johnthagen
2586cc5a97 Fix doc code include in ObjectDownloadView 2021-01-21 12:50:16 -05:00
Rémy HUBSCHER
c602c32d69
Merge pull request #173 from johnthagen/remove-setuptool-dep
Remove setuptools dependency
2021-01-21 18:03:34 +01:00
johnthagen
8fd75de224 Remove setuptools dependency 2021-01-21 12:01:38 -05:00
Rémy HUBSCHER
87daf8e92e
Merge pull request #172 from johnthagen/require-django-2.2
Require supported Django version 2.2
2021-01-21 18:01:34 +01:00
johnthagen
4dae54179b Require supported Django version 2.2 2021-01-21 11:59:37 -05:00
Rémy HUBSCHER
d1972d6460
Merge pull request #170 from johnthagen/update-doc-links
Update external documentation links to point to latest Django and Python releases
2021-01-21 17:46:06 +01:00
Rémy HUBSCHER
cee7810cfd
Fix links. 2021-01-21 17:45:42 +01:00
Rémy HUBSCHER
8df2615a29
Merge pull request #171 from johnthagen/demo-python-version
Update demo README to reference Python 3
2021-01-21 17:39:06 +01:00
Rémy HUBSCHER
252e6f127c
Merge pull request #169 from johnthagen/demo-settings-drop-old-django
Drop support for end of life Django settings in demo project
2021-01-21 17:38:17 +01:00
johnthagen
cc4636098b
Remove extra newline
Co-authored-by: Rémy HUBSCHER <hubscher.remy@gmail.com>
2021-01-21 11:37:57 -05:00
johnthagen
f225d87b85 Update demo README to reference Python 3 2021-01-21 11:37:12 -05:00
johnthagen
deddd2fd2d Update external documentation links to point to latest Django and Python releases 2021-01-21 11:35:00 -05:00
johnthagen
6338f61767 Drop support for end of life Django settings in demo project 2021-01-21 11:28:40 -05:00
Jannis Leidel
9f42e65986
Migrate to GitHub Actions. (#165)
* Add GitHub Actions test workflow.

* Add django version env var handling to tox config.

* Update badges.

* Add release workflow.

* Remove Travis.

* Fix typo.

* Fix more typos.

* Write coverage.xml.

* Remove the need for the VERSION file.

* Simplify demo setup.py.

* Remove VERSION file.

* Update demo/setup.py

* Update setup.py
2020-12-23 11:12:35 +01:00
Rémy HUBSCHER
384e7c5b13
Back to development: 2.3 2020-10-08 19:01:09 +02:00
Rémy HUBSCHER
0c59d6d261
Preparing release 2.2 2020-10-05 11:19:05 +02:00
Jannis Leidel
13d2b3ae58
Add release config.
Fix https://github.com/jazzband-roadies/help/issues/201.
2020-10-02 10:45:07 +02:00
Rémy HUBSCHER
f663fa4b03
Merge pull request #158 from Natim/jazzband
Be ready for Jazzband move.
2020-09-18 17:49:34 +02:00
Rémy HUBSCHER
7ec465d74d
Be ready for Jazzband move. 2020-09-18 16:50:33 +02:00
Rémy HUBSCHER
3bb1a73e6b
Merge pull request #160 from jazzband/add-python3.9-support
Add Python 3.9 support.
2020-09-18 16:50:11 +02:00
Rémy HUBSCHER
c0acf77f73
Merge branch 'master' into add-python3.9-support 2020-09-18 16:21:08 +02:00
Rémy HUBSCHER
d74e094fe7
Merge pull request #159 from Natim/new-url-syntax
Remove obsolete URL syntax.
2020-09-18 16:20:32 +02:00
Rémy HUBSCHER
3da22537df
Fix doc. 2020-09-18 11:27:42 +02:00
Rémy HUBSCHER
91be5c38a8
Add Python 3.9 support. 2020-09-18 11:26:25 +02:00
Rémy HUBSCHER
8e439d3485
Remove obsolete URL syntax. 2020-09-18 11:10:08 +02:00
Rémy HUBSCHER
6cd9ec3dd0
Fix test. 2020-09-18 10:50:26 +02:00
Rémy HUBSCHER
bd41e6f7bf
Merge pull request #156 from timgates42/bugfix_typo_instantiate
docs: Fix simple typo, instanciate -> instantiate
2020-03-08 20:12:05 +01:00
Tim Gates
e2da75ec9d
docs: Fix simple typo, instanciate -> instantiate
There is a small typo in django_downloadview/middlewares.py.

Should read `instantiate` rather than `instanciate`.
2020-03-08 18:03:42 +11:00
Rémy HUBSCHER
cd37fd5084
Back to development: 2.2 2020-01-14 10:26:51 +01:00
Rémy HUBSCHER
bc145254ea
Preparing release 2.1.1 2020-01-14 10:24:28 +01:00
Rémy HUBSCHER
79da070b14
Merge pull request #152 from aleksihakli/patch-1
Fix missing function parameter
2020-01-14 10:23:20 +01:00
Aleksi Häkli
aacb5c7a16
Fix missing function parameter 2020-01-13 17:23:54 +02:00
Rémy HUBSCHER
965218aafb
Back to development: 2.2 2020-01-13 10:51:20 +01:00
103 changed files with 971 additions and 648 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"

40
.github/workflows/release.yml vendored Normal file
View 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
View 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
View file

@ -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

View file

@ -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
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: .

View file

@ -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
View file

@ -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>

View file

@ -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
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

@ -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/

View file

@ -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:

View file

@ -6,4 +6,3 @@ include CONTRIBUTING.rst
include INSTALL include INSTALL
include LICENSE include LICENSE
include README.rst include README.rst
include VERSION

View file

@ -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

View file

@ -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

View file

@ -1 +0,0 @@
2.1

View file

@ -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

View file

@ -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")

View file

@ -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",
),
]

View file

@ -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/",
)

View file

@ -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

View file

@ -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"
} }
} }

View 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 SimpleURLTestCase(django.test.TestCase): class SimpleURLTestCase(django.test.TestCase):
def test_download_response(self): def test_download_response(self):

View file

@ -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"),
) ]

View file

@ -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"
) )

View file

@ -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")

View file

@ -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",
),
]

View file

@ -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/",
)

View file

@ -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")

View file

@ -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",
),
]

View file

@ -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/",
)

View file

@ -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.

View file

@ -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",
), ),
) ]

View 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):

View file

@ -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",
), ),
) ]

View file

@ -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
View 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"),)

View file

@ -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,

View file

@ -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",
), ),
) ]

View file

@ -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)

View file

@ -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
View 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"),
) ]

View 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 TextTestCase(django.test.TestCase): class TextTestCase(django.test.TestCase):
def test_download_response(self): def test_download_response(self):

View file

@ -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"),
) ]

View file

@ -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

View file

@ -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,
)

View file

@ -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("-", "_"))

View file

@ -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

View file

@ -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

View file

@ -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,
) )

View file

@ -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)

View file

@ -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.

View file

@ -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 (

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
) )

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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 "

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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(),
) )

View file

@ -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)

View file

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

View 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())

View file

@ -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

View file

@ -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

View file

@ -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",
),
] ]

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 * 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

View file

@ -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

View file

@ -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/

View file

@ -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

View file

@ -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

View file

@ -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

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`. Learn more about available file wrappers in :doc:`files`.
***************************************************************** *****************************************************************
Middlewares convert DownloadResponse into ProxiedDownloadResponse 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>`, 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.

View file

@ -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-

View file

@ -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!".

View file

@ -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::

View file

@ -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
************* *************

View file

@ -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
View file

@ -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,
)

View file

@ -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)}."
) )

View file

@ -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

View file

@ -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),
)

View file

@ -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