Merge pull request #213 from jazzband/prepare-2.4

Prepare 2.4 release
This commit is contained in:
Rémy HUBSCHER 2024-08-05 14:49:32 +02:00 committed by GitHub
commit 9f42cde8cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 181 additions and 67 deletions

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

@ -0,0 +1,18 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .

21
AUTHORS
View file

@ -6,22 +6,23 @@ Maintainer: Benoît Bryon <benoit@marmelune.net>
Original code by `PeopleDoc <https://www.people-doc.com/>`_ team: 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>

View file

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

View file

@ -11,7 +11,7 @@ This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree t
This document provides guidelines for people who want to contribute to 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

View file

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

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

View file

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

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

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

View file

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

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

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

View file

@ -7,6 +7,7 @@
for details. for details.
""" """
import warnings import warnings
from django.conf import settings from django.conf import settings

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

View file

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