mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
commit
9f42cde8cb
57 changed files with 181 additions and 67 deletions
59
.pre-commit-config.yaml
Normal file
59
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.6.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: v1.1.1
|
||||||
|
hooks:
|
||||||
|
- id: doc8
|
||||||
|
- repo: https://github.com/adamchainz/django-upgrade
|
||||||
|
rev: 1.20.0
|
||||||
|
hooks:
|
||||||
|
- id: django-upgrade
|
||||||
|
args: [--target-version, "4.2"]
|
||||||
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
|
rev: v1.10.0
|
||||||
|
hooks:
|
||||||
|
- id: rst-backticks
|
||||||
|
- id: rst-directive-colons
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
entry: env PRETTIER_LEGACY_CLI=1 prettier
|
||||||
|
types_or: [javascript, css]
|
||||||
|
args:
|
||||||
|
- --trailing-comma=es5
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v9.8.0
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
additional_dependencies:
|
||||||
|
- "eslint@v9.0.0-beta.1"
|
||||||
|
- "@eslint/js@v9.0.0-beta.1"
|
||||||
|
- "globals"
|
||||||
|
files: \.js?$
|
||||||
|
types: [file]
|
||||||
|
args:
|
||||||
|
- --fix
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: 'v0.5.5'
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
|
rev: 2.1.4
|
||||||
|
hooks:
|
||||||
|
- id: pyproject-fmt
|
||||||
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
|
rev: v0.18
|
||||||
|
hooks:
|
||||||
|
- id: validate-pyproject
|
||||||
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.10"
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
21
AUTHORS
21
AUTHORS
|
|
@ -6,22 +6,23 @@ Maintainer: Benoît Bryon <benoit@marmelune.net>
|
||||||
|
|
||||||
Original code by `PeopleDoc <https://www.people-doc.com/>`_ 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/gregtap>
|
|
||||||
* Rémy Hubscher <https://github.com/natim>
|
|
||||||
* 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>
|
||||||
* Johnt Hagen <johnthagen@gmail.com>
|
* David Wolf <68775926+devidw@users.noreply.github.com>
|
||||||
|
* Davide Setti <setti.davide89@gmail.com>
|
||||||
|
* Erik Dykema <dykema@gmail.com>
|
||||||
* Fabre Florian <ffabre@hybird.org>
|
* Fabre Florian <ffabre@hybird.org>
|
||||||
* Peter Marheine <peter@taricorp.net>
|
|
||||||
* Hasan Ramezani <hasan.r67@gmail.com>
|
* Hasan Ramezani <hasan.r67@gmail.com>
|
||||||
* Jannis Leidel <jannis@leidel.info>
|
* Jannis Leidel <jannis@leidel.info>
|
||||||
* Erik Dykema <dykema@gmail.com>
|
* John Hagen <johnthagen@gmail.com>
|
||||||
|
* Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||||
|
* Martin Bächtold <martin@baechtold.me>
|
||||||
* Nikhil Benesch <nikhil.benesch@gmail.com>
|
* Nikhil Benesch <nikhil.benesch@gmail.com>
|
||||||
* Omer Katz <omer.drow@gmail.com>
|
* Omer Katz <omer.drow@gmail.com>
|
||||||
|
* Peter Marheine <peter@taricorp.net>
|
||||||
* René Leonhardt <rene.leonhardt@gmail.com>
|
* René Leonhardt <rene.leonhardt@gmail.com>
|
||||||
* Adam Chainz <adam@adamj.eu>
|
* Rémy HUBSCHER <hubscher.remy@gmail.com>
|
||||||
* Martin Bächtold <martin@baechtold.me>
|
|
||||||
* Tim Gates <tim.gates@iress.com>
|
* Tim Gates <tim.gates@iress.com>
|
||||||
* zero13cool <zero13cool@yandex.ru>
|
* zero13cool <zero13cool@yandex.ru>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ 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.4 (Unreleased)
|
2.4 (2024-08-05)
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
- Drop support for Python 3.6
|
- Drop support for Python 3.6
|
||||||
|
- Escape malicious filenames
|
||||||
|
- Handle headers in XAccel responses
|
||||||
|
|
||||||
|
|
||||||
2.3 (2022-01-11)
|
2.3 (2022-01-11)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree t
|
||||||
|
|
||||||
|
|
||||||
This document provides guidelines for people who want to contribute to
|
This document provides guidelines for people who want to contribute to
|
||||||
`django-downloadview`.
|
``django-downloadview``.
|
||||||
|
|
||||||
|
|
||||||
**************
|
**************
|
||||||
|
|
@ -50,7 +50,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -62,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``.
|
||||||
|
|
@ -70,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``.
|
||||||
|
|
||||||
|
|
@ -84,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.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ documentation. Maintain it along with code and documentation.
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`bugtracker`:
|
.. _`bugtracker`:
|
||||||
https://github.com/jazzband/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
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,16 @@ django-downloadview
|
||||||
:target: https://codecov.io/gh/jazzband/django-downloadview
|
:target: https://codecov.io/gh/jazzband/django-downloadview
|
||||||
:alt: Coverage
|
:alt: Coverage
|
||||||
|
|
||||||
`django-downloadview` makes it easy to serve files with `Django`_:
|
``django-downloadview`` makes it easy to serve files with `Django`_:
|
||||||
|
|
||||||
* you manage files with Django (permissions, filters, generation, ...);
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
*****************************************
|
*****************************************
|
||||||
|
|
@ -32,7 +32,7 @@ Deploy the demo
|
||||||
System requirements:
|
System requirements:
|
||||||
|
|
||||||
* `Python`_ version 3.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
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,4 @@ class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
basename="hello-world.txt",
|
basename="hello-world.txt",
|
||||||
file_path="/apache-modified-headers/hello-world.txt",
|
file_path="/apache-modified-headers/hello-world.txt",
|
||||||
)
|
)
|
||||||
self.assertEqual(response['X-Test'], 'header')
|
self.assertEqual(response["X-Test"], "header")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.apache import views
|
from demoproject.apache import views
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ optimized_by_decorator = x_sendfile(
|
||||||
def _modified_headers(request):
|
def _modified_headers(request):
|
||||||
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response["X-Test"] = 'header'
|
response["X-Test"] = "header"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,4 @@ class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
basename="hello-world.txt",
|
basename="hello-world.txt",
|
||||||
file_path="/lighttpd-modified-headers/hello-world.txt",
|
file_path="/lighttpd-modified-headers/hello-world.txt",
|
||||||
)
|
)
|
||||||
self.assertEqual(response['X-Test'], 'header')
|
self.assertEqual(response["X-Test"], "header")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""URL mapping."""
|
"""URL mapping."""
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from demoproject.lighttpd import views
|
from demoproject.lighttpd import views
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ optimized_by_decorator = x_sendfile(
|
||||||
def _modified_headers(request):
|
def _modified_headers(request):
|
||||||
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response["X-Test"] = 'header'
|
response["X-Test"] = "header"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,4 +70,4 @@ class ModifiedHeadersTestCase(django.test.TestCase):
|
||||||
with_buffering=None,
|
with_buffering=None,
|
||||||
limit_rate=None,
|
limit_rate=None,
|
||||||
)
|
)
|
||||||
self.assertEqual(response['X-Test'], 'header')
|
self.assertEqual(response["X-Test"], "header")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ optimized_by_decorator = x_accel_redirect(
|
||||||
def _modified_headers(request):
|
def _modified_headers(request):
|
||||||
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt")
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response["X-Test"] = 'header'
|
response["X-Test"] = "header"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Django settings for django-downloadview demo project."""
|
"""Django settings for django-downloadview demo project."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +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,
|
url, headers={"if-modified-since": f"Sat, 29 Oct {year} 19:43:31 GMT"}
|
||||||
HTTP_IF_MODIFIED_SINCE=f"Sat, 29 Oct {year} 19:43:31 GMT",
|
|
||||||
)
|
)
|
||||||
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||||
|
|
||||||
|
|
@ -55,7 +54,7 @@ class StaticPathTestCase(django.test.TestCase):
|
||||||
setup_file("1.txt")
|
setup_file("1.txt")
|
||||||
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
url = reverse("storage:static_path", kwargs={"path": "1.txt"})
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
url, HTTP_IF_MODIFIED_SINCE="Sat, 29 Oct 1980 19:43:31 GMT"
|
url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"}
|
||||||
)
|
)
|
||||||
assert_download_response(
|
assert_download_response(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Test suite for demoproject.download."""
|
"""Test suite for demoproject.download."""
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ middleware here, or combine a Django application with an application of another
|
||||||
framework.
|
framework.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""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 importlib.metadata
|
import importlib.metadata
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ See also `documentation of mod_xsendfile for Apache
|
||||||
Apache optimizations </optimizations/apache>`.
|
Apache optimizations </optimizations/apache>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.apache.decorators import x_sendfile # NoQA
|
from django_downloadview.apache.decorators import x_sendfile # NoQA
|
||||||
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
"""Decorators to apply Apache X-Sendfile on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
from django_downloadview.apache.middlewares import XSendfileMiddleware
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,11 +8,13 @@ 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, headers=None):
|
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."""
|
||||||
# content-type must be provided only as keyword argument to response
|
# content-type must be provided only as keyword argument to response
|
||||||
if headers and content_type:
|
if headers and content_type:
|
||||||
headers.pop('Content-Type', None)
|
headers.pop("Content-Type", None)
|
||||||
super().__init__(content_type=content_type, headers=headers)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""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_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ See also `documentation of X-Sendfile for Lighttpd
|
||||||
</optimizations/lighttpd>`.
|
</optimizations/lighttpd>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
|
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
|
||||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,11 +8,13 @@ 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, headers=None):
|
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."""
|
||||||
# content-type must be porvided only as keyword argument to response
|
# content-type must be porvided only as keyword argument to response
|
||||||
if headers and content_type:
|
if headers and content_type:
|
||||||
headers.pop('Content-Type', None)
|
headers.pop("Content-Type", None)
|
||||||
super().__init__(content_type=content_type, headers=headers)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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.abc
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
|
|
@ -36,6 +37,7 @@ class BaseDownloadMiddleware:
|
||||||
Subclasses **must** implement :py:meth:`process_download_response` method.
|
Subclasses **must** implement :py:meth:`process_download_response` method.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
|
|
@ -75,9 +77,8 @@ 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return (
|
return super().is_download_response(response) and bool(
|
||||||
super().is_download_response(response)
|
getattr(response.file, "url", None) or getattr(response.file, "name", None)
|
||||||
and bool(getattr(response.file, 'url', None) or getattr(response.file, 'name', None))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,7 +92,7 @@ class DownloadDispatcher:
|
||||||
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)
|
||||||
|
|
@ -100,7 +101,7 @@ class DownloadDispatcher:
|
||||||
|
|
||||||
def dispatch(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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
||||||
</optimizations/nginx>`.
|
</optimizations/nginx>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
from django_downloadview.nginx.decorators import x_accel_redirect # NoQA
|
||||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Decorators to apply Nginx X-Accel on a specific view."""
|
"""Decorators to apply Nginx X-Accel on a specific view."""
|
||||||
|
|
||||||
from django_downloadview.decorators import DownloadDecorator
|
from django_downloadview.decorators import DownloadDecorator
|
||||||
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -24,7 +25,7 @@ class XAccelRedirectResponse(ProxiedDownloadResponse):
|
||||||
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
"""Return a HttpResponse with headers for Nginx X-Accel-Redirect."""
|
||||||
# content-type must be porvided only as keyword argument to response
|
# content-type must be porvided only as keyword argument to response
|
||||||
if headers and content_type:
|
if headers and content_type:
|
||||||
headers.pop('Content-Type', None)
|
headers.pop("Content-Type", None)
|
||||||
super().__init__(content_type=content_type, headers=headers)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
for details.
|
for details.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -77,11 +78,10 @@ def content_disposition(filename):
|
||||||
# which can permit a reflected file download attack. The UTF-8
|
# which can permit a reflected file download attack. The UTF-8
|
||||||
# version is immune because it's not quoted.
|
# version is immune because it's not quoted.
|
||||||
ascii_filename = (
|
ascii_filename = (
|
||||||
encode_basename_ascii(filename).replace("\\", "\\\\").replace('"', r'\"')
|
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}"'
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Utility functions that may be implemented in external packages."""
|
"""Utility functions that may be implemented in external packages."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)
|
charset_pattern = re.compile(r"charset=(?P<charset>.+)$", re.I | re.U)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Views to stream files."""
|
"""Views to stream files."""
|
||||||
|
|
||||||
# API shortcuts.
|
# API shortcuts.
|
||||||
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
|
from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA
|
||||||
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
from django_downloadview.views.http import HTTPDownloadView # NoQA
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Base material for download views: :class:`DownloadMixin` and
|
"""Base material for download views: :class:`DownloadMixin` and
|
||||||
:class:`BaseDownloadView`"""
|
:class:`BaseDownloadView`"""
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from django.http import Http404, HttpResponseNotModified
|
from django.http import Http404, HttpResponseNotModified
|
||||||
|
|
@ -156,7 +157,7 @@ class DownloadMixin(object):
|
||||||
except exceptions.FileNotFound:
|
except exceptions.FileNotFound:
|
||||||
return self.file_not_found_response()
|
return self.file_not_found_response()
|
||||||
# Respect the If-Modified-Since header.
|
# Respect the If-Modified-Since header.
|
||||||
since = self.request.META.get("HTTP_IF_MODIFIED_SINCE", None)
|
since = self.request.headers.get("if-modified-since", None)
|
||||||
if since is not None:
|
if since is not None:
|
||||||
if not self.was_modified_since(self.file_instance, since):
|
if not self.was_modified_since(self.file_instance, since):
|
||||||
return self.not_modified_response(**response_kwargs)
|
return self.not_modified_response(**response_kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files given an URL, i.e. files you want to proxy."""
|
"""Stream files given an URL, i.e. files you want to proxy."""
|
||||||
|
|
||||||
from django_downloadview.files import HTTPFile
|
from django_downloadview.files import HTTPFile
|
||||||
from django_downloadview.views.base import BaseDownloadView
|
from django_downloadview.views.base import BaseDownloadView
|
||||||
|
|
||||||
|
|
@ -44,5 +45,5 @@ class HTTPDownloadView(BaseDownloadView):
|
||||||
request_factory=self.get_request_factory(),
|
request_factory=self.get_request_factory(),
|
||||||
name=self.get_basename(),
|
name=self.get_basename(),
|
||||||
url=self.get_url(),
|
url=self.get_url(),
|
||||||
**self.get_request_kwargs()
|
**self.get_request_kwargs(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files that live in models."""
|
"""Stream files that live in models."""
|
||||||
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from django_downloadview.exceptions import FileNotFound
|
from django_downloadview.exceptions import FileNotFound
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
""":class:`PathDownloadView`."""
|
""":class:`PathDownloadView`."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Stream files from storage."""
|
"""Stream files from storage."""
|
||||||
|
|
||||||
from django.core.files.storage import DefaultStorage
|
from django.core.files.storage import DefaultStorage
|
||||||
|
|
||||||
from django_downloadview.files import StorageFile
|
from django_downloadview.files import StorageFile
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- 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
|
import importlib.metadata
|
||||||
|
|
@ -51,7 +51,7 @@ author_slug = re.sub(r"([\w_.-]+)", "-", author)
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = importlib.metadata.version("django-downloadview")
|
release = importlib.metadata.version("django-downloadview")
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '.'.join(release.split('.')[:2])
|
version = ".".join(release.split(".")[:2])
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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::
|
||||||
|
|
||||||
|
|
@ -192,4 +192,4 @@ they should be equal.
|
||||||
|
|
||||||
.. target-notes::
|
.. target-notes::
|
||||||
|
|
||||||
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ possible, file wrappers do not embed file data, in order to save memory.
|
||||||
|
|
||||||
Learn more about available file wrappers in :doc:`files`.
|
Learn more about available file wrappers in :doc:`files`.
|
||||||
|
|
||||||
|
|
||||||
*****************************************************************
|
*****************************************************************
|
||||||
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
||||||
*****************************************************************
|
*****************************************************************
|
||||||
|
|
|
||||||
|
|
@ -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-
|
||||||
|
|
||||||
|
|
|
||||||
8
setup.py
8
setup.py
|
|
@ -10,7 +10,7 @@ setup(
|
||||||
setup_requires=["setuptools_scm"],
|
setup_requires=["setuptools_scm"],
|
||||||
description="Serve files with Django and reverse-proxies.",
|
description="Serve files with Django and reverse-proxies.",
|
||||||
long_description=open(os.path.join(here, "README.rst")).read(),
|
long_description=open(os.path.join(here, "README.rst")).read(),
|
||||||
long_description_content_type='text/x-rst',
|
long_description_content_type="text/x-rst",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"License :: OSI Approved :: BSD License",
|
"License :: OSI Approved :: BSD License",
|
||||||
|
|
@ -20,9 +20,9 @@ setup(
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
'Framework :: Django',
|
"Framework :: Django",
|
||||||
'Framework :: Django :: 4.2',
|
"Framework :: Django :: 4.2",
|
||||||
'Framework :: Django :: 5.0',
|
"Framework :: Django :: 5.0",
|
||||||
],
|
],
|
||||||
keywords=" ".join(
|
keywords=" ".join(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -130,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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests around :mod:`django_downloadview.io`."""
|
"""Tests around :mod:`django_downloadview.io`."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django_downloadview import BytesIteratorIO, TextIteratorIO
|
from django_downloadview import BytesIteratorIO, TextIteratorIO
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests around project's distribution and packaging."""
|
"""Tests around project's distribution and packaging."""
|
||||||
|
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -23,12 +24,9 @@ class DownloadResponseTestCase(unittest.TestCase):
|
||||||
def test_content_disposition_escaping(self):
|
def test_content_disposition_escaping(self):
|
||||||
"""Content-Disposition headers escape special characters."""
|
"""Content-Disposition headers escape special characters."""
|
||||||
response = DownloadResponse(
|
response = DownloadResponse(
|
||||||
"fake file",
|
"fake file", attachment=True, basename=r'"malicious\file.exe'
|
||||||
attachment=True,
|
|
||||||
basename=r'"malicious\file.exe'
|
|
||||||
)
|
)
|
||||||
headers = response.default_headers
|
headers = response.default_headers
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
r'filename="\"malicious\\file.exe"',
|
r'filename="\"malicious\\file.exe"', headers["Content-Disposition"]
|
||||||
headers["Content-Disposition"]
|
)
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests around :py:mod:`django_downloadview.sendfile`."""
|
"""Tests around :py:mod:`django_downloadview.sendfile`."""
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
import django.test
|
import django.test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests around :mod:`django_downloadview.views`."""
|
"""Tests around :mod:`django_downloadview.views`."""
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue