././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/0000755000175100001770000000000000000000000016347 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8347323 django_downloadview-2.4.0/.github/0000755000175100001770000000000000000000000017707 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8427322 django_downloadview-2.4.0/.github/workflows/0000755000175100001770000000000000000000000021744 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.github/workflows/release.yml0000644000175100001770000000176200000000000024115 0ustar00runnerdocker00000000000000name: Release on: push: tags: - '*' jobs: build: if: github.repository == 'jazzband/django-downloadview' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v2 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.github/workflows/test.yml0000644000175100001770000000313600000000000023451 0ustar00runnerdocker00000000000000name: 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'] django-version: ['4.2', '5.0', '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@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 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@v2 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@v1 with: name: Python ${{ matrix.python-version }} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.gitignore0000644000175100001770000000047500000000000020345 0ustar00runnerdocker00000000000000# Local binaries. /bin/ # Libraries. /lib/ # Data files. /var/ coverage.xml .coverage/ # Python files. *.pyc *.pyo *.egg-info # Tox files. /.tox/ .eggs *.egg-info # Virtualenv files (created by tox). /build/ /dist/ # Virtual environments (created by user). /venv/ # Editors' temporary buffers. .*.swp *~ .idea ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.isort.cfg0000644000175100001770000000067000000000000020251 0ustar00runnerdocker00000000000000[settings] # # Needed for black compatibility multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 line_length=88 combine_as_imports=True # List sections with django and known_django=django known_downloadview=django_downloadview sections=FUTURE,STDLIB,DJANGO,DOWNLOADVIEW,THIRDPARTY,FIRSTPARTY,LOCALFOLDER # If set, imports will be sorted within their section independent to the import_type. force_sort_within_sections=True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.pre-commit-config.yaml0000644000175100001770000000312600000000000022632 0ustar00runnerdocker00000000000000repos: - 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/.readthedocs.yaml0000644000175100001770000000043500000000000021600 0ustar00runnerdocker00000000000000# .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: . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/AUTHORS0000644000175100001770000000172100000000000017420 0ustar00runnerdocker00000000000000###################### Authors & contributors ###################### Maintainer: Benoît Bryon Original code by `PeopleDoc `_ team: * Adam Chainz * Aleksi Häkli * Benoît Bryon * CJ * David Wolf <68775926+devidw@users.noreply.github.com> * Davide Setti * Erik Dykema * Fabre Florian * Hasan Ramezani * Jannis Leidel * John Hagen * Mariusz Felisiak * Martin Bächtold * Nikhil Benesch * Omer Katz * Peter Marheine * René Leonhardt * Rémy HUBSCHER * Tim Gates * zero13cool ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/CHANGELOG0000644000175100001770000001666700000000000017601 0ustar00runnerdocker00000000000000Changelog ========= This document describes changes between past releases. For information about future releases, check `milestones`_ and :doc:`/about/vision`. 2.4 (2024-08-05) ---------------- - Drop support for Python 3.6 - Escape malicious filenames - Handle headers in XAccel responses 2.3 (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) ---------------- - Add a SignedFileSystemStorage that signs file URLs for clients. (#151) 2.0 (2020-01-07) ---------------- - Drop support for Python 2.7. - Add black and isort. 1.10 (2020-01-07) ----------------- - Introduced support from Django 1.11, 2.2 and 3.0. - Drop support of Django 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 2.0 and 2.1 1.9 (2016-03-15) ---------------- - Feature #112 - Introduced support of Django 1.9. - Feature #113 - Introduced support of Python 3.5. - Feature #116 - ``HTTPFile`` has ``content_type`` property. It makes ``HTTPDownloadView`` proxy ``Content-Type`` header from remote location. 1.8 (2015-07-20) ---------------- Bugfixes. - Bugfix #103 - ``PathDownloadView.get_file()`` makes a single call to ``PathDownloadView.get_file()`` (was doing it twice). - Bugfix #104 - Pass numeric timestamp to Django's ``was_modified_since()`` (was passing a datetime). 1.7 (2015-06-13) ---------------- Bugfixes. - Bugfix #87 - Filenames with commas are now supported. In download responses, filename is now surrounded by double quotes. - Bugfix #97 - ``HTTPFile`` proxies bytes as ``BytesIteratorIO`` (was undecoded urllib3 file object). ``StringIteratorIO`` has been split into ``TextIteratorIO`` and ``BytesIteratorIO``. ``StringIteratorIO`` is deprecated but kept for backward compatibility as an alias for ``TextIteratorIO``. - Bugfix #92 - Run demo using ``make demo runserver`` (was broken). - Feature #99 - Tox runs project's tests with Python 2.7, 3.3 and 3.4, and with Django 1.5 to 1.8. - Refactoring #98 - Refreshed development environment: packaging, Tox and Sphinx. 1.6 (2014-03-03) ---------------- Python 3 support, development environment refactoring. - Feature #46: introduced support for Python>=3.3. - Feature #80: added documentation about "how to serve a file inline VS how to serve a file as attachment". Improved documentation of views' base options inherited from ``DownloadMixin``. - Feature #74: the Makefile in project's repository no longer creates a virtualenv. Developers setup the environment as they like, i.e. using virtualenv, virtualenvwrapper or whatever. Tests are run with tox. 1.5 (2013-11-29) ---------------- X-Sendfile support and helpers to migrate for `django-sendfile`. - Feature #2 - Introduced support of Lighttpd's x-Sendfile. - Feature #36 - Introduced support of Apache's mod_xsendfile. - Feature #41 - ``django_downloadview.sendfile`` is a port of `django-sendfile`'s ``sendfile`` function. The documentation contains notes about migrating from `django-sendfile` to `django-downloadview`. 1.4 (2013-11-24) ---------------- Bugfixes and documentation features. - Bugfix #43 - ``ObjectDownloadView`` returns HTTP 404 if model instance's file field is empty (was HTTP 500). - Bugfix #7 - Special characters in file names (``Content-Disposition`` header) are urlencoded. An US-ASCII fallback is also provided. - Feature #10 - `django-downloadview` is registered on djangopackages.com. - Feature #65 - INSTALL documentation shows "known good set" (KGS) of versions, i.e. versions that have been used in test environment. 1.3 (2013-11-08) ---------------- Big refactoring around middleware configuration, API readability and documentation. - Bugfix #57 - ``PathDownloadView`` opens files in binary mode (was text mode). - Bugfix #48 - Fixed ``basename`` assertion in ``assert_download_response``: checks ``Content-Disposition`` header. - Bugfix #49 - Fixed ``content`` assertion in ``assert_download_response``: checks only response's ``streaming_content`` attribute. - Bugfix #60 - ``VirtualFile.__iter__`` uses ``force_bytes()`` to support both "text-mode" and "binary-mode" content. See https://code.djangoproject.com/ticket/21321 - Feature #50 - Introduced ``django_downloadview.DownloadDispatcherMiddleware`` that iterates over a list of configurable download middlewares. Allows to plug several download middlewares with different configurations. This middleware is mostly dedicated to internal usage. It is used by ``SmartDownloadMiddleware`` described below. - Feature #42 - Documentation shows how to stream generated content (yield). Introduced ``django_downloadview.StringIteratorIO``. - Refactoring #51 - Dropped support of Python 2.6 - Refactoring #25 - Introduced ``django_downloadview.SmartDownloadMiddleware`` which allows to setup multiple optimization rules for one backend. Deprecates the following settings related to previous single-and-global middleware: * ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT`` * ``NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL`` * ``NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES`` * ``NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING`` * ``NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE`` - Refactoring #52 - ObjectDownloadView now inherits from SingleObjectMixin and BaseDownloadView (was DownloadMixin and BaseDetailView). Simplified DownloadMixin.render_to_response() signature. - Refactoring #40 - Documentation includes examples from demo project. - Refactoring #39 - Documentation focuses on usage, rather than API. Improved narrative documentation. - Refactoring #53 - Added base classes in ``django_downloadview.middlewares``, such as ``ProxiedDownloadMiddleware``. - Refactoring #54 - Expose most Python API directly in `django_downloadview` package. Simplifies ``import`` statements in client applications. Splitted nginx module in a package. - Added unit tests, improved code coverage. 1.2 (2013-05-28) ---------------- Bugfixes and documentation improvements. - Bugfix #26 - Prevented computation of virtual file's size, unless the file wrapper implements was_modified_since() method. - Bugfix #34 - Improved support of files that do not implement modification time. - Bugfix #35 - Fixed README conversion from reStructuredText to HTML (PyPI). 1.1 (2013-04-11) ---------------- Various improvements. Contains **backward incompatible changes.** - Added HTTPDownloadView to proxy to arbitrary URL. - Added VirtualDownloadView to support files living in memory. - Using StreamingHttpResponse introduced with Django 1.5. Makes Django 1.5 a requirement! - Added ``django_downloadview.test.assert_download_response`` utility. - Download views and response now use file wrappers. Most logic around file attributes, formerly in views, moved to wrappers. - Replaced DownloadView by PathDownloadView and StorageDownloadView. Use the right one depending on the use case. 1.0 (2012-12-04) ---------------- - Introduced optimizations for Nginx X-Accel: a middleware and a decorator - Introduced generic views: DownloadView and ObjectDownloadView - Initialized project .. rubric:: Notes & references .. target-notes:: .. _`milestones`: https://github.com/jazzband/django-downloadview/milestones ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/CODE_OF_CONDUCT.md0000644000175100001770000000450700000000000021154 0ustar00runnerdocker00000000000000# 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/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/CONTRIBUTING.rst0000644000175100001770000000533500000000000021016 0ustar00runnerdocker00000000000000############ Contributing ############ .. image:: https://jazzband.co/static/img/jazzband.svg :target: https://jazzband.co/ :alt: Jazzband This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. This document provides guidelines for people who want to contribute to ``django-downloadview``. ************** Create tickets ************** Please use the `bugtracker`_ **before** starting some work: * check if the bug or feature request has already been filed. It may have been answered too! * else create a new ticket. * if you plan to contribute, tell us, so that we are given an opportunity to give feedback as soon as possible. * Then, in your commit messages, reference the ticket with some ``refs #TICKET-ID`` syntax. ****************** Use topic branches ****************** * Work in branches. * Prefix your branch with the ticket ID corresponding to the issue. As an example, if you are working on ticket #23 which is about contribute documentation, name your branch like ``23-contribute-doc``. * If you work in a development branch and want to refresh it with changes from master, please `rebase`_ or `merge-based rebase`_, i.e. do not merge master. *********** Fork, clone *********** Clone ``django-downloadview`` repository (adapt to use your own fork): .. code:: sh git clone git@github.com:jazzband/django-downloadview.git cd django-downloadview/ ************* Usual actions ************* The ``Makefile`` is the reference card for usual actions in development environment: * Install development toolkit with `pip`_: ``make develop``. * Run tests with `tox`_: ``make test``. * Build documentation: ``make documentation``. It builds `Sphinx`_ documentation in ``var/docs/html/index.html``. * Release project with `zest.releaser`_: ``make release``. * Cleanup local repository: ``make clean``, ``make distclean`` and ``make maintainer-clean``. See also ``make help``. ********************* Demo project included ********************* The ``demo`` included in project's repository is part of the tests and documentation. Maintain it along with code and documentation. .. rubric:: Notes & references .. target-notes:: .. _`bugtracker`: https://github.com/jazzband/django-downloadview/issues .. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing .. _`merge-based rebase`: https://tech.people-doc.com/psycho-rebasing.html .. _`pip`: https://pypi.python.org/pypi/pip/ .. _`tox`: https://tox.readthedocs.io/ .. _`Sphinx`: https://pypi.python.org/pypi/Sphinx/ .. _`zest.releaser`: https://pypi.python.org/pypi/zest.releaser/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/INSTALL0000644000175100001770000000336600000000000017410 0ustar00runnerdocker00000000000000####### Install ####### .. note:: If you want to install a development environment, please see :doc:`/contributing`. ************ Requirements ************ `django-downloadview` has been tested with `Python`_ 3.7, 3.8, 3.9 and 3.10. Other versions may work, but they are not part of the test suite at the moment. Installing `django-downloadview` will automatically trigger the installation of the following requirements: .. literalinclude:: /../setup.py :language: python :start-after: BEGIN requirements :end-before: END requirements ************ As a library ************ In most cases, you will use `django-downloadview` as a dependency of another project. In such a case, you should add `django-downloadview` in your main project's requirements. Typically in :file:`setup.py`: .. code:: python from setuptools import setup setup( install_requires=[ 'django-downloadview', #... ] # ... ) Then when you install your main project with your favorite package manager (like `pip`_), `django-downloadview` and its recursive dependencies will automatically be installed. ********** Standalone ********** You can install `django-downloadview` with your favorite Python package manager. As an example with `pip`_: .. code:: sh pip install django-downloadview ***** Check ***** Check `django-downloadview` has been installed: .. code:: sh python -c "import django_downloadview;print(django_downloadview.__version__)" You should get installed `django-downloadview`'s version. .. rubric:: Notes & references .. seealso:: * :doc:`/settings` * :doc:`/about/changelog` * :doc:`/about/license` .. target-notes:: .. _`Python`: https://www.python.org/ .. _`pip`: https://pip.pypa.io/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/LICENSE0000644000175100001770000000276100000000000017362 0ustar00runnerdocker00000000000000####### License ####### Copyright (c) 2012-2014, Benoît Bryon. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of django-downloadview nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/MANIFEST.in0000644000175100001770000000025300000000000020105 0ustar00runnerdocker00000000000000recursive-include django_downloadview * global-exclude *.pyc include AUTHORS include CHANGELOG include CONTRIBUTING.rst include INSTALL include LICENSE include README.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/Makefile0000644000175100001770000000411500000000000020010 0ustar00runnerdocker00000000000000# Reference card for usual actions in development environment. # # For standard installation of django-downloadview as a library, see INSTALL. # # For details about django-downloadview's development environment, see # CONTRIBUTING.rst. # PIP = pip TOX = tox BLACK = black ISORT = isort #: help - Display callable targets. .PHONY: help help: @echo "Reference card for usual actions in development environment." @echo "Here are available targets:" @egrep -o "^#: (.+)" [Mm]akefile | sed 's/#: /* /' #: develop - Install minimal development utilities. .PHONY: develop develop: $(PIP) install -e . #: clean - Basic cleanup, mostly temporary files. .PHONY: clean clean: find . -name "*.pyc" -delete find . -name '*.pyo' -delete find . -name "__pycache__" -delete #: distclean - Remove local builds, such as *.egg-info. .PHONY: distclean distclean: clean rm -rf *.egg rm -rf *.egg-info rm -rf demo/*.egg-info #: maintainer-clean - Remove almost everything that can be re-generated. .PHONY: maintainer-clean maintainer-clean: distclean rm -rf build/ rm -rf dist/ rm -rf .tox/ #: test - Run test suites. .PHONY: test test: mkdir -p var $(PIP) install -e .[test] $(TOX) #: documentation - Build documentation (Sphinx, README, ...) .PHONY: documentation documentation: sphinx readme #: sphinx - Build Sphinx documentation (docs). .PHONY: sphinx sphinx: $(TOX) -e sphinx #: readme - Build standalone documentation files (README, CONTRIBUTING...). .PHONY: readme readme: $(TOX) -e readme #: demo - Setup demo project. .PHONY: demo demo: pip install -e . pip install -e demo demo migrate --noinput # Install fixtures. mkdir -p var/media/object var/media/object-other/ var/media/nginx cp -r demo/demoproject/fixtures/* var/media/object/ cp -r demo/demoproject/fixtures/* var/media/object-other/ cp -r demo/demoproject/fixtures/* var/media/nginx/ demo loaddata demo.json #: runserver - Run demo server. .PHONY: runserver runserver: demo demo runserver .PHONY: black black: $(BLACK) demo tests django_downloadview .PHONY: isort isort: $(ISORT) --recursive django_downloadview tests demo ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/PKG-INFO0000644000175100001770000000707700000000000017457 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: django-downloadview Version: 2.4.0 Summary: Serve files with Django and reverse-proxies. Home-page: https://django-downloadview.readthedocs.io/ Author: Benoît Bryon Author-email: benoit@marmelune.net License: BSD Keywords: file stream download FileField ImageField x-accel x-accel-redirect x-sendfile sendfile mod_xsendfile offload Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Framework :: Django Classifier: Framework :: Django :: 4.2 Classifier: Framework :: Django :: 5.0 Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE License-File: AUTHORS Requires-Dist: Django>=4.2 Requires-Dist: requests Provides-Extra: test Requires-Dist: tox; extra == "test" ################### django-downloadview ################### .. 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, ...); * files are stored somewhere or generated somehow (local filesystem, remote storage, memory...); * ``django-downloadview`` helps you stream the files with very little code; * ``django-downloadview`` helps you improve performances with reverse proxies, via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile. ******* Example ******* Let's serve a file stored in a file field of some model: .. code:: python from django.conf.urls import url, url_patterns from django_downloadview import ObjectDownloadView from demoproject.download.models import Document # A model with a FileField # ObjectDownloadView inherits from django.views.generic.BaseDetailView. download = ObjectDownloadView.as_view(model=Document, file_field='file') url_patterns = ('', url('^download/(?P[A-Za-z0-9_-]+)/$', download, name='download'), ) ********* Resources ********* * Documentation: https://django-downloadview.readthedocs.io * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/jazzband/django-downloadview * Bugtracker: https://github.com/jazzband/django-downloadview/issues * Continuous integration: https://github.com/jazzband/django-downloadview/actions * Roadmap: https://github.com/jazzband/django-downloadview/milestones .. _`Django`: https://djangoproject.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/README.rst0000644000175100001770000000476300000000000020050 0ustar00runnerdocker00000000000000################### django-downloadview ################### .. 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, ...); * files are stored somewhere or generated somehow (local filesystem, remote storage, memory...); * ``django-downloadview`` helps you stream the files with very little code; * ``django-downloadview`` helps you improve performances with reverse proxies, via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile. ******* Example ******* Let's serve a file stored in a file field of some model: .. code:: python from django.conf.urls import url, url_patterns from django_downloadview import ObjectDownloadView from demoproject.download.models import Document # A model with a FileField # ObjectDownloadView inherits from django.views.generic.BaseDetailView. download = ObjectDownloadView.as_view(model=Document, file_field='file') url_patterns = ('', url('^download/(?P[A-Za-z0-9_-]+)/$', download, name='download'), ) ********* Resources ********* * Documentation: https://django-downloadview.readthedocs.io * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/jazzband/django-downloadview * Bugtracker: https://github.com/jazzband/django-downloadview/issues * Continuous integration: https://github.com/jazzband/django-downloadview/actions * Roadmap: https://github.com/jazzband/django-downloadview/milestones .. _`Django`: https://djangoproject.com ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8427322 django_downloadview-2.4.0/demo/0000755000175100001770000000000000000000000017273 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/README.rst0000644000175100001770000000305100000000000020761 0ustar00runnerdocker00000000000000############ Demo project ############ `Demo folder in project's repository`_ contains a Django project to illustrate ``django-downloadview`` usage. ***************************************** Documentation includes code from the demo ***************************************** Almost every example in the documentation comes from the demo: * discover examples in the documentation; * browse related code and tests in demo project. Examples in documentation are tested via demo project! *********************** Browse demo code online *********************** See `demo folder in project's repository`_. *************** Deploy the demo *************** System requirements: * `Python`_ version 3.7+, available as ``python`` command. .. note:: You may use `Virtualenv`_ to make sure the active ``python`` is the right one. * ``make`` and ``wget`` to use the provided :file:`Makefile`. Execute: .. code-block:: sh git clone git@github.com:jazzband/django-downloadview.git cd django-downloadview/ make runserver It installs and runs the demo server on localhost, port 8000. So have a look at ``http://localhost:8000/``. .. note:: If you cannot execute the Makefile, read it and adapt the few commands it contains to your needs. Browse and use :file:`demo/demoproject/` as a sandbox. ********** References ********** .. target-notes:: .. _`demo folder in project's repository`: https://github.com/jazzband/django-downloadview/tree/master/demo/demoproject/ .. _`Python`: http://python.org .. _`Virtualenv`: http://virtualenv.org ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8427322 django_downloadview-2.4.0/demo/demoproject/0000755000175100001770000000000000000000000021606 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/__init__.py0000644000175100001770000000000000000000000023705 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8427322 django_downloadview-2.4.0/demo/demoproject/apache/0000755000175100001770000000000000000000000023027 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/apache/__init__.py0000644000175100001770000000003400000000000025135 0ustar00runnerdocker00000000000000"""Apache optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/apache/models.py0000644000175100001770000000005500000000000024664 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/apache/tests.py0000644000175100001770000000370700000000000024552 0ustar00runnerdocker00000000000000import os from django.core.files.base import ContentFile import django.test from django.urls import reverse from django_downloadview.apache import assert_x_sendfile from demoproject.apache.views import storage, storage_dir def setup_file(): if not os.path.exists(storage_dir): os.makedirs(storage_dir) storage.save("hello-world.txt", ContentFile("Hello world!\n")) class OptimizedByMiddlewareTestCase(django.test.TestCase): def test_response(self): """'apache:optimized_by_middleware' returns X-Sendfile response.""" setup_file() url = reverse("apache:optimized_by_middleware") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/apache-optimized-by-middleware/hello-world.txt", ) class OptimizedByDecoratorTestCase(django.test.TestCase): def test_response(self): """'apache:optimized_by_decorator' returns X-Sendfile response.""" setup_file() url = reverse("apache:optimized_by_decorator") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/apache-optimized-by-decorator/hello-world.txt", ) class ModifiedHeadersTestCase(django.test.TestCase): def test_response(self): """'apache:modified_headers' returns X-Sendfile response.""" setup_file() url = reverse("apache:modified_headers") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/apache-modified-headers/hello-world.txt", ) self.assertEqual(response["X-Test"], "header") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/apache/urls.py0000644000175100001770000000076200000000000024373 0ustar00runnerdocker00000000000000"""URL mapping.""" from django.urls import path from demoproject.apache import views app_name = "apache" urlpatterns = [ path( "optimized-by-middleware/", views.optimized_by_middleware, name="optimized_by_middleware", ), path( "optimized-by-decorator/", views.optimized_by_decorator, name="optimized_by_decorator", ), path( "modified_headers/", views.modified_headers, name="modified_headers", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/apache/views.py0000644000175100001770000000176400000000000024546 0ustar00runnerdocker00000000000000import os from django.conf import settings from django.core.files.storage import FileSystemStorage from django_downloadview import StorageDownloadView from django_downloadview.apache import x_sendfile storage_dir = os.path.join(settings.MEDIA_ROOT, "apache") storage = FileSystemStorage( location=storage_dir, base_url="".join([settings.MEDIA_URL, "apache/"]) ) optimized_by_middleware = StorageDownloadView.as_view( storage=storage, path="hello-world.txt" ) optimized_by_decorator = x_sendfile( StorageDownloadView.as_view(storage=storage, path="hello-world.txt"), source_url=storage.base_url, destination_dir="/apache-optimized-by-decorator/", ) def _modified_headers(request): view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt") response = view(request) response["X-Test"] = "header" return response modified_headers = x_sendfile( _modified_headers, source_url=storage.base_url, destination_dir="/apache-modified-headers/", ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8427322 django_downloadview-2.4.0/demo/demoproject/fixtures/0000755000175100001770000000000000000000000023457 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/fixtures/demo.json0000644000175100001770000000026300000000000025277 0ustar00runnerdocker00000000000000[ { "pk": 1, "model": "object.document", "fields": { "slug": "hello-world", "file": "object/hello-world.txt" } } ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/fixtures/hello-world.txt0000644000175100001770000000001500000000000026444 0ustar00runnerdocker00000000000000Hello world! ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8467324 django_downloadview-2.4.0/demo/demoproject/http/0000755000175100001770000000000000000000000022565 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/http/__init__.py0000644000175100001770000000030600000000000024675 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """Demo for :class:`django_downloadview.HTTPDownloadView`. Code in this package is included in documentation's :doc:`/views/http`. Make sure to maintain both together. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/http/models.py0000644000175100001770000000005500000000000024422 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/http/tests.py0000644000175100001770000000150500000000000024302 0ustar00runnerdocker00000000000000import django.test from django.urls import reverse from django_downloadview import assert_download_response class SimpleURLTestCase(django.test.TestCase): def test_download_response(self): """'simple_url' serves 'hello-world.txt' from Github.""" url = reverse("http:simple_url") response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) class AvatarTestCase(django.test.TestCase): def test_download_response(self): """HTTPDownloadView proxies Content-Type header.""" url = reverse("http:avatar_url") response = self.client.get(url) assert_download_response(self, response, mime_type="image/png") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/http/urls.py0000644000175100001770000000034200000000000024123 0ustar00runnerdocker00000000000000from django.urls import path from demoproject.http import views app_name = "http" urlpatterns = [ path("simple_url/", views.simple_url, name="simple_url"), path("avatar_url/", views.avatar_url, name="avatar_url"), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/http/views.py0000644000175100001770000000122100000000000024270 0ustar00runnerdocker00000000000000from django_downloadview import HTTPDownloadView class SimpleURLDownloadView(HTTPDownloadView): def get_url(self): """Return URL of hello-world.txt file on GitHub.""" return ( "https://raw.githubusercontent.com" "/jazzband/django-downloadview" "/b7f660c5e3f37d918b106b02c5af7a887acc0111" "/demo/demoproject/download/fixtures/hello-world.txt" ) class GithubAvatarDownloadView(HTTPDownloadView): def get_url(self): return "https://avatars0.githubusercontent.com/u/235204" simple_url = SimpleURLDownloadView.as_view() avatar_url = GithubAvatarDownloadView.as_view() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8467324 django_downloadview-2.4.0/demo/demoproject/lighttpd/0000755000175100001770000000000000000000000023425 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/lighttpd/__init__.py0000644000175100001770000000003600000000000025535 0ustar00runnerdocker00000000000000"""Lighttpd optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/lighttpd/models.py0000644000175100001770000000005500000000000025262 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/lighttpd/tests.py0000644000175100001770000000373500000000000025151 0ustar00runnerdocker00000000000000import os from django.core.files.base import ContentFile import django.test from django.urls import reverse from django_downloadview.lighttpd import assert_x_sendfile from demoproject.lighttpd.views import storage, storage_dir def setup_file(): if not os.path.exists(storage_dir): os.makedirs(storage_dir) storage.save("hello-world.txt", ContentFile("Hello world!\n")) class OptimizedByMiddlewareTestCase(django.test.TestCase): def test_response(self): """'lighttpd:optimized_by_middleware' returns X-Sendfile response.""" setup_file() url = reverse("lighttpd:optimized_by_middleware") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/lighttpd-optimized-by-middleware/hello-world.txt", ) class OptimizedByDecoratorTestCase(django.test.TestCase): def test_response(self): """'lighttpd:optimized_by_decorator' returns X-Sendfile response.""" setup_file() url = reverse("lighttpd:optimized_by_decorator") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/lighttpd-optimized-by-decorator/hello-world.txt", ) class ModifiedHeadersTestCase(django.test.TestCase): def test_response(self): """'lighttpd:modified_headers' returns X-Sendfile response.""" setup_file() url = reverse("lighttpd:modified_headers") response = self.client.get(url) assert_x_sendfile( self, response, content_type="text/plain; charset=utf-8", basename="hello-world.txt", file_path="/lighttpd-modified-headers/hello-world.txt", ) self.assertEqual(response["X-Test"], "header") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/lighttpd/urls.py0000644000175100001770000000076600000000000024775 0ustar00runnerdocker00000000000000"""URL mapping.""" from django.urls import path from demoproject.lighttpd import views app_name = "lighttpd" urlpatterns = [ path( "optimized-by-middleware/", views.optimized_by_middleware, name="optimized_by_middleware", ), path( "optimized-by-decorator/", views.optimized_by_decorator, name="optimized_by_decorator", ), path( "modified_headers/", views.modified_headers, name="modified_headers", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/lighttpd/views.py0000644000175100001770000000177600000000000025147 0ustar00runnerdocker00000000000000import os from django.conf import settings from django.core.files.storage import FileSystemStorage from django_downloadview import StorageDownloadView from django_downloadview.lighttpd import x_sendfile storage_dir = os.path.join(settings.MEDIA_ROOT, "lighttpd") storage = FileSystemStorage( location=storage_dir, base_url="".join([settings.MEDIA_URL, "lighttpd/"]) ) optimized_by_middleware = StorageDownloadView.as_view( storage=storage, path="hello-world.txt" ) optimized_by_decorator = x_sendfile( StorageDownloadView.as_view(storage=storage, path="hello-world.txt"), source_url=storage.base_url, destination_dir="/lighttpd-optimized-by-decorator/", ) def _modified_headers(request): view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt") response = view(request) response["X-Test"] = "header" return response modified_headers = x_sendfile( _modified_headers, source_url=storage.base_url, destination_dir="/lighttpd-modified-headers/", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/manage.py0000755000175100001770000000042600000000000023415 0ustar00runnerdocker00000000000000#!/usr/bin/env python import os import sys from django.core.management import execute_from_command_line def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{__package__}.settings") execute_from_command_line(sys.argv) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8467324 django_downloadview-2.4.0/demo/demoproject/nginx/0000755000175100001770000000000000000000000022731 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/nginx/__init__.py0000644000175100001770000000003300000000000025036 0ustar00runnerdocker00000000000000"""Nginx optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/nginx/models.py0000644000175100001770000000005500000000000024566 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/nginx/tests.py0000644000175100001770000000446600000000000024457 0ustar00runnerdocker00000000000000import os from django.core.files.base import ContentFile import django.test from django.urls import reverse from django_downloadview.nginx import assert_x_accel_redirect from demoproject.nginx.views import storage, storage_dir def setup_file(): if not os.path.exists(storage_dir): os.makedirs(storage_dir) storage.save("hello-world.txt", ContentFile("Hello world!\n")) class OptimizedByMiddlewareTestCase(django.test.TestCase): def test_response(self): """'nginx:optimized_by_middleware' returns X-Accel response.""" setup_file() url = reverse("nginx:optimized_by_middleware") 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-optimized-by-middleware/hello-world.txt", expires=None, with_buffering=None, limit_rate=None, ) class OptimizedByDecoratorTestCase(django.test.TestCase): def test_response(self): """'nginx:optimized_by_decorator' returns X-Accel response.""" setup_file() url = reverse("nginx:optimized_by_decorator") 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-optimized-by-decorator/hello-world.txt", expires=None, with_buffering=None, limit_rate=None, ) class ModifiedHeadersTestCase(django.test.TestCase): def test_response(self): """'nginx:modified_headers' returns X-Sendfile response.""" setup_file() url = reverse("nginx:modified_headers") response = self.client.get(url) assert_x_accel_redirect( self, response, content_type="text/plain; charset=utf-8", charset="utf-8", basename="hello-world.txt", redirect_url="/nginx-modified-headers/hello-world.txt", expires=None, with_buffering=None, limit_rate=None, ) self.assertEqual(response["X-Test"], "header") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/nginx/urls.py0000644000175100001770000000076000000000000024273 0ustar00runnerdocker00000000000000"""URL mapping.""" from django.urls import path from demoproject.nginx import views app_name = "nginx" urlpatterns = [ path( "optimized-by-middleware/", views.optimized_by_middleware, name="optimized_by_middleware", ), path( "optimized-by-decorator/", views.optimized_by_decorator, name="optimized_by_decorator", ), path( "modified_headers/", views.modified_headers, name="modified_headers", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/nginx/views.py0000644000175100001770000000200100000000000024431 0ustar00runnerdocker00000000000000import os from django.conf import settings from django.core.files.storage import FileSystemStorage from django_downloadview import StorageDownloadView from django_downloadview.nginx import x_accel_redirect storage_dir = os.path.join(settings.MEDIA_ROOT, "nginx") storage = FileSystemStorage( location=storage_dir, base_url="".join([settings.MEDIA_URL, "nginx/"]) ) optimized_by_middleware = StorageDownloadView.as_view( storage=storage, path="hello-world.txt" ) optimized_by_decorator = x_accel_redirect( StorageDownloadView.as_view(storage=storage, path="hello-world.txt"), source_url=storage.base_url, destination_url="/nginx-optimized-by-decorator/", ) def _modified_headers(request): view = StorageDownloadView.as_view(storage=storage, path="hello-world.txt") response = view(request) response["X-Test"] = "header" return response modified_headers = x_accel_redirect( _modified_headers, source_url=storage.base_url, destination_url="/nginx-modified-headers/", ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8467324 django_downloadview-2.4.0/demo/demoproject/object/0000755000175100001770000000000000000000000023054 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/object/__init__.py0000644000175100001770000000031200000000000025161 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """Demo for :class:`django_downloadview.ObjectDownloadView`. Code in this package is included in documentation's :doc:`/views/object`. Make sure to maintain both together. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/object/models.py0000644000175100001770000000037100000000000024712 0ustar00runnerdocker00000000000000from django.db import models class Document(models.Model): slug = models.SlugField() file = models.FileField(upload_to="object") another_file = models.FileField(upload_to="object-other") basename = models.CharField(max_length=100) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/object/tests.py0000644000175100001770000000522100000000000024570 0ustar00runnerdocker00000000000000from django.core.files.base import ContentFile import django.test from django.urls import reverse from django_downloadview import assert_download_response, temporary_media_root from demoproject.object.models import Document # Fixtures. slug = "hello-world" basename = "hello-world.txt" file_name = "file.txt" another_name = "another_file.txt" file_content = "Hello world!\n" another_content = "Goodbye world!\n" def setup_document(): document = Document(slug=slug, basename=basename) document.file.save(file_name, ContentFile(file_content), save=False) document.another_file.save(another_name, ContentFile(another_content), save=False) document.save() return document class DefaultFileTestCase(django.test.TestCase): @temporary_media_root() def test_download_response(self): """'default_file' streams Document.file.""" setup_document() url = reverse("object:default_file", kwargs={"slug": slug}) response = self.client.get(url) assert_download_response( self, response, content=file_content, basename=file_name, mime_type="text/plain", ) class AnotherFileTestCase(django.test.TestCase): @temporary_media_root() def test_download_response(self): """'another_file' streams Document.another_file.""" setup_document() url = reverse("object:another_file", kwargs={"slug": slug}) response = self.client.get(url) assert_download_response( self, response, content=another_content, basename=another_name, mime_type="text/plain", ) class DeserializedBasenameTestCase(django.test.TestCase): @temporary_media_root() def test_download_response(self): "'deserialized_basename' streams Document.file with custom basename." setup_document() url = reverse("object:deserialized_basename", kwargs={"slug": slug}) response = self.client.get(url) assert_download_response( self, response, content=file_content, basename=basename, mime_type="text/plain", ) class InlineFileTestCase(django.test.TestCase): @temporary_media_root() def test_download_response(self): "'inline_file_view' streams Document.file inline." setup_document() url = reverse("object:inline_file", kwargs={"slug": slug}) response = self.client.get(url) assert_download_response( self, response, content=file_content, mime_type="text/plain", attachment=False, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/object/urls.py0000644000175100001770000000124100000000000024411 0ustar00runnerdocker00000000000000from django.urls import re_path from demoproject.object import views app_name = "object" urlpatterns = [ re_path( r"^default-file/(?P[a-zA-Z0-9_-]+)/$", views.default_file_view, name="default_file", ), re_path( r"^another-file/(?P[a-zA-Z0-9_-]+)/$", views.another_file_view, name="another_file", ), re_path( r"^deserialized_basename/(?P[a-zA-Z0-9_-]+)/$", views.deserialized_basename_view, name="deserialized_basename", ), re_path( r"^inline-file/(?P[a-zA-Z0-9_-]+)/$", views.inline_file_view, name="inline_file", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/object/views.py0000644000175100001770000000133100000000000024561 0ustar00runnerdocker00000000000000from django_downloadview import ObjectDownloadView from demoproject.object.models import Document #: Serve ``file`` attribute of ``Document`` model. default_file_view = ObjectDownloadView.as_view(model=Document) #: Serve ``another_file`` attribute of ``Document`` model. another_file_view = ObjectDownloadView.as_view( model=Document, file_field="another_file" ) #: Serve ``file`` attribute of ``Document`` model, using client-side filename #: from model. deserialized_basename_view = ObjectDownloadView.as_view( model=Document, basename_field="basename" ) #: Serve ``file`` attribute of ``Document`` model, inline (not as attachment). inline_file_view = ObjectDownloadView.as_view(model=Document, attachment=False) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8467324 django_downloadview-2.4.0/demo/demoproject/path/0000755000175100001770000000000000000000000022542 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/path/__init__.py0000644000175100001770000000030600000000000024652 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """Demo for :class:`django_downloadview.PathDownloadView`. Code in this package is included in documentation's :doc:`/views/path`. Make sure to maintain both together. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/path/models.py0000644000175100001770000000005500000000000024377 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/path/tests.py0000644000175100001770000000175100000000000024262 0ustar00runnerdocker00000000000000import django.test from django.urls import reverse from django_downloadview import assert_download_response class StaticPathTestCase(django.test.TestCase): def test_download_response(self): """'static_path' serves 'fixtures/hello-world.txt'.""" url = reverse("path:static_path") response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) class DynamicPathTestCase(django.test.TestCase): def test_download_response(self): """'dynamic_path' serves 'fixtures/{path}'.""" url = reverse("path:dynamic_path", kwargs={"path": "hello-world.txt"}) response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/path/urls.py0000644000175100001770000000050300000000000024077 0ustar00runnerdocker00000000000000from django.urls import path, re_path from demoproject.path import views app_name = "path" urlpatterns = [ path("static-path/", views.static_path, name="static_path"), re_path( r"^dynamic-path/(?P[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$", views.dynamic_path, name="dynamic_path", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/path/views.py0000644000175100001770000000212600000000000024252 0ustar00runnerdocker00000000000000import os from django_downloadview import PathDownloadView # Let's initialize some fixtures. app_dir = os.path.dirname(os.path.abspath(__file__)) project_dir = os.path.dirname(app_dir) fixtures_dir = os.path.join(project_dir, "fixtures") #: Path to a text file that says 'Hello world!'. hello_world_path = os.path.join(fixtures_dir, "hello-world.txt") #: Serve ``fixtures/hello-world.txt`` file. static_path = PathDownloadView.as_view(path=hello_world_path) class DynamicPathDownloadView(PathDownloadView): """Serve file in ``settings.MEDIA_ROOT``. .. warning:: Make sure to prevent "../" in path via URL patterns. .. note:: This particular setup would be easier to perform with :class:`StorageDownloadView` """ def get_path(self): """Return path inside fixtures directory.""" # Get path from URL resolvers or as_view kwarg. relative_path = super().get_path() # Make it absolute. absolute_path = os.path.join(fixtures_dir, relative_path) return absolute_path dynamic_path = DynamicPathDownloadView.as_view() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/settings.py0000644000175100001770000001051000000000000024015 0ustar00runnerdocker00000000000000"""Django settings for django-downloadview demo project.""" import os # Configure some relative directories. demoproject_dir = os.path.dirname(os.path.abspath(__file__)) demo_dir = os.path.dirname(demoproject_dir) root_dir = os.path.dirname(demo_dir) data_dir = os.path.join(root_dir, "var") cfg_dir = os.path.join(root_dir, "etc") # Mandatory settings. ROOT_URLCONF = "demoproject.urls" WSGI_APPLICATION = "demoproject.wsgi.application" # Database. DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(data_dir, "db.sqlite"), } } # Required. SECRET_KEY = "This is a secret made public on project's repository." # Media and static files. MEDIA_ROOT = os.path.join(data_dir, "media") MEDIA_URL = "/media/" STATIC_ROOT = os.path.join(data_dir, "static") STATIC_URL = "/static/" # Applications. INSTALLED_APPS = ( # The actual django-downloadview demo. "demoproject", "demoproject.object", # Demo around ObjectDownloadView "demoproject.storage", # Demo around StorageDownloadView "demoproject.path", # Demo around PathDownloadView "demoproject.http", # Demo around HTTPDownloadView "demoproject.virtual", # Demo around VirtualDownloadView "demoproject.nginx", # Sample optimizations for Nginx X-Accel. "demoproject.apache", # Sample optimizations for Apache X-Sendfile. "demoproject.lighttpd", # Sample optimizations for Lighttpd X-Sendfile. # Standard Django applications. "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", ) # BEGIN middlewares MIDDLEWARE = [ "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 # Specific configuration for django_downloadview.SmartDownloadMiddleware. # BEGIN backend DOWNLOADVIEW_BACKEND = "django_downloadview.nginx.XAccelRedirectMiddleware" # END backend """Could also be: DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware' DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware' """ # BEGIN rules DOWNLOADVIEW_RULES = [ { "source_url": "/media/nginx/", "destination_url": "/nginx-optimized-by-middleware/", }, ] # END rules DOWNLOADVIEW_RULES += [ { "source_url": "/media/apache/", "destination_dir": "/apache-optimized-by-middleware/", # Bypass global default backend with additional argument "backend". # Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be # enough. Here, the django_downloadview demo project needs to # demonstrate usage of several backends. "backend": "django_downloadview.apache.XSendfileMiddleware", }, { "source_url": "/media/lighttpd/", "destination_dir": "/lighttpd-optimized-by-middleware/", # Bypass global default backend with additional argument "backend". # Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be # enough. Here, the django_downloadview demo project needs to # demonstrate usage of several backends. "backend": "django_downloadview.lighttpd.XSendfileMiddleware", }, ] # Test/development settings. DEBUG = True TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], "OPTIONS": { "debug": DEBUG, "context_processors": [ # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this # list if you haven't customized them: "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ], }, }, ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8507323 django_downloadview-2.4.0/demo/demoproject/storage/0000755000175100001770000000000000000000000023252 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/__init__.py0000644000175100001770000000031400000000000025361 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """Demo for :class:`django_downloadview.StorageDownloadView`. Code in this package is included in documentation's :doc:`/views/storage`. Make sure to maintain both together. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/models.py0000644000175100001770000000005500000000000025107 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/storage.py0000644000175100001770000000012700000000000025270 0ustar00runnerdocker00000000000000from django.core.files.storage import FileSystemStorage storage = FileSystemStorage() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/tests.py0000644000175100001770000000721500000000000024773 0ustar00runnerdocker00000000000000import datetime import unittest from django.core.files.base import ContentFile from django.http.response import HttpResponseNotModified import django.test from django.urls import reverse from django_downloadview import ( assert_download_response, setup_view, temporary_media_root, ) from demoproject.storage import views # Fixtures. file_content = "Hello world!\n" def setup_file(path): views.storage.save(path, ContentFile(file_content)) class StaticPathTestCase(django.test.TestCase): @temporary_media_root() def test_download_response(self): """'storage:static_path' streams file by path.""" setup_file("1.txt") url = reverse("storage:static_path", kwargs={"path": "1.txt"}) response = self.client.get(url) assert_download_response( self, response, content=file_content, basename="1.txt", mime_type="text/plain", ) @temporary_media_root() def test_not_modified_download_response(self): """'storage:static_path' sends not modified response if unmodified.""" setup_file("1.txt") url = reverse("storage:static_path", kwargs={"path": "1.txt"}) year = datetime.date.today().year + 4 response = self.client.get( url, headers={"if-modified-since": f"Sat, 29 Oct {year} 19:43:31 GMT"} ) self.assertTrue(isinstance(response, HttpResponseNotModified)) @temporary_media_root() def test_modified_since_download_response(self): """'storage:static_path' streams file if modified.""" setup_file("1.txt") url = reverse("storage:static_path", kwargs={"path": "1.txt"}) response = self.client.get( url, headers={"if-modified-since": "Sat, 29 Oct 1980 19:43:31 GMT"} ) assert_download_response( self, response, content=file_content, basename="1.txt", mime_type="text/plain", ) class DynamicPathIntegrationTestCase(django.test.TestCase): """Integration tests around ``storage:dynamic_path`` URL.""" @temporary_media_root() def test_download_response(self): """'dynamic_path' streams file by generated path. As we use ``self.client``, this test involves the whole Django stack, including settings, middlewares, decorators... So we need to setup a file, the storage, and an URL. This test actually asserts the URL ``storage:dynamic_path`` streams a file in storage. """ setup_file("1.TXT") url = reverse("storage:dynamic_path", kwargs={"path": "1.txt"}) response = self.client.get(url) assert_download_response( self, response, content=file_content, basename="1.TXT", mime_type="text/plain", ) class DynamicPathUnitTestCase(unittest.TestCase): """Unit tests around ``views.DynamicStorageDownloadView``.""" def test_get_path(self): """DynamicStorageDownloadView.get_path() returns uppercase path. Uses :func:`~django_downloadview.test.setup_view` to target only overriden methods. This test does not involve URLconf, middlewares or decorators. It is fast. It has clear scope. It does not assert ``storage:dynamic_path`` URL works. It targets only custom ``DynamicStorageDownloadView`` class. """ view = setup_view( views.DynamicStorageDownloadView(), django.test.RequestFactory().get("/fake-url"), path="dummy path", ) path = view.get_path() self.assertEqual(path, "DUMMY PATH") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/urls.py0000644000175100001770000000062200000000000024611 0ustar00runnerdocker00000000000000from django.urls import re_path from demoproject.storage import views app_name = "storage" urlpatterns = [ re_path( r"^static-path/(?P[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$", views.static_path, name="static_path", ), re_path( r"^dynamic-path/(?P[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$", views.dynamic_path, name="dynamic_path", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/storage/views.py0000644000175100001770000000100700000000000024757 0ustar00runnerdocker00000000000000from django.core.files.storage import FileSystemStorage from django_downloadview import StorageDownloadView storage = FileSystemStorage() #: Serve file using ``path`` argument. static_path = StorageDownloadView.as_view(storage=storage) class DynamicStorageDownloadView(StorageDownloadView): """Serve file of storage by path.upper().""" def get_path(self): """Return uppercase path.""" return super().get_path().upper() dynamic_path = DynamicStorageDownloadView.as_view(storage=storage) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8507323 django_downloadview-2.4.0/demo/demoproject/templates/0000755000175100001770000000000000000000000023604 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/templates/home.html0000644000175100001770000000160000000000000025417 0ustar00runnerdocker00000000000000 django-downloadview demo

Welcome to django-downloadview demo!

Here are some demo links. Browse the code to see how they are implemented

Serving files with Django

In the following views, Django streams the files, no optimization has been setup.

Optimized downloads

In the following views, Django delegates actual streaming to another server, for improved performances.

Since nginx and other servers aren't installed on the demo, you will get raw "X-Sendfile" responses. Look at the headers!

././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/tests.py0000644000175100001770000000056000000000000023323 0ustar00runnerdocker00000000000000"""Test suite for demoproject.download.""" from django.test import TestCase from django.urls import reverse class HomeViewTestCase(TestCase): """Test homepage.""" def test_get(self): """Homepage returns HTTP 200.""" home_url = reverse("home") response = self.client.get(home_url) self.assertEqual(response.status_code, 200) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/urls.py0000644000175100001770000000224400000000000023147 0ustar00runnerdocker00000000000000from django.urls import include, path from django.views.generic import TemplateView home = TemplateView.as_view(template_name="home.html") urlpatterns = [ # ObjectDownloadView. path( "object/", include("demoproject.object.urls", namespace="object"), ), # StorageDownloadView. path( "storage/", include("demoproject.storage.urls", namespace="storage"), ), # PathDownloadView. path("path/", include("demoproject.path.urls", namespace="path")), # HTTPDownloadView. path("http/", include("demoproject.http.urls", namespace="http")), # VirtualDownloadView. path( "virtual/", include("demoproject.virtual.urls", namespace="virtual"), ), # Nginx optimizations. path( "nginx/", include("demoproject.nginx.urls", namespace="nginx"), ), # Apache optimizations. path( "apache/", include("demoproject.apache.urls", namespace="apache"), ), # Lighttpd optimizations. path( "lighttpd/", include("demoproject.lighttpd.urls", namespace="lighttpd"), ), # An informative homepage. path("", home, name="home"), ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8507323 django_downloadview-2.4.0/demo/demoproject/virtual/0000755000175100001770000000000000000000000023274 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/virtual/__init__.py0000644000175100001770000000031400000000000025403 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """Demo for :class:`django_downloadview.VirtualDownloadView`. Code in this package is included in documentation's :doc:`/views/virtual`. Make sure to maintain both together. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/virtual/models.py0000644000175100001770000000005500000000000025131 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/virtual/tests.py0000644000175100001770000000261100000000000025010 0ustar00runnerdocker00000000000000import django.test from django.urls import reverse from django_downloadview import assert_download_response class TextTestCase(django.test.TestCase): def test_download_response(self): """'virtual:text' serves 'hello-world.txt' from unicode.""" url = reverse("virtual:text") response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) class StringIOTestCase(django.test.TestCase): def test_download_response(self): """'virtual:stringio' serves 'hello-world.txt' from stringio.""" url = reverse("virtual:stringio") response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) class GeneratedTestCase(django.test.TestCase): def test_download_response(self): """'virtual:generated' serves 'hello-world.txt' from generator.""" url = reverse("virtual:generated") response = self.client.get(url) assert_download_response( self, response, content="Hello world!\n", basename="hello-world.txt", mime_type="text/plain", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/virtual/urls.py0000644000175100001770000000051500000000000024634 0ustar00runnerdocker00000000000000from django.urls import path from demoproject.virtual import views app_name = "virtual" urlpatterns = [ path("text/", views.TextDownloadView.as_view(), name="text"), path("stringio/", views.StringIODownloadView.as_view(), name="stringio"), path("gerenated/", views.GeneratedDownloadView.as_view(), name="generated"), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/virtual/views.py0000644000175100001770000000164600000000000025012 0ustar00runnerdocker00000000000000from io import StringIO from django.core.files.base import ContentFile from django_downloadview import TextIteratorIO, VirtualDownloadView, VirtualFile class TextDownloadView(VirtualDownloadView): def get_file(self): """Return :class:`django.core.files.base.ContentFile` object.""" return ContentFile(b"Hello world!\n", name="hello-world.txt") class StringIODownloadView(VirtualDownloadView): def get_file(self): """Return wrapper on ``six.StringIO`` object.""" file_obj = StringIO("Hello world!\n") return VirtualFile(file_obj, name="hello-world.txt") def generate_hello(): yield "Hello " yield "world!" yield "\n" class GeneratedDownloadView(VirtualDownloadView): def get_file(self): """Return wrapper on ``StringIteratorIO`` object.""" file_obj = TextIteratorIO(generate_hello()) return VirtualFile(file_obj, name="hello-world.txt") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/demoproject/wsgi.py0000755000175100001770000000221300000000000023132 0ustar00runnerdocker00000000000000"""WSGI config for Django-DownloadView demo project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover this application via the ``WSGI_APPLICATION`` setting. Usually you will have the standard Django WSGI application here, but it also might make sense to replace the whole Django WSGI application with a custom one that later delegates to the Django one. For example, you could introduce WSGI middleware here, or combine a Django application with an application of another framework. """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__) # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. application = get_wsgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/demo/setup.py0000644000175100001770000000153100000000000021005 0ustar00runnerdocker00000000000000import os from setuptools import setup here = os.path.abspath(os.path.dirname(__file__)) setup( name="django-downloadview-demo", version="1.0", description="Serve files with Django and reverse-proxies.", long_description=open(os.path.join(here, "README.rst")).read(), classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Framework :: Django", ], author="Benoît Bryon", author_email="benoit@marmelune.net", url="https://django-downloadview.readthedocs.io/", license="BSD", packages=["demoproject"], include_package_data=True, zip_safe=False, install_requires=["django-downloadview", "pytest-django"], entry_points={"console_scripts": ["demo = demoproject.manage:main"]}, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8507323 django_downloadview-2.4.0/django_downloadview/0000755000175100001770000000000000000000000022373 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/__init__.py0000644000175100001770000000036100000000000024504 0ustar00runnerdocker00000000000000"""Serve files with Django and reverse proxies.""" from django_downloadview.api import * # NoQA import importlib.metadata #: Module version, as defined in PEP-0396. __version__ = importlib.metadata.version(__package__.replace("-", "_")) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8547323 django_downloadview-2.4.0/django_downloadview/apache/0000755000175100001770000000000000000000000023614 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/apache/__init__.py0000644000175100001770000000101200000000000025717 0ustar00runnerdocker00000000000000"""Optimizations for Apache. See also `documentation of mod_xsendfile for Apache `_ and :doc:`narrative documentation about Apache optimizations `. """ # API shortcuts. from django_downloadview.apache.decorators import x_sendfile # NoQA from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA from django_downloadview.apache.response import XSendfileResponse # NoQA from django_downloadview.apache.tests import assert_x_sendfile # NoQA ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/apache/decorators.py0000644000175100001770000000102200000000000026326 0ustar00runnerdocker00000000000000"""Decorators to apply Apache X-Sendfile on a specific view.""" from django_downloadview.apache.middlewares import XSendfileMiddleware from django_downloadview.decorators import DownloadDecorator def x_sendfile(view_func, *args, **kwargs): """Apply :class:`~django_downloadview.apache.middlewares.XSendfileMiddleware` to ``view_func``. Proxies (``*args``, ``**kwargs``) to middleware constructor. """ decorator = DownloadDecorator(XSendfileMiddleware) return decorator(view_func, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/apache/middlewares.py0000644000175100001770000000245100000000000026470 0ustar00runnerdocker00000000000000from django_downloadview.apache.response import XSendfileResponse from django_downloadview.middlewares import ( NoRedirectionMatch, ProxiedDownloadMiddleware, ) class XSendfileMiddleware(ProxiedDownloadMiddleware): """Configurable middleware, for use in decorators or in global middlewares. Standard Django middlewares are configured globally via settings. Instances of this class are to be configured individually. It makes it possible to use this class as the factory in :py:class:`django_downloadview.decorators.DownloadDecorator`. """ def __init__( self, get_response=None, source_dir=None, source_url=None, destination_dir=None ): """Constructor.""" super().__init__(get_response, source_dir, source_url, destination_dir) def process_download_response(self, request, response): """Replace DownloadResponse instances by XSendfileResponse ones.""" try: redirect_url = self.get_redirect_url(response) except NoRedirectionMatch: return response return XSendfileResponse( file_path=redirect_url, content_type=response["Content-Type"], basename=response.basename, attachment=response.attachment, headers=response.headers, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/apache/response.py0000644000175100001770000000154600000000000026032 0ustar00runnerdocker00000000000000"""Apache's specific responses.""" import os.path from django_downloadview.response import ProxiedDownloadResponse, content_disposition class XSendfileResponse(ProxiedDownloadResponse): "Delegates serving file to Apache via X-Sendfile header." def __init__( self, file_path, content_type, basename=None, attachment=True, headers=None ): """Return a HttpResponse with headers for Apache X-Sendfile.""" # content-type must be provided only as keyword argument to response if headers and content_type: headers.pop("Content-Type", None) super().__init__(content_type=content_type, headers=headers) if attachment: self.basename = basename or os.path.basename(file_path) self["Content-Disposition"] = content_disposition(self.basename) self["X-Sendfile"] = file_path ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/apache/tests.py0000644000175100001770000000411700000000000025333 0ustar00runnerdocker00000000000000from django_downloadview.apache.response import XSendfileResponse class XSendfileValidator: """Utility class to validate XSendfileResponse instances. See also :py:func:`assert_x_sendfile` shortcut function. """ def __call__(self, test_case, response, **assertions): """Assert that ``response`` is a valid X-Sendfile response. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``file_path``: the value of "X-Sendfile" header. """ self.assert_x_sendfile_response(test_case, response) for key, value in assertions.items(): assert_func = getattr(self, "assert_%s" % key) assert_func(test_case, response, value) def assert_x_sendfile_response(self, test_case, response): test_case.assertTrue(isinstance(response, XSendfileResponse)) def assert_basename(self, test_case, response, value): test_case.assertEqual(response.basename, value) def assert_content_type(self, test_case, response, value): test_case.assertEqual(response["Content-Type"], value) def assert_file_path(self, test_case, response, value): test_case.assertEqual(response["X-Sendfile"], value) def assert_attachment(self, test_case, response, value): header = "Content-Disposition" if value: test_case.assertTrue(response[header].startswith("attachment")) else: test_case.assertFalse(header in response) def assert_x_sendfile(test_case, response, **assertions): """Make ``test_case`` assert that ``response`` is a XSendfileResponse. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``file_path``: the value of "X-Sendfile" header. """ validator = XSendfileValidator() return validator(test_case, response, **assertions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/api.py0000644000175100001770000000151000000000000023513 0ustar00runnerdocker00000000000000# flake8: noqa """Declaration of API shortcuts.""" from django_downloadview.files import HTTPFile, StorageFile, VirtualFile from django_downloadview.io import BytesIteratorIO, TextIteratorIO from django_downloadview.middlewares import ( BaseDownloadMiddleware, DownloadDispatcherMiddleware, SmartDownloadMiddleware, ) from django_downloadview.response import DownloadResponse, ProxiedDownloadResponse from django_downloadview.shortcuts import sendfile from django_downloadview.test import ( assert_download_response, setup_view, temporary_media_root, ) from django_downloadview.views import ( BaseDownloadView, DownloadMixin, HTTPDownloadView, ObjectDownloadView, PathDownloadView, StorageDownloadView, VirtualDownloadView, ) # Backward compatibility. StringIteratorIO = TextIteratorIO ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/decorators.py0000644000175100001770000000472100000000000025116 0ustar00runnerdocker00000000000000"""View decorators. See also decorators provided by server-specific modules, such as :func:`django_downloadview.nginx.x_accel_redirect`. """ from functools import wraps from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.signing import BadSignature, SignatureExpired, TimestampSigner class DownloadDecorator(object): """View decorator factory to apply middleware to ``view_func``'s response. Middleware instance is built from ``middleware_factory`` with ``*args`` and ``**kwargs``. Middleware factory is typically a class, such as some :py:class:`django_downloadview.BaseDownloadMiddleware` subclass. Response is built from view, then the middleware's ``process_response`` method is applied on response. """ def __init__(self, middleware_factory): """Create a download view decorator.""" self.middleware_factory = middleware_factory def __call__(self, view_func, *middleware_args, **middleware_kwargs): """Return ``view_func`` decorated with response middleware.""" def decorated(request, *view_args, **view_kwargs): """Return view's response modified by middleware.""" response = view_func(request, *view_args, **view_kwargs) middleware = self.middleware_factory(*middleware_args, **middleware_kwargs) return middleware.process_response(request, response) return decorated def _signature_is_valid(request): """ Validator that raises a PermissionDenied error on invalid and mismatching signatures. """ signer = TimestampSigner() signature = request.GET.get("X-Signature") expiration = getattr(settings, "DOWNLOADVIEW_URL_EXPIRATION", None) try: signature_path = signer.unsign(signature, max_age=expiration) except SignatureExpired as e: raise PermissionDenied("Signature expired") from e except BadSignature as e: raise PermissionDenied("Signature invalid") from e except Exception as e: raise PermissionDenied("Signature error") from e if request.path != signature_path: raise PermissionDenied("Signature mismatch") def signature_required(function): """ Decorator that checks for X-Signature query parameter to authorize access to views. """ @wraps(function) def decorator(request, *args, **kwargs): _signature_is_valid(request) return function(request, *args, **kwargs) return decorator ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/exceptions.py0000644000175100001770000000036300000000000025130 0ustar00runnerdocker00000000000000"""Custom exceptions.""" class FileNotFound(IOError): """Requested file does not exist. This exception is to be raised when operations (such as read) fail because file does not exist (whatever the storage or location). """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/files.py0000644000175100001770000001616500000000000024060 0ustar00runnerdocker00000000000000"""File wrappers for use as exchange data between views and responses.""" from io import BytesIO from urllib.parse import urlparse from django.core.files.base import File from django.utils.encoding import force_bytes from django_downloadview.io import BytesIteratorIO import requests class StorageFile(File): """A file in a Django storage. This class looks like :py:class:`django.db.models.fields.files.FieldFile`, but unrelated to model instance. """ def __init__(self, storage, name, file=None): """Constructor. storage: Some :py:class:`django.core.files.storage.Storage` instance. name: File identifier in storage, usually a filename as a string. """ self.storage = storage self.name = name self.file = file def _get_file(self): """Getter for :py:attr:``file`` property.""" if not hasattr(self, "_file") or self._file is None: self._file = self.storage.open(self.name, "rb") return self._file def _set_file(self, file): """Setter for :py:attr:``file`` property.""" self._file = file def _del_file(self): """Deleter for :py:attr:``file`` property.""" del self._file #: Required by django.core.files.utils.FileProxy. file = property(_get_file, _set_file, _del_file) def open(self, mode="rb"): """Retrieves the specified file from storage and return open() result. Proxy to self.storage.open(self.name, mode). """ return self.storage.open(self.name, mode) def save(self, content): """Saves new content to the file. Proxy to self.storage.save(self.name). The content should be a proper File object, ready to be read from the beginning. """ return self.storage.save(self.name, content) @property def path(self): """Return a local filesystem path which is suitable for open(). Proxy to self.storage.path(self.name). May raise NotImplementedError if storage doesn't support file access with Python's built-in open() function """ return self.storage.path(self.name) def delete(self): """Delete the specified file from the storage system. Proxy to self.storage.delete(self.name). """ return self.storage.delete(self.name) def exists(self): """Return True if file already exists in the storage system. If False, then the name is available for a new file. """ return self.storage.exists(self.name) @property def size(self): """Return the total size, in bytes, of the file. Proxy to self.storage.size(self.name). """ return self.storage.size(self.name) @property def url(self): """Return an absolute URL where the file's contents can be accessed. Proxy to self.storage.url(self.name). """ return self.storage.url(self.name) @property def accessed_time(self): """Return the last accessed time (as datetime object) of the file. Proxy to self.storage.accessed_time(self.name). """ try: return self.storage.get_accessed_time(self.name) except AttributeError: return self.storage.accessed_time(self.name) @property def created_time(self): """Return the creation time (as datetime object) of the file. Proxy to self.storage.created_time(self.name). """ try: return self.storage.get_created_time(self.name) except AttributeError: return self.storage.created_time(self.name) @property def modified_time(self): """Return the last modification time (as datetime object) of the file. Proxy to self.storage.modified_time(self.name). """ try: return self.storage.get_modified_time(self.name) except AttributeError: return self.storage.modified_time(self.name) class VirtualFile(File): """Wrapper for files that live in memory.""" def __init__(self, file=None, name="", url="", size=None): """Constructor. file: File object. Typically an io.StringIO. name: File basename. url: File URL. """ super().__init__(file, name) self.url = url if size is not None: self._size = size def _get_size(self): try: return self._size except AttributeError: try: self._size = self.file.size except AttributeError: self._size = len(self.file.getvalue()) return self._size def _set_size(self, value): return super()._set_size(value) size = property(_get_size, _set_size) def __iter__(self): """Same as ``File.__iter__()`` but using ``force_bytes()``. See https://code.djangoproject.com/ticket/21321 """ # Iterate over this file-like object by newlines buffer_ = None for chunk in self.chunks(): chunk_buffer = BytesIO(force_bytes(chunk)) for line in chunk_buffer: if buffer_: line = buffer_ + line buffer_ = None # If this is the end of a line, yield # otherwise, wait for the next round if line[-1] in ("\n", "\r"): yield line else: buffer_ = line if buffer_ is not None: yield buffer_ class HTTPFile(File): """Wrapper for files that live on remote HTTP servers. Acts as a proxy. Uses https://pypi.python.org/pypi/requests. Always sets "stream=True" in requests kwargs. """ def __init__(self, request_factory=requests.get, url="", name="", **kwargs): self.request_factory = request_factory self.url = url if name is None: parts = urlparse(url) if parts.path: # Name from path. self.name = parts.path.strip("/").rsplit("/", 1)[-1] else: # Name from domain. self.name = parts.netloc else: self.name = name kwargs["stream"] = True self.request_kwargs = kwargs @property def request(self): try: return self._request except AttributeError: self._request = self.request_factory(self.url, **self.request_kwargs) return self._request @property def file(self): try: return self._file except AttributeError: content = self.request.iter_content(decode_unicode=False) self._file = BytesIteratorIO(content) return self._file @property def size(self): """Return the total size, in bytes, of the file. Reads response's "content-length" header. """ return self.request.headers["Content-Length"] @property def content_type(self): """Return content type of the file (from original response).""" return self.request.headers["Content-Type"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/io.py0000644000175100001770000000740000000000000023355 0ustar00runnerdocker00000000000000"""Low-level IO operations, for use with file wrappers.""" import io from django.utils.encoding import force_bytes, force_str class TextIteratorIO(io.TextIOBase): """A dynamically generated TextIO-like object. Original code by Matt Joiner from: * http://stackoverflow.com/questions/12593576/ * https://gist.github.com/anacrolix/3788413 """ def __init__(self, iterator): #: Iterator/generator for content. self._iter = iterator #: Internal buffer. self._left = "" def readable(self): return True def _read1(self, n=None): while not self._left: try: self._left = next(self._iter) except StopIteration: break else: # Make sure we handle text. self._left = force_str(self._left) ret = self._left[:n] self._left = self._left[len(ret) :] return ret def read(self, n=None): """Return content up to ``n`` length.""" chunks = [] if n is None or n < 0: while True: m = self._read1() if not m: break chunks.append(m) else: while n > 0: m = self._read1(n) if not m: break n -= len(m) chunks.append(m) return "".join(chunks) def readline(self): chunks = [] while True: i = self._left.find("\n") if i == -1: chunks.append(self._left) try: self._left = next(self._iter) except StopIteration: self._left = "" break else: chunks.append(self._left[: i + 1]) self._left = self._left[i + 1 :] break return "".join(chunks) class BytesIteratorIO(io.BytesIO): """A dynamically generated BytesIO-like object. Original code by Matt Joiner from: * http://stackoverflow.com/questions/12593576/ * https://gist.github.com/anacrolix/3788413 """ def __init__(self, iterator): #: Iterator/generator for content. self._iter = iterator #: Internal buffer. self._left = b"" def readable(self): return True def _read1(self, n=None): while not self._left: try: self._left = next(self._iter) except StopIteration: break else: # Make sure we handle text. self._left = force_bytes(self._left) ret = self._left[:n] self._left = self._left[len(ret) :] return ret def read(self, n=None): """Return content up to ``n`` length.""" chunks = [] if n is None or n < 0: while True: m = self._read1() if not m: break chunks.append(m) else: while n > 0: m = self._read1(n) if not m: break n -= len(m) chunks.append(m) return b"".join(chunks) def readline(self): chunks = [] while True: i = self._left.find(b"\n") if i == -1: chunks.append(self._left) try: self._left = next(self._iter) except StopIteration: self._left = b"" break else: chunks.append(self._left[: i + 1]) self._left = self._left[i + 1 :] break return b"".join(chunks) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8547323 django_downloadview-2.4.0/django_downloadview/lighttpd/0000755000175100001770000000000000000000000024212 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/lighttpd/__init__.py0000644000175100001770000000107600000000000026327 0ustar00runnerdocker00000000000000"""Optimizations for Lighttpd. See also `documentation of X-Sendfile for Lighttpd `_ and :doc:`narrative documentation about Lighttpd optimizations `. """ # API shortcuts. from django_downloadview.lighttpd.decorators import x_sendfile # NoQA from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA from django_downloadview.lighttpd.response import XSendfileResponse # NoQA from django_downloadview.lighttpd.tests import assert_x_sendfile # NoQA ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/lighttpd/decorators.py0000644000175100001770000000103000000000000026723 0ustar00runnerdocker00000000000000"""Decorators to apply Lighttpd X-Sendfile on a specific view.""" from django_downloadview.decorators import DownloadDecorator from django_downloadview.lighttpd.middlewares import XSendfileMiddleware def x_sendfile(view_func, *args, **kwargs): """Apply :class:`~django_downloadview.lighttpd.middlewares.XSendfileMiddleware` to ``view_func``. Proxies (``*args``, ``**kwargs``) to middleware constructor. """ decorator = DownloadDecorator(XSendfileMiddleware) return decorator(view_func, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/lighttpd/middlewares.py0000644000175100001770000000245300000000000027070 0ustar00runnerdocker00000000000000from django_downloadview.lighttpd.response import XSendfileResponse from django_downloadview.middlewares import ( NoRedirectionMatch, ProxiedDownloadMiddleware, ) class XSendfileMiddleware(ProxiedDownloadMiddleware): """Configurable middleware, for use in decorators or in global middlewares. Standard Django middlewares are configured globally via settings. Instances of this class are to be configured individually. It makes it possible to use this class as the factory in :py:class:`django_downloadview.decorators.DownloadDecorator`. """ def __init__( self, get_response=None, source_dir=None, source_url=None, destination_dir=None ): """Constructor.""" super().__init__(get_response, source_dir, source_url, destination_dir) def process_download_response(self, request, response): """Replace DownloadResponse instances by XSendfileResponse ones.""" try: redirect_url = self.get_redirect_url(response) except NoRedirectionMatch: return response return XSendfileResponse( file_path=redirect_url, content_type=response["Content-Type"], basename=response.basename, attachment=response.attachment, headers=response.headers, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/lighttpd/response.py0000644000175100001770000000155400000000000026427 0ustar00runnerdocker00000000000000"""Lighttpd's specific responses.""" import os.path from django_downloadview.response import ProxiedDownloadResponse, content_disposition class XSendfileResponse(ProxiedDownloadResponse): "Delegates serving file to Lighttpd via X-Sendfile header." def __init__( self, file_path, content_type, basename=None, attachment=True, headers=None ): """Return a HttpResponse with headers for Lighttpd X-Sendfile.""" # content-type must be porvided only as keyword argument to response if headers and content_type: headers.pop("Content-Type", None) super().__init__(content_type=content_type, headers=headers) if attachment: self.basename = basename or os.path.basename(file_path) self["Content-Disposition"] = content_disposition(self.basename) self["X-Sendfile"] = file_path ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/lighttpd/tests.py0000644000175100001770000000167000000000000025732 0ustar00runnerdocker00000000000000import django_downloadview.apache.tests from django_downloadview.lighttpd.response import XSendfileResponse class XSendfileValidator(django_downloadview.apache.tests.XSendfileValidator): """Utility class to validate XSendfileResponse instances. See also :py:func:`assert_x_sendfile` shortcut function. """ def assert_x_sendfile_response(self, test_case, response): test_case.assertTrue(isinstance(response, XSendfileResponse)) def assert_x_sendfile(test_case, response, **assertions): """Make ``test_case`` assert that ``response`` is a XSendfileResponse. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``file_path``: the value of "X-Sendfile" header. """ validator = XSendfileValidator() return validator(test_case, response, **assertions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/middlewares.py0000644000175100001770000002040700000000000025250 0ustar00runnerdocker00000000000000"""Base material for download middlewares. Download middlewares capture :py:class:`django_downloadview.DownloadResponse` responses and may replace them with optimized download responses. """ import collections.abc import copy import os from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django_downloadview.response import DownloadResponse from django_downloadview.utils import import_member #: Sentinel value to detect whether configuration is to be loaded from Django #: settings or not. AUTO_CONFIGURE = object() def is_download_response(response): """Return ``True`` if ``response`` is a download response. Current implementation returns True if ``response`` is an instance of :py:class:`django_downloadview.response.DownloadResponse`. """ return isinstance(response, DownloadResponse) class BaseDownloadMiddleware: """Base (abstract) Django middleware that handles download responses. 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): """Return True if ``response`` can be considered as a file download. By default, this method uses :py:func:`django_downloadview.middlewares.is_download_response`. Override this method if you want a different behaviour. """ return is_download_response(response) def process_response(self, request, response): """Call `process_download_response()` if ``response`` is download.""" if self.is_download_response(response): return self.process_download_response(request, response) return response def process_download_response(self, request, response): """Handle file download response.""" raise NotImplementedError() class RealDownloadMiddleware(BaseDownloadMiddleware): """Download middleware that cannot handle virtual files.""" def is_download_response(self, response): """Return True for DownloadResponse, except for "virtual" files. This implementation cannot handle files that live in memory or which are to be dynamically iterated over. So, we capture only responses whose file attribute have either an URL or a file name. """ return super().is_download_response(response) and bool( getattr(response.file, "url", None) or getattr(response.file, "name", None) ) class DownloadDispatcher: def __init__(self, middlewares=AUTO_CONFIGURE): #: List of children middlewares. self.middlewares = middlewares if self.middlewares is AUTO_CONFIGURE: self.auto_configure_middlewares() def auto_configure_middlewares(self): """Populate :attr:`middlewares` from ``settings.DOWNLOADVIEW_MIDDLEWARES``.""" for key, import_string, kwargs in getattr( settings, "DOWNLOADVIEW_MIDDLEWARES", [] ): factory = import_member(import_string) middleware = factory(**kwargs) self.middlewares.append((key, middleware)) def dispatch(self, request, response): """Dispatches job to children middlewares.""" for key, middleware in self.middlewares: response = middleware.process_response(request, response) return response 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.""" def __init__( self, get_response, backend_factory=AUTO_CONFIGURE, backend_options=AUTO_CONFIGURE, ): """Constructor.""" super().__init__(get_response, middlewares=[]) #: Callable (typically a class) to instantiate backend (typically a #: :class:`DownloadMiddleware` subclass). self.backend_factory = backend_factory if self.backend_factory is AUTO_CONFIGURE: self.auto_configure_backend_factory() #: List of positional or keyword arguments to instantiate backend #: instances. self.backend_options = backend_options if self.backend_options is AUTO_CONFIGURE: self.auto_configure_backend_options() def auto_configure_backend_factory(self): "Assign :attr:`backend_factory` from ``settings.DOWNLOADVIEW_BACKEND``" try: self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND) except AttributeError: raise ImproperlyConfigured( "SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_BACKEND" ) def auto_configure_backend_options(self): """Populate :attr:`dispatcher` using :attr:`factory` and ``settings.DOWNLOADVIEW_RULES``.""" try: options_list = copy.deepcopy(settings.DOWNLOADVIEW_RULES) except AttributeError: raise ImproperlyConfigured( "SmartDownloadMiddleware requires " "settings.DOWNLOADVIEW_RULES" ) for key, options in enumerate(options_list): args = [] kwargs = {} if isinstance(options, collections.abc.Mapping): # Using kwargs. kwargs = options else: args = options if "backend" in kwargs: # Specific backend for this rule. factory = import_member(kwargs["backend"]) del kwargs["backend"] else: # Fallback to global backend. factory = self.backend_factory middleware_instance = factory(*args, **kwargs) self.dispatcher.middlewares.append((key, middleware_instance)) class NoRedirectionMatch(Exception): """Response object does not match redirection rules.""" class ProxiedDownloadMiddleware(RealDownloadMiddleware): """Base class for middlewares that use optimizations of reverse proxies.""" def __init__( self, get_response, source_dir=None, source_url=None, destination_url=None ): """Constructor.""" super().__init__(get_response) self.source_dir = source_dir self.source_url = source_url self.destination_url = destination_url def get_redirect_url(self, response): """Return redirect URL for file wrapped into response.""" url = None file_url = "" if self.source_url: try: file_url = response.file.url except AttributeError: pass else: if file_url.startswith(self.source_url): file_url = file_url[len(self.source_url) :] url = file_url file_name = "" if url is None and self.source_dir: try: file_name = response.file.name except AttributeError: pass else: if file_name.startswith(self.source_dir): file_name = os.path.relpath(file_name, self.source_dir) url = file_name.replace(os.path.sep, "/") if url is None: message = ( """Couldn't capture/convert file attributes into a """ """redirection. """ """``source_url`` is "%(source_url)s", """ """file's URL is "%(file_url)s". """ """``source_dir`` is "%(source_dir)s", """ """file's name is "%(file_name)s". """ % { "source_url": self.source_url, "file_url": file_url, "source_dir": self.source_dir, "file_name": file_name, } ) raise NoRedirectionMatch(message) return "/".join((self.destination_url.rstrip("/"), url.lstrip("/"))) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8547323 django_downloadview-2.4.0/django_downloadview/nginx/0000755000175100001770000000000000000000000023516 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/__init__.py0000644000175100001770000000101000000000000025617 0ustar00runnerdocker00000000000000"""Optimizations for Nginx. See also `Nginx X-accel documentation `_ and :doc:`narrative documentation about Nginx optimizations `. """ # API shortcuts. from django_downloadview.nginx.decorators import x_accel_redirect # NoQA from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware # NoQA from django_downloadview.nginx.response import XAccelRedirectResponse # NoQA from django_downloadview.nginx.tests import assert_x_accel_redirect # NoQA ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/decorators.py0000644000175100001770000000104100000000000026231 0ustar00runnerdocker00000000000000"""Decorators to apply Nginx X-Accel on a specific view.""" from django_downloadview.decorators import DownloadDecorator from django_downloadview.nginx.middlewares import XAccelRedirectMiddleware def x_accel_redirect(view_func, *args, **kwargs): """Apply :class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware` to ``view_func``. Proxies (``*args``, ``**kwargs``) to middleware constructor. """ decorator = DownloadDecorator(XAccelRedirectMiddleware) return decorator(view_func, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/middlewares.py0000644000175100001770000001165200000000000026375 0ustar00runnerdocker00000000000000import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django_downloadview.middlewares import ( NoRedirectionMatch, ProxiedDownloadMiddleware, ) from django_downloadview.nginx.response import XAccelRedirectResponse class XAccelRedirectMiddleware(ProxiedDownloadMiddleware): """Configurable middleware, for use in decorators or in global middlewares. Standard Django middlewares are configured globally via settings. Instances of this class are to be configured individually. It makes it possible to use this class as the factory in :py:class:`django_downloadview.decorators.DownloadDecorator`. """ def __init__( self, get_response=None, source_dir=None, source_url=None, destination_url=None, expires=None, with_buffering=None, limit_rate=None, media_root=None, media_url=None, ): """Constructor.""" if media_url is not None: warnings.warn( "%s ``media_url`` is deprecated. Use " "``destination_url`` instead." % self.__class__.__name__, DeprecationWarning, ) if destination_url is None: destination_url = media_url else: destination_url = destination_url else: destination_url = destination_url if media_root is not None: warnings.warn( "%s ``media_root`` is deprecated. Use " "``source_dir`` instead." % self.__class__.__name__, DeprecationWarning, ) if source_dir is None: source_dir = media_root else: source_dir = source_dir else: source_dir = source_dir super().__init__(get_response, source_dir, source_url, destination_url) self.expires = expires self.with_buffering = with_buffering self.limit_rate = limit_rate def process_download_response(self, request, response): """Replace DownloadResponse instances by NginxDownloadResponse ones.""" try: redirect_url = self.get_redirect_url(response) except NoRedirectionMatch: return response if self.expires: expires = self.expires else: try: expires = response.expires except AttributeError: expires = None return XAccelRedirectResponse( redirect_url=redirect_url, content_type=response["Content-Type"], basename=response.basename, expires=expires, with_buffering=self.with_buffering, limit_rate=self.limit_rate, attachment=response.attachment, headers=response.headers, ) class SingleXAccelRedirectMiddleware(XAccelRedirectMiddleware): """Apply X-Accel-Redirect globally, via Django settings. Available settings are: NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL: The string at the beginning of URLs to replace with ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``. If ``None``, then URLs aren't captured. Defaults to ``settings.MEDIA_URL``. NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR: The string at the beginning of filenames (path) to replace with ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``. If ``None``, then filenames aren't captured. Defaults to ``settings.MEDIA_ROOT``. NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL: The base URL where requests are proxied to. If ``None`` an ImproperlyConfigured exception is raised. .. note:: The following settings are deprecated since version 1.1. URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT" and "MEDIA_URL" became too confuse. NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT: Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``. NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL: Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``. """ def __init__(self, get_response=None): """Use Django settings as configuration.""" if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None: raise ImproperlyConfigured( "settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is " "required by %s middleware" % self.__class__.__name__ ) super().__init__( get_response=get_response, source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR, source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL, destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL, expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES, with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING, limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/response.py0000644000175100001770000000326400000000000025733 0ustar00runnerdocker00000000000000"""Nginx's specific responses.""" from datetime import timedelta from django.utils.timezone import now from django_downloadview.response import ProxiedDownloadResponse, content_disposition from django_downloadview.utils import content_type_to_charset, url_basename class XAccelRedirectResponse(ProxiedDownloadResponse): "Http response that delegates serving file to Nginx via X-Accel headers." def __init__( self, redirect_url, content_type, basename=None, expires=None, with_buffering=None, limit_rate=None, attachment=True, headers=None, ): """Return a HttpResponse with headers for Nginx X-Accel-Redirect.""" # content-type must be porvided only as keyword argument to response if headers and content_type: headers.pop("Content-Type", None) super().__init__(content_type=content_type, headers=headers) if attachment: self.basename = basename or url_basename(redirect_url, content_type) self["Content-Disposition"] = content_disposition(self.basename) self["X-Accel-Redirect"] = redirect_url self["X-Accel-Charset"] = content_type_to_charset(content_type) if with_buffering is not None: self["X-Accel-Buffering"] = with_buffering and "yes" or "no" if expires: expire_seconds = timedelta(expires - now()).seconds self["X-Accel-Expires"] = expire_seconds elif expires is not None: # We explicitely want it off. self["X-Accel-Expires"] = "off" if limit_rate is not None: self["X-Accel-Limit-Rate"] = limit_rate and "%d" % limit_rate or "off" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/settings.py0000644000175100001770000001143500000000000025734 0ustar00runnerdocker00000000000000"""Django settings around Nginx X-Accel. .. warning:: These settings are deprecated since version 1.3. You can now provide custom configuration via `DOWNLOADVIEW_BACKEND` setting. See :doc:`/settings` for details. """ import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured # In version 1.3, former XAccelRedirectMiddleware has been renamed to # SingleXAccelRedirectMiddleware. So tell the users. deprecated_middleware = "django_downloadview.nginx.XAccelRedirectMiddleware" if deprecated_middleware in settings.MIDDLEWARE: raise ImproperlyConfigured( "{deprecated_middleware} middleware has been renamed as of " "django-downloadview version 1.3. You may use " '"django_downloadview.nginx.SingleXAccelRedirectMiddleware" instead, ' 'or upgrade to "django_downloadview.SmartDownloadDispatcher". ' ) deprecated_msg = ( "settings.{deprecated} is deprecated. You should combine " '"django_downloadview.SmartDownloadDispatcher" with ' "with DOWNLOADVIEW_BACKEND and DOWNLOADVIEW_RULES instead." ) #: Default value for X-Accel-Buffering header. #: Also default value for #: ``settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``. #: #: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Buffering #: #: Default value is None, which means "let Nginx choose", i.e. use Nginx #: defaults or specific configuration. #: #: If set to ``False``, Nginx buffering is disabled. #: If set to ``True``, Nginx buffering is enabled. DEFAULT_WITH_BUFFERING = None setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_WITH_BUFFERING) #: Default value for X-Accel-Limit-Rate header. #: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``. #: #: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Rate #: #: Default value is None, which means "let Nginx choose", i.e. use Nginx #: defaults or specific configuration. #: #: If set to ``False``, Nginx limit rate is disabled. #: Else, it indicates the limit rate in bytes. DEFAULT_LIMIT_RATE = None setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_LIMIT_RATE) #: Default value for X-Accel-Limit-Expires header. #: Also default value for ``settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES``. #: #: See http://wiki.nginx.org/X-accel#X-Accel-Limit-Expires #: #: Default value is None, which means "let Nginx choose", i.e. use Nginx #: defaults or specific configuration. #: #: If set to ``False``, Nginx buffering is disabled. #: Else, it indicates the expiration delay, in seconds. DEFAULT_EXPIRES = None setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_EXPIRES) #: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR. DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_SOURCE_DIR) #: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL. DEFAULT_SOURCE_URL = settings.MEDIA_URL setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_SOURCE_URL) #: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL. DEFAULT_DESTINATION_URL = None setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL setting_name = "NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL" if hasattr(settings, setting_name): warnings.warn(deprecated_msg.format(deprecated=setting_name), DeprecationWarning) if not hasattr(settings, setting_name): setattr(settings, setting_name, DEFAULT_DESTINATION_URL) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/nginx/tests.py0000644000175100001770000001105200000000000025231 0ustar00runnerdocker00000000000000from django_downloadview.nginx.response import XAccelRedirectResponse class XAccelRedirectValidator: """Utility class to validate XAccelRedirectResponse instances. See also :py:func:`assert_x_accel_redirect` shortcut function. """ def __call__(self, test_case, response, **assertions): """Assert that ``response`` is a valid X-Accel-Redirect response. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``redirect_url``: the value of "X-Accel-Redirect" header. * ``charset``: the value of ``X-Accel-Charset`` header. * ``with_buffering``: the value of ``X-Accel-Buffering`` header. If ``False``, then makes sure that the header disables buffering. If ``None``, then makes sure that the header is not set. * ``expires``: the value of ``X-Accel-Expires`` header. If ``False``, then makes sure that the header disables expiration. If ``None``, then makes sure that the header is not set. * ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header. If ``False``, then makes sure that the header disables limit rate. If ``None``, then makes sure that the header is not set. """ self.assert_x_accel_redirect_response(test_case, response) for key, value in assertions.items(): assert_func = getattr(self, "assert_%s" % key) assert_func(test_case, response, value) def assert_x_accel_redirect_response(self, test_case, response): test_case.assertTrue(isinstance(response, XAccelRedirectResponse)) def assert_basename(self, test_case, response, value): test_case.assertEqual(response.basename, value) def assert_content_type(self, test_case, response, value): test_case.assertEqual(response["Content-Type"], value) def assert_redirect_url(self, test_case, response, value): test_case.assertEqual(response["X-Accel-Redirect"], value) def assert_charset(self, test_case, response, value): test_case.assertEqual(response["X-Accel-Charset"], value) def assert_with_buffering(self, test_case, response, value): header = "X-Accel-Buffering" if value is None: test_case.assertFalse(header in response) elif value: test_case.assertEqual(header, "yes") else: test_case.assertEqual(header, "no") def assert_expires(self, test_case, response, value): header = "X-Accel-Expires" if value is None: test_case.assertFalse(header in response) elif not value: test_case.assertEqual(header, "off") else: test_case.assertEqual(header, value) def assert_limit_rate(self, test_case, response, value): header = "X-Accel-Limit-Rate" if value is None: test_case.assertFalse(header in response) elif not value: test_case.assertEqual(header, "off") else: test_case.assertEqual(header, value) def assert_attachment(self, test_case, response, value): header = "Content-Disposition" if value: test_case.assertTrue(response[header].startswith("attachment")) else: test_case.assertFalse(header in response) def assert_x_accel_redirect(test_case, response, **assertions): """Make ``test_case`` assert that ``response`` is a XAccelRedirectResponse. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``redirect_url``: the value of "X-Accel-Redirect" header. * ``charset``: the value of ``X-Accel-Charset`` header. * ``with_buffering``: the value of ``X-Accel-Buffering`` header. If ``False``, then makes sure that the header disables buffering. If ``None``, then makes sure that the header is not set. * ``expires``: the value of ``X-Accel-Expires`` header. If ``False``, then makes sure that the header disables expiration. If ``None``, then makes sure that the header is not set. * ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header. If ``False``, then makes sure that the header disables limit rate. If ``None``, then makes sure that the header is not set. """ validator = XAccelRedirectValidator() return validator(test_case, response, **assertions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/response.py0000644000175100001770000002034600000000000024610 0ustar00runnerdocker00000000000000""":py:class:`django.http.HttpResponse` subclasses.""" import mimetypes import os import re import unicodedata from urllib.parse import quote from django.conf import settings from django.http import HttpResponse, StreamingHttpResponse from django.utils.encoding import force_str def encode_basename_ascii(value): """Return US-ASCII encoded ``value`` for Content-Disposition header. >>> print(encode_basename_ascii(u'éà')) ea Spaces are converted to underscores. >>> print(encode_basename_ascii(' ')) _ Of course, ASCII values are not modified. >>> print(encode_basename_ascii('ea')) ea >>> print(encode_basename_ascii(b'ea')) ea """ if isinstance(value, bytes): value = value.decode("utf-8") ascii_basename = str(value) ascii_basename = unicodedata.normalize("NFKD", ascii_basename) ascii_basename = ascii_basename.encode("ascii", "ignore") ascii_basename = ascii_basename.decode("ascii") ascii_basename = re.sub(r"[\s]", "_", ascii_basename) return ascii_basename def encode_basename_utf8(value): """Return UTF-8 encoded ``value`` for use in Content-Disposition header. >>> print(encode_basename_utf8(u' .txt')) %20.txt >>> print(encode_basename_utf8(u'éà')) %C3%A9%C3%A0 """ return quote(force_str(value)) def content_disposition(filename): """Return value of ``Content-Disposition`` header with 'attachment'. >>> print(content_disposition('demo.txt')) attachment; filename="demo.txt" If filename is empty, only "attachment" is returned. >>> print(content_disposition('')) attachment If filename contains non US-ASCII characters, the returned value contains UTF-8 encoded filename and US-ASCII fallback. >>> print(content_disposition(u'é.txt')) attachment; filename="e.txt"; filename*=UTF-8''%C3%A9.txt """ if not filename: return "attachment" # ASCII filenames are quoted and must ensure escape sequences # in the filename won't break out of the quoted header value # which can permit a reflected file download attack. The UTF-8 # version is immune because it's not quoted. ascii_filename = ( encode_basename_ascii(filename).replace("\\", "\\\\").replace('"', r"\"") ) utf8_filename = encode_basename_utf8(filename) if ascii_filename == utf8_filename: # ASCII only. return f'attachment; filename="{ascii_filename}"' else: return ( f'attachment; filename="{ascii_filename}"; ' f"filename*=UTF-8''{utf8_filename}" ) class DownloadResponse(StreamingHttpResponse): """File download response (Django serves file, client downloads it). This is a specialization of :class:`django.http.StreamingHttpResponse` where :attr:`~django.http.StreamingHttpResponse.streaming_content` is a file wrapper. Constructor differs a bit from :class:`~django.http.response.HttpResponse`. Here are some highlights to understand internal mechanisms and motivations: * Let's start by quoting :pep:`3333` (WSGI specification): For large files, or for specialized uses of HTTP streaming, applications will usually return an iterator (often a generator-iterator) that produces the output in a block-by-block fashion. * Django WSGI handler (application implementation) returns response object (see :mod:`django.core.handlers.wsgi`). * :class:`django.http.HttpResponse` and subclasses are iterators. * In :class:`~django.http.StreamingHttpResponse`, the :meth:`~container.__iter__` implementation proxies to :attr:`~django.http.StreamingHttpResponse.streaming_content`. * In :class:`DownloadResponse` and subclasses, :attr:`streaming_content` is a :doc:`file wrapper `. File wrapper is itself an iterator over actual file content, and it also encapsulates access to file attributes (size, name, ...). """ def __init__( self, file_instance, attachment=True, basename=None, status=200, content_type=None, file_mimetype=None, file_encoding=None, ): """Constructor. :param content_type: Value for ``Content-Type`` header. If ``None``, then mime-type and encoding will be populated by the response (default implementation uses :mod:`mimetypes`, based on file name). """ #: A :doc:`file wrapper instance `, such as #: :class:`~django.core.files.base.File`. self.file = file_instance super().__init__( streaming_content=self.file, status=status, content_type=content_type ) #: Client-side name of the file to stream. #: Only used if ``attachment`` is ``True``. #: Affects ``Content-Disposition`` header. self.basename = basename #: Whether to return the file as attachment or not. #: Affects ``Content-Disposition`` header. self.attachment = attachment if not content_type: del self["Content-Type"] # Will be set later. #: Value for file's mimetype. #: If ``None`` (the default), then the file's mimetype will be guessed #: via Python's :mod:`mimetypes`. See :meth:`get_mime_type`. self.file_mimetype = file_mimetype #: Value for file's encoding. If ``None`` (the default), then the #: file's encoding will be guessed via Python's :mod:`mimetypes`. See #: :meth:`get_encoding`. self.file_encoding = file_encoding # Apply default headers. for header, value in self.default_headers.items(): if header not in self: self[header] = value # Does self support setdefault? @property def default_headers(self): """Return dictionary of automatically-computed headers. Uses an internal ``_default_headers`` cache. Default values are computed if only cache hasn't been set. ``Content-Disposition`` header is encoded according to `RFC 5987 `_. See also http://stackoverflow.com/questions/93551/. """ try: return self._default_headers except AttributeError: headers = {} headers["Content-Type"] = self.get_content_type() try: headers["Content-Length"] = self.file.size except (AttributeError, NotImplementedError): pass # Generated files. if self.attachment: basename = self.get_basename() headers["Content-Disposition"] = content_disposition(basename) self._default_headers = headers return self._default_headers def get_basename(self): """Return basename.""" if self.basename: return self.basename else: return os.path.basename(self.file.name) def get_content_type(self): """Return a suitable "Content-Type" header for ``self.file``.""" try: return self.file.content_type except AttributeError: return f"{self.get_mime_type()}; charset={self.get_charset()}" def get_mime_type(self): """Return mime-type of the file.""" if self.file_mimetype is not None: return self.file_mimetype default_mime_type = "application/octet-stream" basename = self.get_basename() mime_type, encoding = mimetypes.guess_type(basename) return mime_type or default_mime_type def get_encoding(self): """Return encoding of the file to serve.""" if self.file_encoding is not None: return self.file_encoding basename = self.get_basename() mime_type, encoding = mimetypes.guess_type(basename) return encoding def get_charset(self): """Return the charset of the file to serve.""" return settings.DEFAULT_CHARSET class ProxiedDownloadResponse(HttpResponse): """Base class for internal redirect download responses. This base class makes it possible to identify several types of specific responses such as :py:class:`~django_downloadview.nginx.response.XAccelRedirectResponse`. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/shortcuts.py0000644000175100001770000000121500000000000025002 0ustar00runnerdocker00000000000000"""Port of django-sendfile in django-downloadview.""" from django_downloadview.views.path import PathDownloadView def sendfile( request, filename, attachment=False, attachment_filename=None, mimetype=None, encoding=None, ): """Port of django-sendfile's API in django-downloadview. Instantiates a :class:`~django_downloadview.views.path.PathDownloadView` to stream the file by ``filename``. """ view = PathDownloadView.as_view( path=filename, attachment=attachment, basename=attachment_filename, mimetype=mimetype, encoding=encoding, ) return view(request) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/storage.py0000644000175100001770000000121600000000000024411 0ustar00runnerdocker00000000000000from django.core.files.storage import FileSystemStorage from django.core.signing import TimestampSigner class SignedURLMixin: """ Mixin for generating signed file URLs with compatible storage backends. Adds X-Signature query parameters to the normal URLs generated by the storage class. """ def url(self, name): path = super().url(name) signer = TimestampSigner() signature = signer.sign(path) return "{}?X-Signature={}".format(path, signature) class SignedFileSystemStorage(SignedURLMixin, FileSystemStorage): """ Specialized filesystem storage that signs file URLs for clients. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/test.py0000644000175100001770000001347100000000000023732 0ustar00runnerdocker00000000000000import shutil import tempfile from django.conf import settings from django.test.utils import override_settings from django.utils.encoding import force_bytes from django_downloadview.middlewares import is_download_response from django_downloadview.response import encode_basename_ascii, encode_basename_utf8 def setup_view(view, request, *args, **kwargs): """Mimic ``as_view()``, but returns view instance. Use this function to get view instances on which you can run unit tests, by testing specific methods. This is an early implementation of https://code.djangoproject.com/ticket/20456 ``view`` A view instance, such as ``TemplateView(template_name='dummy.html')``. Initialization arguments are the same you would pass to ``as_view()``. ``request`` A request object, typically built with :class:`~django.test.client.RequestFactory`. ``args`` and ``kwargs`` "URLconf" positional and keyword arguments, the same you would pass to :func:`~django.core.urlresolvers.reverse`. """ view.request = request view.args = args view.kwargs = kwargs return view class temporary_media_root(override_settings): """Temporarily override settings.MEDIA_ROOT with a temporary directory. The temporary directory is automatically created and destroyed. Use this function as a context manager: >>> from django_downloadview.test import temporary_media_root >>> from django.conf import settings # NoQA >>> global_media_root = settings.MEDIA_ROOT >>> with temporary_media_root(): ... global_media_root == settings.MEDIA_ROOT False >>> global_media_root == settings.MEDIA_ROOT True Or as a decorator: >>> @temporary_media_root() ... def use_temporary_media_root(): ... return settings.MEDIA_ROOT >>> tmp_media_root = use_temporary_media_root() >>> global_media_root == tmp_media_root False >>> global_media_root == settings.MEDIA_ROOT True """ def enable(self): """Create a temporary directory and use it to override settings.MEDIA_ROOT.""" tmp_dir = tempfile.mkdtemp() self.options["MEDIA_ROOT"] = tmp_dir super().enable() def disable(self): """Remove directory settings.MEDIA_ROOT then restore original setting.""" shutil.rmtree(settings.MEDIA_ROOT) super().disable() class DownloadResponseValidator(object): """Utility class to validate DownloadResponse instances.""" def __call__(self, test_case, response, **assertions): """Assert that ``response`` is a valid DownloadResponse instance. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``mime_type``: the MIME type part of "Content-Type" header (without charset). * ``content``: the contents of the file. * ``attachment``: whether the file is returned as attachment or not. """ self.assert_download_response(test_case, response) for key, value in assertions.items(): assert_func = getattr(self, "assert_%s" % key) assert_func(test_case, response, value) def assert_download_response(self, test_case, response): test_case.assertTrue(is_download_response(response)) def assert_basename(self, test_case, response, value): """Implies ``attachement is True``.""" ascii_name = encode_basename_ascii(value) utf8_name = encode_basename_utf8(value) check_utf8 = False check_ascii = False if ascii_name == utf8_name: # Only ASCII characters. check_ascii = True if "filename*=" in response["Content-Disposition"]: check_utf8 = True else: check_utf8 = True if "filename=" in response["Content-Disposition"]: check_ascii = True if check_ascii: test_case.assertIn( f'filename="{ascii_name}"', response["Content-Disposition"], ) if check_utf8: test_case.assertIn( f"filename*=UTF-8''{utf8_name}", response["Content-Disposition"], ) def assert_content_type(self, test_case, response, value): test_case.assertEqual(response["Content-Type"], value) def assert_mime_type(self, test_case, response, value): test_case.assertTrue(response["Content-Type"].startswith(value)) def assert_content(self, test_case, response, value): """Assert value equals response's content (byte comparison).""" parts = [force_bytes(s) for s in response.streaming_content] test_case.assertEqual(b"".join(parts), force_bytes(value)) def assert_attachment(self, test_case, response, value): if value: test_case.assertTrue("attachment;" in response["Content-Disposition"]) else: test_case.assertTrue( "Content-Disposition" not in response or "attachment;" not in response["Content-Disposition"] ) def assert_download_response(test_case, response, **assertions): """Make ``test_case`` assert that ``response`` meets ``assertions``. Optional ``assertions`` dictionary can be used to check additional items: * ``basename``: the basename of the file in the response. * ``content_type``: the value of "Content-Type" header. * ``mime_type``: the MIME type part of "Content-Type" header (without charset). * ``content``: the contents of the file. * ``attachment``: whether the file is returned as attachment or not. """ validator = DownloadResponseValidator() return validator(test_case, response, **assertions) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/utils.py0000644000175100001770000000240000000000000024101 0ustar00runnerdocker00000000000000"""Utility functions that may be implemented in external packages.""" import re charset_pattern = re.compile(r"charset=(?P.+)$", re.I | re.U) def content_type_to_charset(content_type): """Return charset part of content-type header. >>> from django_downloadview.utils import content_type_to_charset >>> content_type_to_charset('text/html; charset=utf-8') 'utf-8' """ match = re.search(charset_pattern, content_type) if match: return match.group("charset") def url_basename(url, content_type): """Return best-guess basename from URL and content-type. >>> from django_downloadview.utils import url_basename If URL contains extension, it is kept as-is. >>> print(url_basename(u'/path/to/somefile.rst', 'text/plain')) somefile.rst """ return url.split("/")[-1] def import_member(import_string): """Import one member of Python module by path. >>> import os.path >>> imported = import_member('os.path.supports_unicode_filenames') >>> os.path.supports_unicode_filenames is imported True """ module_name, factory_name = str(import_string).rsplit(".", 1) module = __import__(module_name, globals(), locals(), [factory_name], 0) return getattr(module, factory_name) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8547323 django_downloadview-2.4.0/django_downloadview/views/0000755000175100001770000000000000000000000023530 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/__init__.py0000644000175100001770000000074600000000000025650 0ustar00runnerdocker00000000000000"""Views to stream files.""" # API shortcuts. from django_downloadview.views.base import BaseDownloadView, DownloadMixin # NoQA from django_downloadview.views.http import HTTPDownloadView # NoQA from django_downloadview.views.object import ObjectDownloadView # NoQA from django_downloadview.views.path import PathDownloadView # NoQA from django_downloadview.views.storage import StorageDownloadView # NoQA from django_downloadview.views.virtual import VirtualDownloadView # NoQA ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/base.py0000644000175100001770000001373000000000000025020 0ustar00runnerdocker00000000000000"""Base material for download views: :class:`DownloadMixin` and :class:`BaseDownloadView`""" import calendar from django.http import Http404, HttpResponseNotModified from django.views.generic.base import View from django.views.static import was_modified_since from django_downloadview import exceptions from django_downloadview.response import DownloadResponse class DownloadMixin(object): """Placeholders and base implementation to create file download views. .. note:: This class does not inherit from :py:class:`django.views.generic.base.View`. The :py:meth:`get_file` method is a placeholder subclasses must implement. Base implementation raises ``NotImplementedError``. Other methods provide a base implementation that use the file wrapper returned by :py:meth:`get_file`. """ #: Response class, to be used in :py:meth:`render_to_response`. response_class = DownloadResponse #: Whether to return the response as attachment or not. #: #: When ``True`` (the default), the view returns file "as attachment", #: which usually triggers a "Save the file as ..." prompt. #: #: When ``False``, the view returns file "inline", as if it was an element #: of the current page. #: #: .. note:: #: #: The actual behaviour client-side depends on the browser and its #: configuration. #: #: In fact, affects the "Content-Disposition" header via :attr:`response's #: attachment attribute #: `. attachment = True #: Client-side filename, if only file is returned as attachment. basename = None #: File's mime type. #: If ``None`` (the default), then the file's mime type will be guessed via #: :mod:`mimetypes`. mimetype = None #: File's encoding. #: If ``None`` (the default), then the file's encoding will be guessed via #: :mod:`mimetypes`. encoding = None def get_file(self): """Return a file wrapper instance. Raises :class:`~django_downloadview.exceptions.FileNotFound` if file does not exist. """ raise NotImplementedError() def get_basename(self): """Return :attr:`basename`. Override this method if you need more dynamic basename. """ return self.basename def get_mimetype(self): """Return :attr:`mimetype`. Override this method if you need more dynamic mime type. """ return self.mimetype def get_encoding(self): """Return :attr:`encoding`. Override this method if you need more dynamic encoding. """ return self.encoding def was_modified_since(self, file_instance, since): """Return True if ``file_instance`` was modified after ``since``. Uses file wrapper's ``was_modified_since`` if available, with value of ``since`` as positional argument. Else, fallbacks to default implementation, which uses :py:func:`django.views.static.was_modified_since`. Django's ``was_modified_since`` function needs a datetime. It is passed the ``modified_time`` attribute from file wrapper. If file wrapper does not support this attribute (``AttributeError`` or ``NotImplementedError`` is raised), then the file is considered as modified and ``True`` is returned. """ try: return file_instance.was_modified_since(since) except (AttributeError, NotImplementedError): try: modification_time = calendar.timegm( file_instance.modified_time.utctimetuple() ) except (AttributeError, NotImplementedError) as e: print("!=======!", e) return True else: return was_modified_since(since, modification_time) def not_modified_response(self, *response_args, **response_kwargs): """Return :class:`django.http.HttpResponseNotModified` instance.""" return HttpResponseNotModified(*response_args, **response_kwargs) def download_response(self, *response_args, **response_kwargs): """Return :class:`~django_downloadview.response.DownloadResponse`.""" response_kwargs.setdefault("file_instance", self.file_instance) response_kwargs.setdefault("attachment", self.attachment) response_kwargs.setdefault("basename", self.get_basename()) response_kwargs.setdefault("file_mimetype", self.get_mimetype()) response_kwargs.setdefault("file_encoding", self.get_encoding()) response = self.response_class(*response_args, **response_kwargs) return response def file_not_found_response(self): """Raise Http404.""" raise Http404() def render_to_response(self, *response_args, **response_kwargs): """Return "download" response (if everything is ok). Return :meth:`file_not_found_response` if file does not exist. Respects the "HTTP_IF_MODIFIED_SINCE" header if any. In that case, uses :py:meth:`was_modified_since` and :py:meth:`not_modified_response`. Else, uses :py:meth:`download_response` to return a download response. """ try: self.file_instance = self.get_file() except exceptions.FileNotFound: return self.file_not_found_response() # Respect the If-Modified-Since header. since = self.request.headers.get("if-modified-since", None) if since is not None: if not self.was_modified_since(self.file_instance, since): return self.not_modified_response(**response_kwargs) # Return download response. return self.download_response(*response_args, **response_kwargs) class BaseDownloadView(DownloadMixin, View): """A base :class:`DownloadMixin` that implements :meth:`get`.""" def get(self, request, *args, **kwargs): """Handle GET requests: stream a file.""" return self.render_to_response() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/http.py0000644000175100001770000000246300000000000025066 0ustar00runnerdocker00000000000000"""Stream files given an URL, i.e. files you want to proxy.""" from django_downloadview.files import HTTPFile from django_downloadview.views.base import BaseDownloadView import requests class HTTPDownloadView(BaseDownloadView): """Proxy files that live on remote servers.""" #: URL to download (the one we are proxying). url = "" #: Additional keyword arguments for request handler. request_kwargs = {} def get_request_factory(self): """Return request factory to perform actual HTTP request. Default implementation returns :func:`requests.get` callable. """ return requests.get def get_request_kwargs(self): """Return keyword arguments for use with :meth:`get_request_factory`. Default implementation returns :attr:`request_kwargs`. """ return self.request_kwargs def get_url(self): """Return remote file URL (the one we are proxying). Default implementation returns :attr:`url`. """ return self.url def get_file(self): """Return wrapper which has an ``url`` attribute.""" return HTTPFile( request_factory=self.get_request_factory(), name=self.get_basename(), url=self.get_url(), **self.get_request_kwargs(), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/object.py0000644000175100001770000000704100000000000025352 0ustar00runnerdocker00000000000000"""Stream files that live in models.""" from django.views.generic.detail import SingleObjectMixin from django_downloadview.exceptions import FileNotFound from django_downloadview.views.base import BaseDownloadView class ObjectDownloadView(SingleObjectMixin, BaseDownloadView): """Serve file fields from models. This class extends :class:`~django.views.generic.detail.SingleObjectMixin`, so you can use its arguments to target the instance to operate on: ``slug``, ``slug_kwarg``, ``model``, ``queryset``... In addition to :class:`~django.views.generic.detail.SingleObjectMixin` arguments, you can set arguments related to the file to be downloaded: * :attr:`file_field`; * :attr:`basename_field`; * :attr:`encoding_field`; * :attr:`mime_type_field`; * :attr:`charset_field`; * :attr:`modification_time_field`; * :attr:`size_field`. :attr:`file_field` is the main one. Other arguments are provided for convenience, in case your model holds some (deserialized) metadata about the file, such as its basename, its modification time, its MIME type... These fields may be particularly handy if your file storage is not the local filesystem. """ #: Name of the model's attribute which contains the file to be streamed. #: Typically the name of a FileField. file_field = "file" #: Optional name of the model's attribute which contains the basename. basename_field = None #: Optional name of the model's attribute which contains the encoding. encoding_field = None #: Optional name of the model's attribute which contains the MIME type. mime_type_field = None #: Optional name of the model's attribute which contains the charset. charset_field = None #: Optional name of the model's attribute which contains the modification # time. modification_time_field = None #: Optional name of the model's attribute which contains the size. size_field = None def get_file(self): """Return :class:`~django.db.models.fields.files.FieldFile` instance. The file wrapper is model's field specified as :attr:`file_field`. It is typically a :class:`~django.db.models.fields.files.FieldFile` or subclass. Raises :class:`~django_downloadview.exceptions.FileNotFound` if instance's field is empty. Additional attributes are set on the file wrapper if :attr:`encoding`, :attr:`mime_type`, :attr:`charset`, :attr:`modification_time` or :attr:`size` are configured. """ file_instance = getattr(self.object, self.file_field) if not file_instance: raise FileNotFound( f'Field="{self.file_field}" on object="{self.object}" is empty' ) for field in ("encoding", "mime_type", "charset", "modification_time", "size"): model_field = getattr(self, "%s_field" % field, False) if model_field: value = getattr(self.object, model_field) setattr(file_instance, field, value) return file_instance def get_basename(self): """Return client-side filename.""" basename = super().get_basename() if basename is None: field = "basename" model_field = getattr(self, "%s_field" % field, False) if model_field: basename = getattr(self.object, model_field) return basename def get(self, request, *args, **kwargs): self.object = self.get_object() return super().get(request, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/path.py0000644000175100001770000000234400000000000025041 0ustar00runnerdocker00000000000000""":class:`PathDownloadView`.""" import os from django.core.files import File from django_downloadview.exceptions import FileNotFound from django_downloadview.views.base import BaseDownloadView class PathDownloadView(BaseDownloadView): """Serve a file using filename.""" #: Server-side name (including path) of the file to serve. #: #: Filename is supposed to be an absolute filename of a file located on the #: local filesystem. path = None #: Name of the URL argument that contains path. path_url_kwarg = "path" def get_path(self): """Return actual path of the file to serve. Default implementation simply returns view's :py:attr:`path`. Override this method if you want custom implementation. As an example, :py:attr:`path` could be relative and your custom :py:meth:`get_path` implementation makes it absolute. """ return self.kwargs.get(self.path_url_kwarg, self.path) def get_file(self): """Use path to return wrapper around file to serve.""" filename = self.get_path() if not os.path.isfile(filename): raise FileNotFound(f'File "{filename}" does not exists') return File(open(filename, "rb")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/storage.py0000644000175100001770000000116500000000000025551 0ustar00runnerdocker00000000000000"""Stream files from storage.""" from django.core.files.storage import DefaultStorage from django_downloadview.files import StorageFile from django_downloadview.views.path import PathDownloadView class StorageDownloadView(PathDownloadView): """Serve a file using storage and filename.""" #: Storage the file to serve belongs to. storage = DefaultStorage() #: Path to the file to serve relative to storage. path = None # Override docstring. def get_file(self): """Return :class:`~django_downloadview.files.StorageFile` instance.""" return StorageFile(self.storage, self.get_path()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/django_downloadview/views/virtual.py0000644000175100001770000000240600000000000025572 0ustar00runnerdocker00000000000000"""Stream files that you generate or that live in memory.""" from django_downloadview.views.base import BaseDownloadView class VirtualDownloadView(BaseDownloadView): """Serve not-on-disk or generated-on-the-fly file. Override the :py:meth:`get_file` method to customize file wrapper. """ def was_modified_since(self, file_instance, since): """Delegate to file wrapper's was_modified_since, or return True. This is the implementation of an edge case: when files are generated on the fly, we cannot guess whether they have been modified or not. If the file wrapper implements ``was_modified_since()`` method, then we trust it. Otherwise it is safer to suppose that the file has been modified. This behaviour prevents file size to be computed on the Django side. Because computing file size means iterating over all the file contents, and we want to avoid that whenever possible. As an example, it could reduce all the benefits of working with dynamic file generators... which is a major feature of virtual files. """ try: return file_instance.was_modified_since(since) except (AttributeError, NotImplementedError): return True ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/django_downloadview.egg-info/0000755000175100001770000000000000000000000024065 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/PKG-INFO0000644000175100001770000000707700000000000025175 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: django-downloadview Version: 2.4.0 Summary: Serve files with Django and reverse-proxies. Home-page: https://django-downloadview.readthedocs.io/ Author: Benoît Bryon Author-email: benoit@marmelune.net License: BSD Keywords: file stream download FileField ImageField x-accel x-accel-redirect x-sendfile sendfile mod_xsendfile offload Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Framework :: Django Classifier: Framework :: Django :: 4.2 Classifier: Framework :: Django :: 5.0 Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE License-File: AUTHORS Requires-Dist: Django>=4.2 Requires-Dist: requests Provides-Extra: test Requires-Dist: tox; extra == "test" ################### django-downloadview ################### .. 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, ...); * files are stored somewhere or generated somehow (local filesystem, remote storage, memory...); * ``django-downloadview`` helps you stream the files with very little code; * ``django-downloadview`` helps you improve performances with reverse proxies, via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile. ******* Example ******* Let's serve a file stored in a file field of some model: .. code:: python from django.conf.urls import url, url_patterns from django_downloadview import ObjectDownloadView from demoproject.download.models import Document # A model with a FileField # ObjectDownloadView inherits from django.views.generic.BaseDetailView. download = ObjectDownloadView.as_view(model=Document, file_field='file') url_patterns = ('', url('^download/(?P[A-Za-z0-9_-]+)/$', download, name='download'), ) ********* Resources ********* * Documentation: https://django-downloadview.readthedocs.io * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/jazzband/django-downloadview * Bugtracker: https://github.com/jazzband/django-downloadview/issues * Continuous integration: https://github.com/jazzband/django-downloadview/actions * Roadmap: https://github.com/jazzband/django-downloadview/milestones .. _`Django`: https://djangoproject.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/SOURCES.txt0000644000175100001770000001015400000000000025752 0ustar00runnerdocker00000000000000.gitignore .isort.cfg .pre-commit-config.yaml .readthedocs.yaml AUTHORS CHANGELOG CODE_OF_CONDUCT.md CONTRIBUTING.rst INSTALL LICENSE MANIFEST.in Makefile README.rst setup.py tox.ini .github/workflows/release.yml .github/workflows/test.yml demo/README.rst demo/setup.py demo/demoproject/__init__.py demo/demoproject/manage.py demo/demoproject/settings.py demo/demoproject/tests.py demo/demoproject/urls.py demo/demoproject/wsgi.py demo/demoproject/apache/__init__.py demo/demoproject/apache/models.py demo/demoproject/apache/tests.py demo/demoproject/apache/urls.py demo/demoproject/apache/views.py demo/demoproject/fixtures/demo.json demo/demoproject/fixtures/hello-world.txt demo/demoproject/http/__init__.py demo/demoproject/http/models.py demo/demoproject/http/tests.py demo/demoproject/http/urls.py demo/demoproject/http/views.py demo/demoproject/lighttpd/__init__.py demo/demoproject/lighttpd/models.py demo/demoproject/lighttpd/tests.py demo/demoproject/lighttpd/urls.py demo/demoproject/lighttpd/views.py demo/demoproject/nginx/__init__.py demo/demoproject/nginx/models.py demo/demoproject/nginx/tests.py demo/demoproject/nginx/urls.py demo/demoproject/nginx/views.py demo/demoproject/object/__init__.py demo/demoproject/object/models.py demo/demoproject/object/tests.py demo/demoproject/object/urls.py demo/demoproject/object/views.py demo/demoproject/path/__init__.py demo/demoproject/path/models.py demo/demoproject/path/tests.py demo/demoproject/path/urls.py demo/demoproject/path/views.py demo/demoproject/storage/__init__.py demo/demoproject/storage/models.py demo/demoproject/storage/storage.py demo/demoproject/storage/tests.py demo/demoproject/storage/urls.py demo/demoproject/storage/views.py demo/demoproject/templates/home.html demo/demoproject/virtual/__init__.py demo/demoproject/virtual/models.py demo/demoproject/virtual/tests.py demo/demoproject/virtual/urls.py demo/demoproject/virtual/views.py django_downloadview/__init__.py django_downloadview/api.py django_downloadview/decorators.py django_downloadview/exceptions.py django_downloadview/files.py django_downloadview/io.py django_downloadview/middlewares.py django_downloadview/response.py django_downloadview/shortcuts.py django_downloadview/storage.py django_downloadview/test.py django_downloadview/utils.py django_downloadview.egg-info/PKG-INFO django_downloadview.egg-info/SOURCES.txt django_downloadview.egg-info/dependency_links.txt django_downloadview.egg-info/not-zip-safe django_downloadview.egg-info/requires.txt django_downloadview.egg-info/top_level.txt django_downloadview/apache/__init__.py django_downloadview/apache/decorators.py django_downloadview/apache/middlewares.py django_downloadview/apache/response.py django_downloadview/apache/tests.py django_downloadview/lighttpd/__init__.py django_downloadview/lighttpd/decorators.py django_downloadview/lighttpd/middlewares.py django_downloadview/lighttpd/response.py django_downloadview/lighttpd/tests.py django_downloadview/nginx/__init__.py django_downloadview/nginx/decorators.py django_downloadview/nginx/middlewares.py django_downloadview/nginx/response.py django_downloadview/nginx/settings.py django_downloadview/nginx/tests.py django_downloadview/views/__init__.py django_downloadview/views/base.py django_downloadview/views/http.py django_downloadview/views/object.py django_downloadview/views/path.py django_downloadview/views/storage.py django_downloadview/views/virtual.py docs/Makefile docs/conf.py docs/contributing.txt docs/demo.txt docs/django-sendfile.txt docs/files.txt docs/healthchecks.txt docs/index.txt docs/install.txt docs/overview.txt docs/responses.txt docs/settings.txt docs/testing.txt docs/about/alternatives.txt docs/about/authors.txt docs/about/changelog.txt docs/about/index.txt docs/about/license.txt docs/about/vision.txt docs/optimizations/apache.txt docs/optimizations/index.txt docs/optimizations/lighttpd.txt docs/optimizations/nginx.txt docs/views/custom.txt docs/views/http.txt docs/views/index.txt docs/views/object.txt docs/views/path.txt docs/views/storage.txt docs/views/virtual.txt tests/__init__.py tests/api.py tests/io.py tests/packaging.py tests/response.py tests/sendfile.py tests/signature.py tests/views.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/dependency_links.txt0000644000175100001770000000000100000000000030133 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/not-zip-safe0000644000175100001770000000000100000000000026313 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/requires.txt0000644000175100001770000000004100000000000026460 0ustar00runnerdocker00000000000000Django>=4.2 requests [test] tox ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862276.0 django_downloadview-2.4.0/django_downloadview.egg-info/top_level.txt0000644000175100001770000000002400000000000026613 0ustar00runnerdocker00000000000000django_downloadview ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8587322 django_downloadview-2.4.0/docs/0000755000175100001770000000000000000000000017277 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/Makefile0000644000175100001770000001276500000000000020752 0ustar00runnerdocker00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../var/docs # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-downloadview.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-downloadview.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-downloadview" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-downloadview" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8587322 django_downloadview-2.4.0/docs/about/0000755000175100001770000000000000000000000020411 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/alternatives.txt0000644000175100001770000000524300000000000023657 0ustar00runnerdocker00000000000000################################# Alternatives and related projects ################################# This document presents other projects that provide similar or complementary functionalities. It focuses on differences with django-downloadview. There is a comparison grid on djangopackages.com: https://www.djangopackages.com/grids/g/file-streaming/. Here are additional highlights... ************************* Django's static file view ************************* `django.contrib.staticfiles provides a view to serve files`_. It is simple and quite naive by design: it is meant for development, not for production. See `Django ticket #2131`_: advanced file streaming is left to third-party applications. `django-downloadview` is such a third-party application. *************** django-sendfile *************** `django-sendfile`_ is a wrapper around web-server specific methods for sending files to web clients. .. note:: :func:`django_downloadview.shortcuts.sendfile` is a port of `django-sendfile`'s main function. See :doc:`/django-sendfile` for details. ``django-senfile``'s main focus is simplicity: API is made of a single ``sendfile()`` function you call inside your views: .. code:: python from sendfile import sendfile def hello_world(request): """Send 'hello-world.pdf' file as a response.""" return sendfile(request, '/path/to/hello-world.pdf') The download response type depends on the chosen backend, which could be Django, Lighttpd's X-Sendfile, Nginx's X-Accel... depending your settings: .. code:: python SENDFILE_BACKEND = 'sendfile.backends.nginx' # sendfile() will return # X-Accel responses. # Additional settings for sendfile's nginx backend. SENDFILE_ROOT = '/path/to' SENDFILE_URL = '/proxied-download' Here are main differences between the two projects: * ``django-sendfile`` supports only files that live on local filesystem (i.e. where ``os.path.exists`` returns ``True``). Whereas ``django-downloadview`` allows you to serve or proxy files stored in various locations, including remote ones. * ``django-sendfile`` uses a single global configuration (i.e. ``settings.SENDFILE_ROOT``), thus optimizations are limited to a single root folder. Whereas ``django-downloadview``'s ``DownloadDispatcherMiddleware`` supports multiple configurations. .. rubric:: References .. target-notes:: .. _`django.contrib.staticfiles provides a view to serve files`: https://docs.djangoproject.com/en/3.0/ref/contrib/staticfiles/#static-file-development-view .. _`Django ticket #2131`: https://code.djangoproject.com/ticket/2131 .. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/authors.txt0000644000175100001770000000003300000000000022633 0ustar00runnerdocker00000000000000.. include:: ../../AUTHORS ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/changelog.txt0000644000175100001770000000003500000000000023077 0ustar00runnerdocker00000000000000.. include:: ../../CHANGELOG ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/index.txt0000644000175100001770000000023200000000000022256 0ustar00runnerdocker00000000000000######################### About django-downloadview ######################### .. toctree:: vision alternatives license authors changelog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/license.txt0000644000175100001770000000003300000000000022570 0ustar00runnerdocker00000000000000.. include:: ../../LICENSE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/about/vision.txt0000644000175100001770000000122600000000000022462 0ustar00runnerdocker00000000000000###### Vision ###### `django-downloadview` tries to simplify the development of "download" views using `Django`_ framework. It provides generic views that cover most common patterns. Django is not the best solution to serve files: reverse proxies are far more efficient. `django-downloadview` makes it easy to implement this best-practice. Tests matter: `django-downloadview` provides tools to test download views and optimizations. .. rubric:: Notes & references .. seealso:: * :doc:`/about/alternatives` * `roadmap `_ .. target-notes:: .. _`Django`: https://www.djangoproject.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/conf.py0000644000175100001770000001014100000000000020573 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """django-downloadview documentation build configuration file.""" import re import importlib.metadata # Minimal Django settings. Required to use sphinx.ext.autodoc, because # django-downloadview depends on Django... from django.conf import settings settings.configure( DATABASES={}, # Required to load ``django.views.generic``. ) # -- General configuration ---------------------------------------------------- # Extensions. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.doctest", "sphinx.ext.coverage", "sphinx.ext.intersphinx", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".txt" # The encoding of source files. source_encoding = "utf-8" # The master toctree document. master_doc = "index" # General information about the project. project = "django-downloadview" project_slug = re.sub(r"([\w_.-]+)", "-", project) copyright = "2012-2015, Benoît Bryon" author = "Benoît Bryon" author_slug = re.sub(r"([\w_.-]+)", "-", author) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The full version, including alpha/beta/rc tags. release = importlib.metadata.version("django-downloadview") # The short X.Y version. version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "alabaster" # 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, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Custom sidebar templates, maps document names to template names. html_sidebars = { "**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"], } # Output file base name for HTML help builder. htmlhelp_basename = "{project}doc".format(project=project_slug) # -- Options for sphinx.ext.intersphinx --------------------------------------- intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "django": ( "https://docs.djangoproject.com/en/3.1/", "https://docs.djangoproject.com/en/3.1/_objects/", ), "requests": ("https://requests.readthedocs.io/en/master/", None), } # -- Options for LaTeX output ------------------------------------------------- latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( "index", "{project}.tex".format(project=project_slug), "{project} Documentation".format(project=project), author, "manual", ), ] # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ("index", project, "{project} Documentation".format(project=project), [author], 1) ] # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", project_slug, "{project} Documentation".format(project=project), author, project, "One line description of project.", "Miscellaneous", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/contributing.txt0000644000175100001770000000004100000000000022542 0ustar00runnerdocker00000000000000.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/demo.txt0000644000175100001770000000004000000000000020756 0ustar00runnerdocker00000000000000.. include:: ../demo/README.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/django-sendfile.txt0000644000175100001770000000361700000000000023100 0ustar00runnerdocker00000000000000############################## Migrating from django-sendfile ############################## `django-sendfile`_ is a wrapper around web-server specific methods for sending files to web clients. See :doc:`/about/alternatives` for details about this project. `django-downloadview` provides a :func:`port of django-sendfile's main function `. .. warning:: `django-downloadview` can replace the following `django-sendfile`'s backends: ``nginx``, ``xsendfile``, ``simple``. But it currently cannot replace ``mod_wsgi`` backend. Here are tips to migrate from `django-sendfile` to `django-downloadview`... 1. In your project's and apps dependencies, replace ``django-sendfile`` by ``django-downloadview``. 2. In your Python scripts, replace ``import sendfile`` and ``from sendfile`` by ``import django_downloadview`` and ``from django_downloadview``. You get something like ``from django_downloadview import sendfile`` 3. Adapt your settings as explained in :doc:`/settings`. Pay attention to: * replace ``sendfile`` by ``django_downloadview`` in ``INSTALLED_APPS``. * replace ``SENDFILE_BACKEND`` by ``DOWNLOADVIEW_BACKEND`` * setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do more. * register ``django_downloadview.SmartDownloadMiddleware`` in ``MIDDLEWARE``. 4. Change your tests if any. You can no longer use `django-senfile`'s ``development`` backend. See :doc:`/testing` for `django-downloadview`'s toolkit. 5. Here you are! ... or please report your story/bug at `django-downloadview's bugtracker`_ ;) ************* API reference ************* .. autofunction:: django_downloadview.shortcuts.sendfile .. rubric:: References .. target-notes:: .. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile .. _`django-downloadview's bugtracker`: https://github.com/jazzband/django-downloadview/issues ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/files.txt0000644000175100001770000000610100000000000021140 0ustar00runnerdocker00000000000000############# File wrappers ############# .. module:: django_downloadview.files A view return :class:`~django_downloadview.response.DownloadResponse` which itself carries a file wrapper. Here are file wrappers distributed by Django and django-downloadview. ***************** Django's builtins ***************** `Django itself provides some file wrappers`_ you can use within ``django-downloadview``: * :class:`django.core.files.File` wraps a file that live on local filesystem, initialized with a path. ``django-downloadview`` uses this wrapper in :doc:`/views/path`. * :class:`django.db.models.fields.files.FieldFile` wraps a file that is managed in a model. ``django-downloadview`` uses this wrapper in :doc:`/views/object`. * :class:`django.core.files.base.ContentFile` wraps a bytes, string or unicode object. You may use it with :doc:`VirtualDownloadView `. **************************** django-downloadview builtins **************************** ``django-downloadview`` implements additional file wrappers: * :class:`StorageFile` wraps a file that is managed via a storage (but not necessarily via a model). :doc:`/views/storage` uses this wrapper. * :class:`HTTPFile` wraps a file that lives at some (remote) location, initialized with an URL. :doc:`/views/http` uses this wrapper. * :class:`VirtualFile` wraps a file that lives in memory, i.e. built as a string. This is a convenient wrapper to use in :doc:`/views/virtual` subclasses. ********************** Low-level IO utilities ********************** `django-downloadview` provides two classes to implement file-like objects whose content is dynamically generated: * :class:`~django_downloadview.io.TextIteratorIO` for generated text; * :class:`~django_downloadview.io.BytesIteratorIO` for generated bytes. These classes may be handy to serve dynamically generated files. See :doc:`/views/virtual` for details. .. tip:: **Text or bytes?** (formerly "unicode or str?") As `django-downloadview` is meant to serve files, as opposed to read or parse files, what matters is file contents is preserved. `django-downloadview` tends to handle files in binary mode and as bytes. ************* API reference ************* StorageFile =========== .. autoclass:: StorageFile :members: :undoc-members: :show-inheritance: :member-order: bysource HTTPFile ======== .. autoclass:: HTTPFile :members: :undoc-members: :show-inheritance: :member-order: bysource VirtualFile =========== .. autoclass:: VirtualFile :members: :undoc-members: :show-inheritance: :member-order: bysource BytesIteratorIO =============== .. autoclass:: django_downloadview.io.BytesIteratorIO :members: :undoc-members: :show-inheritance: :member-order: bysource TextIteratorIO ============== .. autoclass:: django_downloadview.io.TextIteratorIO :members: :undoc-members: :show-inheritance: :member-order: bysource .. rubric:: Notes & references .. target-notes:: .. _`Django itself provides some file wrappers`: https://docs.djangoproject.com/en/3.0/ref/files/file/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/healthchecks.txt0000644000175100001770000000640200000000000022470 0ustar00runnerdocker00000000000000################## Write healthchecks ################## In the previous :doc:`testing ` topic, you made sure the views and middlewares work as expected... within a test environment. One common issue when deploying in production is that the reverse-proxy's configuration does not fit. You cannot check that within test environment. **Healthchecks are made to diagnose issues in live (production) environments**. ************************ Introducing healthchecks ************************ Healthchecks (sometimes called "smoke tests" or "diagnosis") are assertions you run on a live (typically production) service, as opposed to fake/mock service used during tests (unit, integration, functional). See `hospital`_ and `django-doctor`_ projects about writing healthchecks for Python and Django. ******************** Typical healthchecks ******************** Here is a typical healthcheck setup for download views with reverse-proxy optimizations. When you run this healthcheck suite, you get a good overview if a problem occurs: you can compare expected results and learn which part (Django, reverse-proxy or remote storage) is guilty. .. note:: In the examples below, we use "localhost" and ports "80" (reverse-proxy) or "8000" (Django). Adapt them to your configuration. Check storage ============= Put a dummy file on the storage Django uses. The write a healthcheck that asserts you can read the dummy file from storage. **On success, you know remote storage is ok.** Issues may involve permissions or communications (remote storage). .. note:: This healthcheck may be outside Django. Check Django VS storage ======================= Implement a download view dedicated to healthchecks. It is typically a public (but not referenced) view that streams a dummy file from real storage. Let's say you register it as ``/healthcheck-utils/download/`` URL. Write a healthcheck that asserts ``GET http://localhost:8000/healtcheck-utils/download/`` (notice the `8000` port: local Django server) returns the expected reverse-proxy response (X-Accel, X-Sendfile...). **On success, you know there is no configuration issue on the Django side.** Check reverse proxy VS storage ============================== Write a location in your reverse-proxy's configuration that proxy-pass to a dummy file on storage. Write a healthcheck that asserts this location returns the expected dummy file. **On success, you know the reverse proxy can serve files from storage.** Check them all together ======================= We just checked all parts separately, so let's make sure they can work together. Configure the reverse-proxy so that `/healthcheck-utils/download/` is proxied to Django. Then write a healthcheck that asserts ``GET http://localhost:80/healthcheck-utils/download`` (notice the `80` port: reverse-proxy server) returns the expected dummy file. **On success, you know everything is ok.** On failure, there is an issue in the X-Accel/X-Sendfile configuration. .. note:: This last healthcheck should be the first one to run, i.e. if it passes, others should pass too. The others are useful when this one fails. .. rubric:: Notes & references .. target-notes:: .. _`hospital`: https://pypi.python.org/pypi/hospital .. _`django-doctor`: https://pypi.python.org/pypi/django-doctor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/index.txt0000644000175100001770000000043300000000000021147 0ustar00runnerdocker00000000000000.. include:: ../README.rst ******** Contents ******** .. toctree:: :maxdepth: 2 :titlesonly: overview install settings views/index optimizations/index testing healthchecks files responses django-sendfile demo about/index contributing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/install.txt0000644000175100001770000000003000000000000021477 0ustar00runnerdocker00000000000000.. include:: ../INSTALL ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8587322 django_downloadview-2.4.0/docs/optimizations/0000755000175100001770000000000000000000000022210 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/optimizations/apache.txt0000644000175100001770000000662500000000000024203 0ustar00runnerdocker00000000000000###### Apache ###### If you serve Django behind Apache, then you can delegate the file streaming to Apache and get increased performance: * lower resources used by Python/Django workers ; * faster download. See `Apache mod_xsendfile documentation`_ for details. ***************** Known limitations ***************** * Apache needs access to the resource by path on local filesystem. * Thus only files that live on local filesystem can be streamed by Apache. ************ Given a view ************ Let's consider the following view: .. literalinclude:: /../demo/demoproject/apache/views.py :language: python :lines: 1-6, 8-16 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. .. note:: It is generally easier to setup rules based on URL rather than based on name in filesystem. This is because path is generally relative to storage, whereas URL usually contains some storage identifier, i.e. it is easier to target a specific location by URL rather than by filesystem name. *************************** Setup XSendfile middlewares *************************** Make sure ``django_downloadview.SmartDownloadMiddleware`` is in ``MIDDLEWARE`` of your `Django` settings. Example: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 63-70 Then set ``django_downloadview.apache.XSendfileMiddleware`` as ``DOWNLOADVIEW_BACKEND``: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 79 Then register as many ``DOWNLOADVIEW_RULES`` as you wish: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 84, 92-100, 110 Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed to the middleware factory. In the example above, we capture responses by ``source_url`` and convert them to internal redirects to ``destination_dir``. .. autoclass:: django_downloadview.apache.middlewares.XSendfileMiddleware :members: :inherited-members: :undoc-members: :show-inheritance: :member-order: bysource **************************************** Per-view setup with x_sendfile decorator **************************************** Middlewares should be enough for most use cases, but you may want per-view configuration. For `Apache`, there is ``x_sendfile``: .. autofunction:: django_downloadview.apache.decorators.x_sendfile As an example: .. literalinclude:: /../demo/demoproject/apache/views.py :language: python :lines: 1-7, 17- ************************************* Test responses with assert_x_sendfile ************************************* Use :func:`~django_downloadview.apache.decorators.assert_x_sendfile` function as a shortcut in your tests. .. literalinclude:: /../demo/demoproject/apache/tests.py :language: python .. autofunction:: django_downloadview.apache.tests.assert_x_sendfile The tests above assert the `Django` part is OK. Now let's configure `Apache`. ************ Setup Apache ************ See `Apache mod_xsendfile documentation`_ for details. ********************************************* Assert everything goes fine with healthchecks ********************************************* :doc:`Healthchecks ` are the best way to check the complete setup. .. rubric:: References .. target-notes:: .. _`Apache mod_xsendfile documentation`: https://tn123.org/mod_xsendfile/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/optimizations/index.txt0000644000175100001770000000712600000000000024066 0ustar00runnerdocker00000000000000################## Optimize streaming ################## Some reverse proxies allow applications to delegate actual download to the proxy: * with Django, manage permissions, generate files... * let the reverse proxy serve the file. As a result, you get increased performance: reverse proxies are more efficient than Django at serving static files. *********************** Supported features grid *********************** Supported features depend on backend. Given the file you want to stream, the backend may or may not be able to handle it: +-----------------------+-------------------------+-------------------------+-------------------------+ | View / File | :doc:`nginx` | :doc:`apache` | :doc:`lighttpd` | +=======================+=========================+=========================+=========================+ | :doc:`/views/path` | Yes, local filesystem. | Yes, local filesystem. | Yes, local filesystem. | +-----------------------+-------------------------+-------------------------+-------------------------+ | :doc:`/views/storage` | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. | +-----------------------+-------------------------+-------------------------+-------------------------+ | :doc:`/views/object` | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. | +-----------------------+-------------------------+-------------------------+-------------------------+ | :doc:`/views/http` | Yes. | No. | No. | +-----------------------+-------------------------+-------------------------+-------------------------+ | :doc:`/views/virtual` | No. | No. | No. | +-----------------------+-------------------------+-------------------------+-------------------------+ As an example, :doc:`Nginx X-Accel ` handles URL for internal redirects, so it can manage :class:`~django_downloadview.files.HTTPFile`; whereas :doc:`Apache X-Sendfile ` handles absolute path, so it can only deal with files on local filesystem. There are currently no optimizations to stream in-memory files, since they only live on Django side, i.e. they do not persist after Django returned a response. Note: there is `a feature request about "local cache" for streamed files`_. ***************** How does it work? ***************** View return some :class:`~django_downloadview.response.DownloadResponse` instance, which itself carries a :doc:`file wrapper `. `django-downloadview` provides response middlewares and decorators that are able to capture :class:`~django_downloadview.response.DownloadResponse` instances and convert them to :class:`~django_downloadview.response.ProxiedDownloadResponse`. The :class:`~django_downloadview.response.ProxiedDownloadResponse` is specific to the reverse-proxy (backend): it tells the reverse proxy to stream some resource. .. note:: The feature is inspired by :mod:`Django's TemplateResponse ` *********************** Available optimizations *********************** Here are optimizations builtin `django_downloadview`: .. toctree:: :titlesonly: nginx apache lighttpd .. note:: If you need support for additional optimizations, `tell us`_! .. rubric:: Notes & references .. target-notes:: .. _`tell us`: https://github.com/jazzband/django-downloadview/issues?labels=optimizations .. _`a feature request about "local cache" for streamed files`: https://github.com/jazzband/django-downloadview/issues/70 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/optimizations/lighttpd.txt0000644000175100001770000000752000000000000024574 0ustar00runnerdocker00000000000000######## Lighttpd ######## If you serve Django behind `Lighttpd`, then you can delegate the file streaming to `Lighttpd` and get increased performance: * lower resources used by Python/Django workers ; * faster download. See `Lighttpd X-Sendfile documentation`_ for details. .. note:: Currently, `django_downloadview` supports ``X-Sendfile``, but not ``X-Sendfile2``. If you need ``X-Sendfile2`` or know how to handle it, check `X-Sendfile2 feature request on django_downloadview's bugtracker`_. ***************** Known limitations ***************** * Lighttpd needs access to the resource by path on local filesystem. * Thus only files that live on local filesystem can be streamed by Lighttpd. ************ Given a view ************ Let's consider the following view: .. literalinclude:: /../demo/demoproject/lighttpd/views.py :language: python :lines: 1-6, 8-17 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. .. note:: It is generally easier to setup rules based on URL rather than based on name in filesystem. This is because path is generally relative to storage, whereas URL usually contains some storage identifier, i.e. it is easier to target a specific location by URL rather than by filesystem name. *************************** Setup XSendfile middlewares *************************** Make sure ``django_downloadview.SmartDownloadMiddleware`` is in ``MIDDLEWARE`` of your `Django` settings. Example: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 63-70 Then set ``django_downloadview.lighttpd.XSendfileMiddleware`` as ``DOWNLOADVIEW_BACKEND``: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 80 Then register as many ``DOWNLOADVIEW_RULES`` as you wish: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 84, 101-110 Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed to the middleware factory. In the example above, we capture responses by ``source_url`` and convert them to internal redirects to ``destination_dir``. .. autoclass:: django_downloadview.lighttpd.middlewares.XSendfileMiddleware :members: :inherited-members: :undoc-members: :show-inheritance: :member-order: bysource **************************************** Per-view setup with x_sendfile decorator **************************************** Middlewares should be enough for most use cases, but you may want per-view configuration. For `Lighttpd`, there is ``x_sendfile``: .. autofunction:: django_downloadview.lighttpd.decorators.x_sendfile As an example: .. literalinclude:: /../demo/demoproject/lighttpd/views.py :language: python :lines: 1-7, 18- ************************************* Test responses with assert_x_sendfile ************************************* Use :func:`~django_downloadview.lighttpd.decorators.assert_x_sendfile` function as a shortcut in your tests. .. literalinclude:: /../demo/demoproject/lighttpd/tests.py :language: python .. autofunction:: django_downloadview.lighttpd.tests.assert_x_sendfile The tests above assert the `Django` part is OK. Now let's configure `Lighttpd`. ************** Setup Lighttpd ************** See `Lighttpd X-Sendfile documentation`_ for details. ********************************************* Assert everything goes fine with healthchecks ********************************************* :doc:`Healthchecks ` are the best way to check the complete setup. .. rubric:: References .. target-notes:: .. _`Lighttpd X-Sendfile documentation`: http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file .. _`X-Sendfile2 feature request on django_downloadview's bugtracker`: https://github.com/jazzband/django-downloadview/issues/67 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/optimizations/nginx.txt0000644000175100001770000001222200000000000024073 0ustar00runnerdocker00000000000000##### Nginx ##### If you serve Django behind Nginx, then you can delegate the file streaming to Nginx and get increased performance: * lower resources used by Python/Django workers ; * faster download. See `Nginx X-accel documentation`_ for details. ***************** Known limitations ***************** * Nginx needs access to the resource by URL (proxy) or path (location). * Thus :class:`~django_downloadview.files.VirtualFile` and any generated files cannot be streamed by Nginx. ************ Given a view ************ Let's consider the following view: .. literalinclude:: /../demo/demoproject/nginx/views.py :language: python :lines: 1-6, 8-17 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. .. note:: It is generally easier to setup rules based on URL rather than based on name in filesystem. This is because path is generally relative to storage, whereas URL usually contains some storage identifier, i.e. it is easier to target a specific location by URL rather than by filesystem name. ******************************** Setup XAccelRedirect middlewares ******************************** Make sure ``django_downloadview.SmartDownloadMiddleware`` is in ``MIDDLEWARE`` of your `Django` settings. Example: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 62-69 Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as ``DOWNLOADVIEW_BACKEND``: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 75 Then register as many ``DOWNLOADVIEW_RULES`` as you wish: .. literalinclude:: /../demo/demoproject/settings.py :language: python :lines: 83-88 Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed to the middleware factory. In the example above, we capture responses by ``source_url`` and convert them to internal redirects to ``destination_url``. .. autoclass:: django_downloadview.nginx.middlewares.XAccelRedirectMiddleware :members: :inherited-members: :undoc-members: :show-inheritance: :member-order: bysource ********************************************** Per-view setup with x_accel_redirect decorator ********************************************** Middlewares should be enough for most use cases, but you may want per-view configuration. For `nginx`, there is ``x_accel_redirect``: .. autofunction:: django_downloadview.nginx.decorators.x_accel_redirect As an example: .. literalinclude:: /../demo/demoproject/nginx/views.py :language: python :lines: 1-7, 17- ******************************************* Test responses with assert_x_accel_redirect ******************************************* Use :func:`~django_downloadview.nginx.decorators.assert_x_accel_redirect` function as a shortcut in your tests. .. literalinclude:: /../demo/demoproject/nginx/tests.py :language: python .. autofunction:: django_downloadview.nginx.tests.assert_x_accel_redirect The tests above assert the `Django` part is OK. Now let's configure `nginx`. *********** Setup Nginx *********** See `Nginx X-accel documentation`_ for details. Here is what you could have in :file:`/etc/nginx/sites-available/default`: .. code-block:: nginx charset utf-8; # Django-powered service. upstream frontend { server 127.0.0.1:8000 fail_timeout=0; } server { listen 80 default; # File-download proxy. # # Will serve /var/www/files/myfile.tar.gz when passed URI # like /optimized-download/myfile.tar.gz # # See http://wiki.nginx.org/X-accel # and https://django-downloadview.readthedocs.io # location /proxied-download { internal; # Location to files on disk. alias /var/www/files/; } # Proxy to Django-powered frontend. location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://frontend; } } ... where specific configuration is the ``location /optimized-download`` section. .. note:: ``/proxied-download`` has the ``internal`` flag, so this location is not available for the client, i.e. users are not able to download files via ``/optimized-download/``. ********************************************* Assert everything goes fine with healthchecks ********************************************* :doc:`Healthchecks ` are the best way to check the complete setup. ************* Common issues ************* ``Unknown charset "utf-8" to override`` ======================================= Add ``charset utf-8;`` in your nginx configuration file. ``open() "path/to/something" failed (2: No such file or directory)`` ==================================================================== Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR`` in Django configuration VS ``alias`` in nginx configuration: in a standard configuration, they should be equal. .. rubric:: References .. target-notes:: .. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/overview.txt0000644000175100001770000000630200000000000021707 0ustar00runnerdocker00000000000000################## Overview, concepts ################## Given: * you manage files with Django (permissions, filters, generation, ...) * files are stored somewhere or generated somehow (local filesystem, remote storage, memory...) As a developer, you want to serve files quick and efficiently. Here is an overview of `django-downloadview`'s answer... ************************************ Generic views cover commons patterns ************************************ Choose the generic view depending on the file you want to serve: * :doc:`/views/object`: file field in a model; * :doc:`/views/storage`: file in a storage; * :doc:`/views/path`: absolute filename on local filesystem; * :doc:`/views/http`: file at URL (the resource is proxied); * :doc:`/views/virtual`: bytes, text, file-like objects, generated files... ************************************************* Generic views and mixins allow easy customization ************************************************* If your use case is a bit specific, you can easily extend the views above or :doc:`create your own based on mixins `. ***************************** Views return DownloadResponse ***************************** Views return :py:class:`~django_downloadview.response.DownloadResponse`. It is a special :py:class:`django.http.StreamingHttpResponse` where content is encapsulated in a file wrapper. Learn more in :doc:`responses`. *********************************** DownloadResponse carry file wrapper *********************************** Views instanciate a :doc:`file wrapper ` and use it to initialize responses. **File wrappers describe files**: they carry files properties such as name, size, encoding... **File wrappers implement loading and iterating over file content**. Whenever possible, file wrappers do not embed file data, in order to save memory. Learn more about available file wrappers in :doc:`files`. ***************************************************************** Middlewares convert DownloadResponse into ProxiedDownloadResponse ***************************************************************** Before WSGI application use file wrapper and actually use file contents, middlewares or decorators) are given the opportunity to capture :class:`~django_downloadview.response.DownloadResponse` instances. Let's take this opportunity to optimize file loading and streaming! A good optimization it to delegate streaming to a reverse proxy, such as `nginx`_ via `X-Accel`_ internal redirects. This way, Django doesn't load file content in memory. `django_downloadview` provides middlewares that convert :class:`~django_downloadview.response.DownloadResponse` into :class:`~django_downloadview.response.ProxiedDownloadResponse`. Learn more in :doc:`optimizations/index`. *************** Testing matters *************** `django-downloadview` also helps you :doc:`test the views you customized `. You may also :doc:`write healthchecks ` to make sure everything goes fine in live environments. ************ What's next? ************ Let's :doc:`install django-downloadview `. .. rubric:: Notes & references .. target-notes:: .. _`nginx`: http://nginx.org .. _`X-Accel`: http://wiki.nginx.org/X-accel ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/responses.txt0000644000175100001770000000114500000000000022062 0ustar00runnerdocker00000000000000######### Responses ######### .. currentmodule:: django_downloadview.response Views return :class:`DownloadResponse`. Middlewares (and decorators) are given the opportunity to capture responses and convert them to :class:`ProxiedDownloadResponse`. **************** DownloadResponse **************** .. autoclass:: DownloadResponse :members: :undoc-members: :show-inheritance: :member-order: bysource *********************** ProxiedDownloadResponse *********************** .. autoclass:: ProxiedDownloadResponse :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/settings.txt0000644000175100001770000000713200000000000021703 0ustar00runnerdocker00000000000000######### Configure ######### Here is the list of Django settings for `django-downloadview`. ************** INSTALLED_APPS ************** There is no need to register this application in ``INSTALLED_APPS``. ****************** MIDDLEWARE ****************** If you plan to setup :doc:`reverse-proxy optimizations `, add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE``. It is a response middleware. Move it after middlewares that compute the response content such as gzip middleware. Example: .. literalinclude:: /../demo/demoproject/settings.py :language: python :start-after: BEGIN middlewares :end-before: END middlewares ******************** DEFAULT_FILE_STORAGE ******************** django-downloadview offers a built-in signed file storage, which cryptographically signs requested file URLs with the Django's built-in TimeStampSigner. To utilize the signed storage views you can configure .. code:: python DEFAULT_FILE_STORAGE='django_downloadview.storage.SignedStorage' The signed file storage system inserts a ``X-Signature`` header to the requested file URLs, and they can then be verified with the supplied ``signature_required`` wrapper function: .. code:: python from django.conf.urls import url, url_patterns from django_downloadview import ObjectDownloadView from django_downloadview.decorators import signature_required from demoproject.download.models import Document # A model with a FileField # ObjectDownloadView inherits from django.views.generic.BaseDetailView. download = ObjectDownloadView.as_view(model=Document, file_field='file') urlpatterns = [ path('download//', signature_required(download)), ] Make sure to test the desired functionality after configuration. *************************** DOWNLOADVIEW_URL_EXPIRATION *************************** Number of seconds signed download URLs are valid before expiring. Default value for this flag is None and URLs never expire. ******************** DOWNLOADVIEW_BACKEND ******************** This setting is used by :class:`~django_downloadview.middlewares.SmartDownloadMiddleware`. It is the import string of a callable (typically a class) of an optimization backend (typically a :class:`~django_downloadview.BaseDownloadMiddleware` subclass). Example: .. literalinclude:: /../demo/demoproject/settings.py :language: python :start-after: BEGIN backend :end-before: END backend See :doc:`/optimizations/index` for a list of available backends (middlewares). When ``django_downloadview.SmartDownloadMiddleware`` is in your ``MIDDLEWARE``, this setting must be explicitely configured (no default value). Else, you can ignore this setting. ****************** DOWNLOADVIEW_RULES ****************** This setting is used by :class:`~django_downloadview.middlewares.SmartDownloadMiddleware`. It is a list of positional arguments or keyword arguments that will be used to instanciate class mentioned as ``DOWNLOADVIEW_BACKEND``. Each item in the list can be either a list of positional arguments, or a dictionary of keyword arguments. One item cannot contain both positional and keyword arguments. Here is an example containing one rule using keyword arguments: .. literalinclude:: /../demo/demoproject/settings.py :language: python :start-after: BEGIN rules :end-before: END rules See :doc:`/optimizations/index` for details about builtin backends (middlewares) and their options. When ``django_downloadview.SmartDownloadMiddleware`` is in your ``MIDDLEWARE``, this setting must be explicitely configured (no default value). Else, you can ignore this setting. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/testing.txt0000644000175100001770000000200700000000000021514 0ustar00runnerdocker00000000000000########### Write tests ########### `django_downloadview` embeds test utilities: * :func:`~django_downloadview.test.temporary_media_root` * :func:`~django_downloadview.test.assert_download_response` * :func:`~django_downloadview.test.setup_view` * :func:`~django_downloadview.nginx.tests.assert_x_accel_redirect` ******************** temporary_media_root ******************** .. autofunction:: django_downloadview.test.temporary_media_root ************************ assert_download_response ************************ .. autofunction:: django_downloadview.test.assert_download_response Examples, related to :doc:`StorageDownloadView demo `: .. literalinclude:: /../demo/demoproject/storage/tests.py :language: python :lines: 3-7, 9-57 ********** setup_view ********** .. autofunction:: django_downloadview.test.setup_view Example, related to :doc:`StorageDownloadView demo `: .. literalinclude:: /../demo/demoproject/storage/tests.py :language: python :lines: 1-2, 8-12, 59- ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/docs/views/0000755000175100001770000000000000000000000020434 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/custom.txt0000644000175100001770000000564300000000000022517 0ustar00runnerdocker00000000000000################## Make your own view ################## .. currentmodule:: django_downloadview.views.base ************* DownloadMixin ************* The :py:class:`django_downloadview.views.DownloadMixin` class is not a view. It is a base class which you can inherit of to create custom download views. ``DownloadMixin`` is a base of `BaseDownloadView`_, which itself is a base of all other django_downloadview's builtin views. .. autoclass:: DownloadMixin :members: :undoc-members: :show-inheritance: :member-order: bysource **************** BaseDownloadView **************** The :py:class:`django_downloadview.views.BaseDownloadView` class is a base class to create download views. It inherits `DownloadMixin`_ and :py:class:`django.views.generic.base.View`. The only thing it does is to implement :py:meth:`get `: it triggers :py:meth:`DownloadMixin's render_to_response `. .. autoclass:: BaseDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource *********************************************** Serving a file inline rather than as attachment *********************************************** Use :attr:`~DownloadMixin.attachment` to make a view serve a file inline rather than as attachment, i.e. to display the file as if it was an internal part of a page rather than triggering "Save file as..." prompt. See details in :attr:`attachment API documentation <~DownloadMixin.attachment>`. .. literalinclude:: /../demo/demoproject/object/views.py :language: python :lines: 1-2, 19 ************************************ Handling http not modified responses ************************************ Sometimes, you know the latest date and time the content was generated at, and you know a new request would generate exactly the same content. In such a case, you should implement :py:meth:`~VirtualDownloadView.was_modified_since` in your view. .. note:: Default :py:meth:`~VirtualDownloadView.was_modified_since` implementation trusts file wrapper's ``was_modified_since`` if any. Else (if calling ``was_modified_since()`` raises ``NotImplementedError`` or ``AttributeError``) it returns ``True``, i.e. it assumes the file was modified. As an example, the download views above always generate "Hello world!"... so, if the client already downloaded it, we can safely return some HTTP "304 Not Modified" response: .. code:: python from django.core.files.base import ContentFile from django_downloadview import VirtualDownloadView class TextDownloadView(VirtualDownloadView): def get_file(self): """Return :class:`django.core.files.base.ContentFile` object.""" return ContentFile("Hello world!", name='hello-world.txt') def was_modified_since(self, file_instance, since): return False # Never modified, always "Hello world!". ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/http.txt0000644000175100001770000000206200000000000022154 0ustar00runnerdocker00000000000000################ HTTPDownloadView ################ .. py:module:: django_downloadview.views.http :class:`HTTPDownloadView` **serves a file given an URL.**, i.e. it acts like a proxy. This view is particularly handy when: * the client does not have access to the file resource, while your Django server does. * the client does trust your server, your server trusts a third-party, you do not want to bother the client with the third-party. ************** Simple example ************** Setup a view to stream files given URL: .. literalinclude:: /../demo/demoproject/http/views.py :language: python ************ Base options ************ :class:`HTTPDownloadView` inherits from :class:`~django_downloadview.views.base.DownloadMixin`, which has various options such as :attr:`~django_downloadview.views.base.DownloadMixin.basename` or :attr:`~django_downloadview.views.base.DownloadMixin.attachment`. ************* API reference ************* .. autoclass:: HTTPDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/index.txt0000644000175100001770000000105000000000000022300 0ustar00runnerdocker00000000000000########### Setup views ########### Setup views depending on your needs: * :doc:`/views/object` when you have a model with a file field; * :doc:`/views/storage` when you manage files in a storage; * :doc:`/views/path` when you have an absolute filename on local filesystem; * :doc:`/views/http` when you have an URL (the resource is proxied); * :doc:`/views/virtual` when you generate a file dynamically; * :doc:`bases and mixins ` to make your own. .. toctree:: :hidden: object storage path http virtual custom ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/object.txt0000644000175100001770000000546700000000000022457 0ustar00runnerdocker00000000000000################## ObjectDownloadView ################## .. py:module:: django_downloadview.views.object :class:`ObjectDownloadView` **serves files managed in models with file fields** such as :class:`~django.db.models.FileField` or :class:`~django.db.models.ImageField`. Use this view like Django's builtin :class:`~django.views.generic.detail.DetailView`. Additional options allow you to store file metadata (size, content-type, ...) in the model, as deserialized fields. ************** Simple example ************** Given a model with a :class:`~django.db.models.FileField`: .. literalinclude:: /../demo/demoproject/object/models.py :language: python :lines: 1-6 Setup a view to stream the ``file`` attribute: .. literalinclude:: /../demo/demoproject/object/views.py :language: python :lines: 1-6 :class:`~django_downloadview.views.object.ObjectDownloadView` inherits from :class:`~django.views.generic.detail.BaseDetailView`, i.e. it expects either ``slug`` or ``pk``: .. literalinclude:: /../demo/demoproject/object/urls.py :language: python :lines: 1-7, 8-11, 27 ************ Base options ************ :class:`ObjectDownloadView` inherits from :class:`~django_downloadview.views.base.DownloadMixin`, which has various options such as :attr:`~django_downloadview.views.base.DownloadMixin.basename` or :attr:`~django_downloadview.views.base.DownloadMixin.attachment`. *************************** Serving specific file field *************************** If your model holds several file fields, or if the file field name is not "file", you can use :attr:`ObjectDownloadView.file_field` to specify the field to use. Here is a model where there are two file fields: .. literalinclude:: /../demo/demoproject/object/models.py :language: python :lines: 1-6, 7 Then here is the code to serve "another_file" instead of the default "file": .. literalinclude:: /../demo/demoproject/object/views.py :language: python :lines: 1-4, 8-11 ********************************** Mapping file attributes to model's ********************************** Sometimes, you use Django model to store file's metadata. Some of this metadata can be used when you serve the file. As an example, let's consider the client-side basename lives in model and not in storage: .. literalinclude:: /../demo/demoproject/object/models.py :language: python :lines: 1-6, 8 Then you can configure the :attr:`ObjectDownloadView.basename_field` option: .. literalinclude:: /../demo/demoproject/object/views.py :language: python :lines: 1-4, 13-17 .. note:: ``basename`` could have been a model's property instead of a ``CharField``. See details below for a full list of options. ************* API reference ************* .. autoclass:: ObjectDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/path.txt0000644000175100001770000000317600000000000022140 0ustar00runnerdocker00000000000000################ PathDownloadView ################ .. py:module:: django_downloadview.views.path :class:`PathDownloadView` **serves file given a path on local filesystem**. Use this view whenever you just have a path, outside storage or model. .. warning:: Take care of path validation, especially if you compute paths from user input: an attacker may be able to download files from arbitrary locations. In most cases, you should consider managing files in storages, because they implement default security mechanisms. ************** Simple example ************** Setup a view to stream files given path: .. literalinclude:: /../demo/demoproject/path/views.py :language: python :lines: 1-13 :emphasize-lines: 13 ************ Base options ************ :class:`PathDownloadView` inherits from :class:`~django_downloadview.views.base.DownloadMixin`, which has various options such as :attr:`~django_downloadview.views.base.DownloadMixin.basename` or :attr:`~django_downloadview.views.base.DownloadMixin.attachment`. ************************** Computing path dynamically ************************** Override the :meth:`PathDownloadView.get_path` method to adapt path resolution to your needs: .. literalinclude:: /../demo/demoproject/path/views.py :language: python :lines: 1-9, 15- The view accepts a ``path`` argument you can setup either in ``as_view`` or via URLconfs: .. literalinclude:: /../demo/demoproject/path/urls.py :language: python :lines: 1-13 ************* API reference ************* .. autoclass:: PathDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/storage.txt0000644000175100001770000000313000000000000022636 0ustar00runnerdocker00000000000000################### StorageDownloadView ################### .. py:module:: django_downloadview.views.storage :class:`StorageDownloadView` **serves files given a storage and a path**. Use this view when you manage files in a storage (which is a good practice), unrelated to a model. ************** Simple example ************** Given a storage: .. literalinclude:: /../demo/demoproject/storage/views.py :language: python :lines: 1, 4-5 Setup a view to stream files in storage: .. literalinclude:: /../demo/demoproject/storage/views.py :language: python :lines: 3-6, 8-9 The view accepts a ``path`` argument you can setup either in ``as_view`` or via URLconfs: .. literalinclude:: /../demo/demoproject/storage/urls.py :language: python :lines: 1-6, 7-11, 17 ************ Base options ************ :class:`StorageDownloadView` inherits from :class:`~django_downloadview.views.base.DownloadMixin`, which has various options such as :attr:`~django_downloadview.views.base.DownloadMixin.basename` or :attr:`~django_downloadview.views.base.DownloadMixin.attachment`. ************************** Computing path dynamically ************************** Override the :meth:`StorageDownloadView.get_path` method to adapt path resolution to your needs. As an example, here is the same view as above, but the path is converted to uppercase: .. literalinclude:: /../demo/demoproject/storage/views.py :language: python :lines: 3-5, 11-20 ************* API reference ************* .. autoclass:: StorageDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/docs/views/virtual.txt0000644000175100001770000000435000000000000022665 0ustar00runnerdocker00000000000000################### VirtualDownloadView ################### .. py:module:: django_downloadview.views.virtual :class:`VirtualDownloadView` **serves files that do not live on disk**. Use it when you want to stream a file which content is dynamically generated or which lives in memory. It is all about overriding :meth:`VirtualDownloadView.get_file` method so that it returns a suitable file wrapper... .. note:: Current implementation does not support reverse-proxy optimizations, because content is actually generated within Django, not stored in some third-party place. ************ Base options ************ :class:`VirtualDownloadView` inherits from :class:`~django_downloadview.views.base.DownloadMixin`, which has various options such as :attr:`~django_downloadview.views.base.DownloadMixin.basename` or :attr:`~django_downloadview.views.base.DownloadMixin.attachment`. *************************************** Serve text (string or unicode) or bytes *************************************** Let's consider you build text dynamically, as a bytes or string or unicode object. Serve it with Django's builtin :class:`~django.core.files.base.ContentFile` wrapper: .. literalinclude:: /../demo/demoproject/virtual/views.py :language: python :lines: 1, 3, 7-11 ************** Serve StringIO ************** :class:`~StringIO.StringIO` object lives in memory. Let's wrap it in some download view via :class:`~django_downloadview.files.VirtualFile`: .. literalinclude:: /../demo/demoproject/virtual/views.py :language: python :lines: 1-4, 5-6, 13-17 ************************ Stream generated content ************************ Let's consider you have a generator function (``yield``) or an iterator object (``__iter__()``): .. literalinclude:: /../demo/demoproject/virtual/views.py :language: python :lines: 20-23 Stream generated content using :class:`VirtualDownloadView`, :class:`~django_downloadview.files.VirtualFile` and :class:`~django_downloadview.io.BytesIteratorIO`: .. literalinclude:: /../demo/demoproject/virtual/views.py :language: python :lines: 3, 26-30 ************* API reference ************* .. autoclass:: VirtualDownloadView :members: :undoc-members: :show-inheritance: :member-order: bysource ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/setup.cfg0000644000175100001770000000004600000000000020170 0ustar00runnerdocker00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/setup.py0000644000175100001770000000362100000000000020063 0ustar00runnerdocker00000000000000import os from setuptools import setup #: Absolute path to directory containing setup.py file. here = os.path.abspath(os.path.dirname(__file__)) setup( name="django-downloadview", use_scm_version={"version_scheme": "post-release"}, setup_requires=["setuptools_scm"], description="Serve files with Django and reverse-proxies.", long_description=open(os.path.join(here, "README.rst")).read(), long_description_content_type="text/x-rst", classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", ], keywords=" ".join( [ "file", "stream", "download", "FileField", "ImageField", "x-accel", "x-accel-redirect", "x-sendfile", "sendfile", "mod_xsendfile", "offload", ] ), author="Benoît Bryon", author_email="benoit@marmelune.net", url="https://django-downloadview.readthedocs.io/", license="BSD", packages=[ "django_downloadview", "django_downloadview.apache", "django_downloadview.lighttpd", "django_downloadview.nginx", "django_downloadview.views", ], include_package_data=True, zip_safe=False, python_requires=">=3.8", install_requires=[ # BEGIN requirements "Django>=4.2", "requests", # END requirements ], extras_require={ "test": ["tox"], }, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1722862276.8627324 django_downloadview-2.4.0/tests/0000755000175100001770000000000000000000000017511 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/__init__.py0000644000175100001770000000000000000000000021610 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/api.py0000644000175100001770000001255000000000000020637 0ustar00runnerdocker00000000000000"""Test suite around :mod:`django_downloadview.api` and deprecation plan.""" from importlib import import_module, reload import unittest import warnings from django.core.exceptions import ImproperlyConfigured import django.test from django.test.utils import override_settings class APITestCase(unittest.TestCase): """Make sure django_downloadview exposes API.""" def assert_module_attributes(self, module_path, attribute_names): """Assert imported ``module_path`` has ``attribute_names``.""" module = import_module(module_path) missing_attributes = [] for attribute_name in attribute_names: if not hasattr(module, attribute_name): missing_attributes.append(attribute_name) if missing_attributes: self.fail( 'Missing attributes in "{module_path}": {", ".join(missing_attributes)}' ) def test_root_attributes(self): """API is exposed in django_downloadview root package. The goal of this test is to make sure that main items of project's API are easy to import... and prevent refactoring from breaking main API. If this test is broken by refactoring, a :class:`DeprecationWarning` or simimar should be raised. """ api = [ # Views: "ObjectDownloadView", "StorageDownloadView", "PathDownloadView", "HTTPDownloadView", "VirtualDownloadView", "BaseDownloadView", "DownloadMixin", # File wrappers: "StorageFile", "HTTPFile", "VirtualFile", # Responses: "DownloadResponse", "ProxiedDownloadResponse", # Middlewares: "BaseDownloadMiddleware", "DownloadDispatcherMiddleware", "SmartDownloadMiddleware", # Testing: "assert_download_response", "setup_view", "temporary_media_root", # Utilities: "StringIteratorIO", "sendfile", ] self.assert_module_attributes("django_downloadview", api) def test_nginx_attributes(self): """Nginx-related API is exposed in django_downloadview.nginx.""" api = [ "XAccelRedirectResponse", "XAccelRedirectMiddleware", "x_accel_redirect", "assert_x_accel_redirect", ] self.assert_module_attributes("django_downloadview.nginx", api) def test_apache_attributes(self): """Apache-related API is exposed in django_downloadview.apache.""" api = [ "XSendfileResponse", "XSendfileMiddleware", "x_sendfile", "assert_x_sendfile", ] self.assert_module_attributes("django_downloadview.apache", api) def test_lighttpd_attributes(self): """Lighttpd-related API is exposed in django_downloadview.lighttpd.""" api = [ "XSendfileResponse", "XSendfileMiddleware", "x_sendfile", "assert_x_sendfile", ] self.assert_module_attributes("django_downloadview.lighttpd", api) class DeprecatedAPITestCase(django.test.SimpleTestCase): """Make sure using deprecated items raise DeprecationWarning.""" def test_nginx_x_accel_redirect_middleware(self): "XAccelRedirectMiddleware in settings triggers ImproperlyConfigured." with override_settings( MIDDLEWARE=["django_downloadview.nginx.XAccelRedirectMiddleware"], ): with self.assertRaises(ImproperlyConfigured): import django_downloadview.nginx.settings reload(django_downloadview.nginx.settings) def test_nginx_x_accel_redirect_global_settings(self): """Global settings for Nginx middleware are deprecated.""" settings_overrides = { "NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING": True, "NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE": 32, "NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES": 3600, "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT": "/", "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR": "/", "NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL": "/", "NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL": "/", "NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL": "/", } import django_downloadview.nginx.settings missed_warnings = [] for setting_name, setting_value in settings_overrides.items(): warnings.resetwarnings() warnings.simplefilter("always") with warnings.catch_warnings(record=True) as warning_list: with override_settings(**{setting_name: setting_value}): reload(django_downloadview.nginx.settings) caught = False for warning_item in warning_list: if warning_item.category is DeprecationWarning: if "deprecated" in str(warning_item.message): if setting_name in str(warning_item.message): caught = True break if not caught: missed_warnings.append(setting_name) if missed_warnings: self.fail( f"No DeprecationWarning raised about following settings: " f'{", ".join(missed_warnings)}.' ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/io.py0000644000175100001770000000305400000000000020474 0ustar00runnerdocker00000000000000"""Tests around :mod:`django_downloadview.io`.""" import unittest from django_downloadview import BytesIteratorIO, TextIteratorIO HELLO_TEXT = "Hello world!\né\n" HELLO_BYTES = b"Hello world!\n\xc3\xa9\n" def generate_hello_text(): """Generate u'Hello world!\n'.""" yield "Hello " yield "world!" yield "\n" yield "é" yield "\n" def generate_hello_bytes(): """Generate b'Hello world!\n'.""" yield b"Hello " yield b"world!" yield b"\n" yield b"\xc3\xa9" yield b"\n" class TextIteratorIOTestCase(unittest.TestCase): """Tests around :class:`~django_downloadview.io.TextIteratorIO`.""" def test_read_text(self): """TextIteratorIO obviously accepts text generator.""" file_obj = TextIteratorIO(generate_hello_text()) self.assertEqual(file_obj.read(), HELLO_TEXT) def test_read_bytes(self): """TextIteratorIO converts bytes as text.""" file_obj = TextIteratorIO(generate_hello_bytes()) self.assertEqual(file_obj.read(), HELLO_TEXT) class BytesIteratorIOTestCase(unittest.TestCase): """Tests around :class:`~django_downloadview.io.BytesIteratorIO`.""" def test_read_bytes(self): """BytesIteratorIO obviously accepts bytes generator.""" file_obj = BytesIteratorIO(generate_hello_bytes()) self.assertEqual(file_obj.read(), HELLO_BYTES) def test_read_text(self): """BytesIteratorIO converts text as bytes.""" file_obj = BytesIteratorIO(generate_hello_text()) self.assertEqual(file_obj.read(), HELLO_BYTES) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/packaging.py0000644000175100001770000000262500000000000022014 0ustar00runnerdocker00000000000000"""Tests around project's distribution and packaging.""" import importlib.metadata import os import unittest tests_dir = os.path.dirname(os.path.abspath(__file__)) project_dir = os.path.dirname(tests_dir) build_dir = os.path.join(project_dir, "var", "docs", "html") class VersionTestCase(unittest.TestCase): """Various checks around project's version info.""" def get_version(self): """Return django_downloadview.__version__.""" from django_downloadview import __version__ return __version__ def test_version_present(self): """:PEP:`396` - django_downloadview has __version__ attribute.""" try: self.get_version() except ImportError: self.fail("django_downloadview package has no __version__.") def test_version_match(self): """django_downloadview.__version__ matches importlib metadata.""" distribution = importlib.metadata.distribution("django-downloadview") installed_version = distribution.version self.assertEqual( installed_version, self.get_version(), "Version mismatch: django_downloadview.__version__ " 'is "%s" whereas importlib.metadata tells "%s". ' "You may need to run ``make develop`` to update the " "installed version in development environment." % (self.get_version(), installed_version), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/response.py0000644000175100001770000000213700000000000021724 0ustar00runnerdocker00000000000000"""Unit tests around responses.""" import unittest from django_downloadview.response import DownloadResponse class DownloadResponseTestCase(unittest.TestCase): """Tests around :class:`django_downloadviews.response.DownloadResponse`.""" def test_content_disposition_encoding(self): """Content-Disposition header is encoded.""" response = DownloadResponse( "fake file", attachment=True, basename="espacé .txt", ) headers = response.default_headers self.assertIn('filename="espace_.txt"', headers["Content-Disposition"]) self.assertIn( "filename*=UTF-8''espac%C3%A9%20.txt", headers["Content-Disposition"] ) def test_content_disposition_escaping(self): """Content-Disposition headers escape special characters.""" response = DownloadResponse( "fake file", attachment=True, basename=r'"malicious\file.exe' ) headers = response.default_headers self.assertIn( r'filename="\"malicious\\file.exe"', headers["Content-Disposition"] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/sendfile.py0000644000175100001770000000321600000000000021656 0ustar00runnerdocker00000000000000"""Tests around :py:mod:`django_downloadview.sendfile`.""" from django.http import Http404 import django.test from django_downloadview.response import DownloadResponse from django_downloadview.shortcuts import sendfile class SendfileTestCase(django.test.TestCase): """Tests around :func:`django_downloadview.sendfile.sendfile`.""" def test_defaults(self): """sendfile() takes at least request and filename.""" request = django.test.RequestFactory().get("/fake-url") filename = __file__ response = sendfile(request, filename) self.assertTrue(isinstance(response, DownloadResponse)) self.assertFalse(response.attachment) def test_custom(self): """sendfile() accepts various arguments for response tuning.""" request = django.test.RequestFactory().get("/fake-url") filename = __file__ response = sendfile( request, filename, attachment=True, attachment_filename="toto.txt", mimetype="test/octet-stream", encoding="gzip", ) self.assertTrue(isinstance(response, DownloadResponse)) self.assertTrue(response.attachment) self.assertEqual(response.basename, "toto.txt") self.assertEqual(response["Content-Type"], "test/octet-stream; charset=utf-8") self.assertEqual(response.get_encoding(), "gzip") def test_404(self): """sendfile() raises Http404 if file does not exists.""" request = django.test.RequestFactory().get("/fake-url") filename = "i-do-no-exist" with self.assertRaises(Http404): sendfile(request, filename) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/signature.py0000644000175100001770000000314700000000000022071 0ustar00runnerdocker00000000000000""" Test signature generation and validation. """ import unittest from django.core.exceptions import PermissionDenied from django.core.signing import TimestampSigner from django_downloadview.decorators import _signature_is_valid from django_downloadview.storage import SignedURLMixin class TestStorage: def url(self, name): return "https://example.com/{name}".format(name=name) class SignedTestStorage(SignedURLMixin, TestStorage): pass class SignatureGeneratorTestCase(unittest.TestCase): def test_signed_storage(self): """ django_downloadview.storage.SignedURLMixin adds X-Signature to URLs. """ storage = SignedTestStorage() url = storage.url("test") self.assertIn("https://example.com/test?X-Signature=", url) class SignatureValidatorTestCase(unittest.TestCase): def test_verify_signature(self): """ django_downloadview.decorators._signature_is_valid returns True on valid signatures. """ signer = TimestampSigner() request = unittest.mock.MagicMock() request.path = "test" request.GET = {"X-Signature": signer.sign("test")} self.assertIsNone(_signature_is_valid(request)) def test_verify_signature_invalid(self): """ django_downloadview.decorators._signature_is_valid raises PermissionDenied on invalid signatures. """ request = unittest.mock.MagicMock() request.path = "test" request.GET = {"X-Signature": "not-valid"} with self.assertRaises(PermissionDenied): _signature_is_valid(request) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tests/views.py0000644000175100001770000003222300000000000021222 0ustar00runnerdocker00000000000000"""Tests around :mod:`django_downloadview.views`.""" import calendar from datetime import datetime import os import unittest from unittest import mock from django.core.files import File from django.http import Http404 from django.http.response import HttpResponseNotModified import django.test from django_downloadview import exceptions, views from django_downloadview.test import setup_view class DownloadMixinTestCase(unittest.TestCase): """Test suite around :class:`django_downloadview.views.DownloadMixin`.""" def test_get_file(self): """DownloadMixin.get_file() raise NotImplementedError. Subclasses must implement it! """ mixin = views.DownloadMixin() with self.assertRaises(NotImplementedError): mixin.get_file() def test_get_basename(self): """DownloadMixin.get_basename() returns basename attribute.""" mixin = views.DownloadMixin() self.assertEqual(mixin.get_basename(), None) mixin.basename = "fake" self.assertEqual(mixin.get_basename(), "fake") def test_was_modified_since_specific(self): """DownloadMixin.was_modified_since() delegates to file wrapper.""" file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock( return_value=mock.sentinel.return_value ) mixin = views.DownloadMixin() since = mock.sentinel.since return_value = mixin.was_modified_since(file_wrapper, since) self.assertEqual(return_value, mock.sentinel.return_value) file_wrapper.was_modified_since.assert_called_once_with(since) def test_was_modified_since_not_implemented(self): """DownloadMixin.was_modified_since() returns True if file wrapper does not support ``modified_time`` or ``size`` attributes.""" fields = ["modified_time", "size"] side_effects = [AttributeError("fake"), NotImplementedError("fake")] for field in fields: for side_effect in side_effects: file_wrapper = mock.Mock() setattr( file_wrapper, field, mock.Mock(side_effect=AttributeError("fake")) ) mixin = views.DownloadMixin() since = mock.sentinel.since self.assertTrue(mixin.was_modified_since(file_wrapper, since)) def test_was_modified_since_file(self): """DownloadMixin.was_modified_since() tries (1) file's implementation. :meth:`django_downloadview.views.base.DownloadMixin.was_modified_since` first tries to delegate computations to file wrapper's implementation. """ file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock( return_value=mock.sentinel.was_modified ) mixin = views.DownloadMixin() self.assertIs( mixin.was_modified_since(file_wrapper, mock.sentinel.since), mock.sentinel.was_modified, ) file_wrapper.was_modified_since.assert_called_once_with(mock.sentinel.since) def test_was_modified_since_django(self): """DownloadMixin.was_modified_since() tries (2) files attributes. When calling file wrapper's ``was_modified_since()`` raises ``NotImplementedError`` or ``AttributeError``, :meth:`django_downloadview.views.base.DownloadMixin.was_modified_since` tries to pass file wrapper's ``modified_time`` to :func:`django.views.static import was_modified_since`. """ file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock(side_effect=AttributeError) file_wrapper.modified_time = datetime.now() was_modified_since_mock = mock.Mock(return_value=mock.sentinel.was_modified) mixin = views.DownloadMixin() with mock.patch( "django_downloadview.views.base.was_modified_since", new=was_modified_since_mock, ): self.assertIs( mixin.was_modified_since(file_wrapper, mock.sentinel.since), mock.sentinel.was_modified, ) was_modified_since_mock.assert_called_once_with( mock.sentinel.since, calendar.timegm(file_wrapper.modified_time.utctimetuple()), ) def test_was_modified_since_fallback(self): """DownloadMixin.was_modified_since() fallbacks to `True`. When: * calling file wrapper's ``was_modified_since()`` raises ``NotImplementedError`` or ``AttributeError``; * and accessing ``modified_time`` from file wrapper raises ``NotImplementedError`` or ``AttributeError``... ... then :meth:`django_downloadview.views.base.DownloadMixin.was_modified_since` returns ``True``. """ file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock(side_effect=NotImplementedError) type(file_wrapper).modified_time = mock.PropertyMock( side_effect=NotImplementedError ) mixin = views.DownloadMixin() self.assertIs(mixin.was_modified_since(file_wrapper, "fake since"), True) def test_not_modified_response(self): "DownloadMixin.not_modified_response returns HttpResponseNotModified." mixin = views.DownloadMixin() response = mixin.not_modified_response() self.assertTrue(isinstance(response, HttpResponseNotModified)) def test_download_response(self): "DownloadMixin.download_response() returns download response instance." mixin = views.DownloadMixin() mixin.file_instance = mock.sentinel.file_wrapper response_factory = mock.Mock(return_value=mock.sentinel.response) mixin.response_class = response_factory response_kwargs = { "dummy": "value", "file_instance": mock.sentinel.file_wrapper, "attachment": True, "basename": None, "file_mimetype": None, "file_encoding": None, } response = mixin.download_response(**response_kwargs) self.assertIs(response, mock.sentinel.response) response_factory.assert_called_once_with(**response_kwargs) # Not args def test_render_to_response_not_modified(self): """DownloadMixin.render_to_response() respects HTTP_IF_MODIFIED_SINCE header (calls ``not_modified_response()``).""" # Setup. mixin = views.DownloadMixin() mixin.request = django.test.RequestFactory().get( "/dummy-url", HTTP_IF_MODIFIED_SINCE=mock.sentinel.http_if_modified_since ) mixin.was_modified_since = mock.Mock(return_value=False) mixin.not_modified_response = mock.Mock( return_value=mock.sentinel.http_not_modified_response ) mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper) # Run. response = mixin.render_to_response() # Check. self.assertIs(response, mock.sentinel.http_not_modified_response) mixin.get_file.assert_called_once_with() mixin.was_modified_since.assert_called_once_with( mock.sentinel.file_wrapper, mock.sentinel.http_if_modified_since ) mixin.not_modified_response.assert_called_once_with() def test_render_to_response_modified(self): """DownloadMixin.render_to_response() calls download_response().""" # Setup. mixin = views.DownloadMixin() mixin.request = django.test.RequestFactory().get( "/dummy-url", HTTP_IF_MODIFIED_SINCE=None ) mixin.was_modified_since = mock.Mock() mixin.download_response = mock.Mock( return_value=mock.sentinel.download_response ) mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper) # Run. response = mixin.render_to_response() # Check. self.assertIs(response, mock.sentinel.download_response) mixin.get_file.assert_called_once_with() self.assertEqual(mixin.was_modified_since.call_count, 0) mixin.download_response.assert_called_once_with() def test_render_to_response_file_not_found(self): "DownloadMixin.render_to_response() calls file_not_found_response()." # Setup. mixin = views.DownloadMixin() mixin.request = django.test.RequestFactory().get("/dummy-url") mixin.get_file = mock.Mock(side_effect=exceptions.FileNotFound) mixin.file_not_found_response = mock.Mock() # Run. mixin.render_to_response() # Check. mixin.file_not_found_response.assert_called_once_with() def test_file_not_found_response(self): """DownloadMixin.file_not_found_response() raises Http404.""" mixin = views.DownloadMixin() with self.assertRaises(Http404): mixin.file_not_found_response() class BaseDownloadViewTestCase(unittest.TestCase): "Tests around :class:`django_downloadviews.views.base.BaseDownloadView`." def test_get(self): """BaseDownloadView.get() calls render_to_response().""" request = django.test.RequestFactory().get("/dummy-url") args = ["dummy-arg"] kwargs = {"dummy": "kwarg"} view = setup_view(views.BaseDownloadView(), request, *args, **kwargs) view.render_to_response = mock.Mock(return_value=mock.sentinel.response) response = view.get(request, *args, **kwargs) self.assertIs(response, mock.sentinel.response) view.render_to_response.assert_called_once_with() class PathDownloadViewTestCase(unittest.TestCase): "Tests for :class:`django_downloadviews.views.path.PathDownloadView`." def test_get_file_ok(self): "PathDownloadView.get_file() returns ``File`` instance." view = setup_view(views.PathDownloadView(path=__file__), "fake request") file_wrapper = view.get_file() self.assertTrue(isinstance(file_wrapper, File)) def test_get_file_does_not_exist(self): """PathDownloadView.get_file() raises FileNotFound if field does not exist. """ view = setup_view(views.PathDownloadView(path="i-do-no-exist"), "fake request") with self.assertRaises(exceptions.FileNotFound): view.get_file() def test_get_file_is_directory(self): """PathDownloadView.get_file() raises FileNotFound if file is a directory.""" view = setup_view( views.PathDownloadView(path=os.path.dirname(__file__)), "fake request" ) with self.assertRaises(exceptions.FileNotFound): view.get_file() class ObjectDownloadViewTestCase(unittest.TestCase): "Tests for :class:`django_downloadviews.views.object.ObjectDownloadView`." def test_get_file_ok(self): "ObjectDownloadView.get_file() returns ``file`` field by default." view = setup_view(views.ObjectDownloadView(), "fake request") view.object = mock.Mock(spec=["file"]) view.get_file() def test_get_file_wrong_field(self): """ObjectDownloadView.get_file() raises AttributeError if field does not exist. ``AttributeError`` is expected because this is a configuration error, i.e. it is related to Python code. """ view = setup_view( views.ObjectDownloadView(file_field="other_field"), "fake request" ) view.object = mock.Mock(spec=["file"]) with self.assertRaises(AttributeError): view.get_file() def test_get_file_empty_field(self): """ObjectDownloadView.get_file() raises FileNotFound if field does not exist.""" view = setup_view( views.ObjectDownloadView(file_field="other_field"), "fake request" ) view.object = mock.Mock() view.object.other_field = None with self.assertRaises(exceptions.FileNotFound): view.get_file() class VirtualDownloadViewTestCase(unittest.TestCase): """Test suite around :py:class:`django_downloadview.views.VirtualDownloadView`.""" def test_was_modified_since_specific(self): """VirtualDownloadView.was_modified_since() delegates to file wrapper.""" file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock( return_value=mock.sentinel.from_file_wrapper ) view = views.VirtualDownloadView() since = mock.sentinel.since return_value = view.was_modified_since(file_wrapper, since) self.assertTrue(return_value is mock.sentinel.from_file_wrapper) file_wrapper.was_modified_since.assert_called_once_with(since) def test_was_modified_since_not_implemented(self): """VirtualDownloadView.was_modified_since() returns True if file wrapper does not implement ``was_modified_since()``.""" file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock(side_effect=AttributeError) modified_time = mock.PropertyMock() setattr(file_wrapper, "modified_time", modified_time) size = mock.PropertyMock() setattr(file_wrapper, "size", size) view = views.VirtualDownloadView() since = mock.sentinel.since result = view.was_modified_since(file_wrapper, since) self.assertTrue(result is True) self.assertFalse(modified_time.called) self.assertFalse(size.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1722862269.0 django_downloadview-2.4.0/tox.ini0000644000175100001770000000306300000000000017664 0ustar00runnerdocker00000000000000[tox] envlist = py{38,39,310,311,312}-dj42 py{310,311,312}-dj{50,main} lint sphinx readme [gh-actions] python = 3.8: py38, lint, sphinx, readme 3.9: py39 3.10: py310 3.11: py311 3.12: py312 [gh-actions:env] DJANGO = 4.2: dj42 5.0: dj50 main: djmain [testenv] deps = coverage dj42: Django>=4.2,<5.0 dj50: Django>=5.0,<5.1 djmain: https://github.com/django/django/archive/main.tar.gz pytest pytest-cov commands = pip install -e . pip install -e demo # doctests and unit tests pytest --cov=django_downloadview --cov=demoproject {posargs} # demo project integration tests coverage run --append {envbindir}/demo test {posargs: demoproject} coverage xml pip freeze ignore_outcome = djmain: True [testenv:lint] deps = flake8 black isort commands = flake8 demo django_downloadview tests black --check demo django_downloadview tests isort --check-only --recursive demo django_downloadview tests [testenv:sphinx] deps = Sphinx commands = pip install -e . make --directory=docs SPHINXOPTS='-W' clean {posargs:html doctest linkcheck} whitelist_externals = make [testenv:readme] description = Ensure README renders on PyPI deps = twine commands = {envpython} setup.py -q sdist bdist_wheel twine check dist/* [flake8] max-line-length = 88 ignore = E203, W503 [coverage:run] source = django_downloadview,demo [pytest] DJANGO_SETTINGS_MODULE = demoproject.settings addopts = --doctest-modules --ignore=docs/ python_files = tests/*.py