././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9324439 django-downloadview-2.3.0/0000755000175100001710000000000000000000000016256 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9164436 django-downloadview-2.3.0/.github/0000755000175100001710000000000000000000000017616 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9204435 django-downloadview-2.3.0/.github/workflows/0000755000175100001710000000000000000000000021653 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/.github/workflows/release.yml0000644000175100001710000000176200000000000024024 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=1641894155.0 django-downloadview-2.3.0/.github/workflows/test.yml0000644000175100001710000000346400000000000023364 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.6', '3.7', '3.8', '3.9', '3.10'] django-version: ['2.2', '3.1', '3.2', '4.0', 'main'] exclude: # Django prior to 3.2 does not support Python 3.10 - django-version: '2.2' python-version: '3.10' - django-version: '3.1' python-version: '3.10' # Django after 3.2 dropped support for Python prior to 3.8 - django-version: '4.0' python-version: '3.6' - django-version: 'main' python-version: '3.6' - django-version: '4.0' python-version: '3.7' - django-version: 'main' python-version: '3.7' 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=1641894155.0 django-downloadview-2.3.0/.gitignore0000644000175100001710000000047500000000000020254 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=1641894155.0 django-downloadview-2.3.0/.isort.cfg0000644000175100001710000000067000000000000020160 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=1641894155.0 django-downloadview-2.3.0/AUTHORS0000644000175100001710000000166500000000000017336 0ustar00runnerdocker00000000000000###################### Authors & contributors ###################### Maintainer: Benoît Bryon Original code by `PeopleDoc `_ team: * Nicolas Tobo * Lauréline Guérin * Gregory Tappero * Rémy Hubscher * Benoît Bryon * Aleksi Häkli * Johnt Hagen * Fabre Florian * Peter Marheine * Hasan Ramezani * Jannis Leidel * Erik Dykema * Nikhil Benesch * Omer Katz * René Leonhardt * Adam Chainz * Martin Bächtold * Tim Gates * zero13cool ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/CHANGELOG0000644000175100001710000001652600000000000017502 0ustar00runnerdocker00000000000000Changelog ========= This document describes changes between past releases. For information about future releases, check `milestones`_ and :doc:`/about/vision`. 2.3 (unreleased) ---------------- - Drop Django 3.0 support - Add Django 3.2 support - Add support for Python 3.10 - Add support for Django 4.0 2.2 (unreleased) ---------------- - Remove support for Python 3.5 and Django 1.11 - Add support for Python 3.9 and Django 3.1 - Remove old urls syntax and adopt the new one - 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=1641894155.0 django-downloadview-2.3.0/CONTRIBUTING.rst0000644000175100001710000000532400000000000020723 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=1641894155.0 django-downloadview-2.3.0/INSTALL0000644000175100001710000000336000000000000017311 0ustar00runnerdocker00000000000000####### Install ####### .. note:: If you want to install a development environment, please see :doc:`/contributing`. ************ Requirements ************ `django-downloadview` has been tested with `Python`_ 3.6, 3.7 and 3.8. Other versions may work, but they are not part of the test suite at the moment. 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=1641894155.0 django-downloadview-2.3.0/LICENSE0000644000175100001710000000276100000000000017271 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=1641894155.0 django-downloadview-2.3.0/MANIFEST.in0000644000175100001710000000025300000000000020014 0ustar00runnerdocker00000000000000recursive-include django_downloadview * global-exclude *.pyc include AUTHORS include CHANGELOG include CONTRIBUTING.rst include INSTALL include LICENSE include README.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/Makefile0000644000175100001710000000411500000000000017717 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=1641894166.9324439 django-downloadview-2.3.0/PKG-INFO0000644000175100001710000000705300000000000017360 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: django-downloadview Version: 2.3.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 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.1 Classifier: Framework :: Django :: 3.2 Classifier: Framework :: Django :: 4.0 Description-Content-Type: text/x-rst Provides-Extra: test License-File: LICENSE License-File: AUTHORS ################### 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=1641894155.0 django-downloadview-2.3.0/README.rst0000644000175100001710000000475500000000000017760 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=1641894166.9204435 django-downloadview-2.3.0/demo/0000755000175100001710000000000000000000000017202 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/README.rst0000644000175100001710000000305100000000000020670 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.6+, 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=1641894166.9204435 django-downloadview-2.3.0/demo/demoproject/0000755000175100001710000000000000000000000021515 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/__init__.py0000644000175100001710000000000000000000000023614 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9204435 django-downloadview-2.3.0/demo/demoproject/apache/0000755000175100001710000000000000000000000022736 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/apache/__init__.py0000644000175100001710000000003400000000000025044 0ustar00runnerdocker00000000000000"""Apache optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/apache/models.py0000644000175100001710000000005500000000000024573 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/apache/tests.py0000644000175100001710000000263400000000000024457 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", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/apache/urls.py0000644000175100001710000000060200000000000024273 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", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/apache/views.py0000644000175100001710000000124300000000000024445 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/", ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9204435 django-downloadview-2.3.0/demo/demoproject/fixtures/0000755000175100001710000000000000000000000023366 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/fixtures/demo.json0000644000175100001710000000026600000000000025211 0ustar00runnerdocker00000000000000[ { "pk": 1, "model": "object.document", "fields": { "slug": "hello-world", "file": "object/hello-world.txt" } } ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/fixtures/hello-world.txt0000644000175100001710000000001500000000000026353 0ustar00runnerdocker00000000000000Hello world! ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9204435 django-downloadview-2.3.0/demo/demoproject/http/0000755000175100001710000000000000000000000022474 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/http/__init__.py0000644000175100001710000000030600000000000024604 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/http/models.py0000644000175100001710000000005500000000000024331 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/http/tests.py0000644000175100001710000000150500000000000024211 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/http/urls.py0000644000175100001710000000034200000000000024032 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/http/views.py0000644000175100001710000000122100000000000024177 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=1641894166.9204435 django-downloadview-2.3.0/demo/demoproject/lighttpd/0000755000175100001710000000000000000000000023334 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/lighttpd/__init__.py0000644000175100001710000000003600000000000025444 0ustar00runnerdocker00000000000000"""Lighttpd optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/lighttpd/models.py0000644000175100001710000000005500000000000025171 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/lighttpd/tests.py0000644000175100001710000000265400000000000025057 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", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/lighttpd/urls.py0000644000175100001710000000060600000000000024675 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", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/lighttpd/views.py0000644000175100001710000000125300000000000025044 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/", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/manage.py0000755000175100001710000000042600000000000023324 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=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/nginx/0000755000175100001710000000000000000000000022640 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/nginx/__init__.py0000644000175100001710000000003300000000000024745 0ustar00runnerdocker00000000000000"""Nginx optimizations.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/nginx/models.py0000644000175100001710000000005500000000000024475 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/nginx/tests.py0000644000175100001710000000322000000000000024351 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, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/nginx/urls.py0000644000175100001710000000060100000000000024174 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", ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/nginx/views.py0000644000175100001710000000125300000000000024350 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/", ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/object/0000755000175100001710000000000000000000000022763 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/object/__init__.py0000644000175100001710000000031200000000000025070 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/object/models.py0000644000175100001710000000037100000000000024621 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/object/tests.py0000644000175100001710000000522100000000000024477 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/object/urls.py0000644000175100001710000000124100000000000024320 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/object/views.py0000644000175100001710000000133100000000000024470 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=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/path/0000755000175100001710000000000000000000000022451 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/path/__init__.py0000644000175100001710000000030600000000000024561 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/path/models.py0000644000175100001710000000005500000000000024306 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/path/tests.py0000644000175100001710000000175100000000000024171 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/path/urls.py0000644000175100001710000000050300000000000024006 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/path/views.py0000644000175100001710000000216300000000000024162 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(DynamicPathDownloadView, self).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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/settings.py0000644000175100001710000001050700000000000023732 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=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/storage/0000755000175100001710000000000000000000000023161 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/__init__.py0000644000175100001710000000031400000000000025270 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/models.py0000644000175100001710000000005500000000000025016 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/storage.py0000644000175100001710000000012700000000000025177 0ustar00runnerdocker00000000000000from django.core.files.storage import FileSystemStorage storage = FileSystemStorage() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/tests.py0000644000175100001710000000721200000000000024677 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, HTTP_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, HTTP_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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/urls.py0000644000175100001710000000062200000000000024520 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/storage/views.py0000644000175100001710000000104700000000000024672 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(DynamicStorageDownloadView, self).get_path().upper() dynamic_path = DynamicStorageDownloadView.as_view(storage=storage) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/templates/0000755000175100001710000000000000000000000023513 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/templates/home.html0000644000175100001710000000160000000000000025326 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/tests.py0000644000175100001710000000055700000000000023240 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/urls.py0000644000175100001710000000224400000000000023056 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=1641894166.9244437 django-downloadview-2.3.0/demo/demoproject/virtual/0000755000175100001710000000000000000000000023203 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/virtual/__init__.py0000644000175100001710000000031400000000000025312 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/virtual/models.py0000644000175100001710000000005500000000000025040 0ustar00runnerdocker00000000000000"""Required to make a Django application.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/demo/demoproject/virtual/tests.py0000644000175100001710000000261100000000000024717 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/virtual/urls.py0000644000175100001710000000051500000000000024543 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/virtual/views.py0000644000175100001710000000164600000000000024721 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=1641894155.0 django-downloadview-2.3.0/demo/demoproject/wsgi.py0000755000175100001710000000221200000000000023040 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=1641894155.0 django-downloadview-2.3.0/demo/setup.py0000644000175100001710000000153100000000000020714 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=1641894166.9244437 django-downloadview-2.3.0/django_downloadview/0000755000175100001710000000000000000000000022302 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/__init__.py0000644000175100001710000000036700000000000024421 0ustar00runnerdocker00000000000000"""Serve files with Django and reverse proxies.""" from django_downloadview.api import * # NoQA import pkg_resources #: Module version, as defined in PEP-0396. __version__ = pkg_resources.get_distribution(__package__.replace("-", "_")).version ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9284437 django-downloadview-2.3.0/django_downloadview/apache/0000755000175100001710000000000000000000000023523 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/apache/__init__.py0000644000175100001710000000101100000000000025625 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/apache/decorators.py0000644000175100001710000000102100000000000026234 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/apache/middlewares.py0000644000175100001710000000246200000000000026401 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(XSendfileMiddleware, self).__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, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/apache/response.py0000644000175100001710000000125700000000000025740 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): """Return a HttpResponse with headers for Apache X-Sendfile.""" super(XSendfileResponse, self).__init__(content_type=content_type) 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/apache/tests.py0000644000175100001710000000412700000000000025243 0ustar00runnerdocker00000000000000from django_downloadview.apache.response import XSendfileResponse class XSendfileValidator(object): """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=1641894155.0 django-downloadview-2.3.0/django_downloadview/api.py0000644000175100001710000000150700000000000023430 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/decorators.py0000644000175100001710000000472100000000000025025 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/exceptions.py0000644000175100001710000000036300000000000025037 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/files.py0000644000175100001710000001622600000000000023765 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(VirtualFile, self).__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(VirtualFile, self)._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=1641894155.0 django-downloadview-2.3.0/django_downloadview/io.py0000644000175100001710000000737700000000000023301 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=1641894166.9284437 django-downloadview-2.3.0/django_downloadview/lighttpd/0000755000175100001710000000000000000000000024121 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/lighttpd/__init__.py0000644000175100001710000000107500000000000026235 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/lighttpd/decorators.py0000644000175100001710000000102700000000000026640 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/lighttpd/middlewares.py0000644000175100001710000000246400000000000027001 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(XSendfileMiddleware, self).__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, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/lighttpd/response.py0000644000175100001710000000126500000000000026335 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): """Return a HttpResponse with headers for Lighttpd X-Sendfile.""" super(XSendfileResponse, self).__init__(content_type=content_type) 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/lighttpd/tests.py0000644000175100001710000000167000000000000025641 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/middlewares.py0000644000175100001710000002074700000000000025166 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. """ if super(RealDownloadMiddleware, self).is_download_response(response): try: return response.file.url or response.file.name except AttributeError: return False else: return True return False 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(DownloadDispatcherMiddleware, self).__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(SmartDownloadMiddleware, self).__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(ProxiedDownloadMiddleware, self).__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=1641894166.9284437 django-downloadview-2.3.0/django_downloadview/nginx/0000755000175100001710000000000000000000000023425 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/__init__.py0000644000175100001710000000100700000000000025534 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/decorators.py0000644000175100001710000000104000000000000026137 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/middlewares.py0000644000175100001710000001173400000000000026305 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(XAccelRedirectMiddleware, self).__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, ) 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(SingleXAccelRedirectMiddleware, self).__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=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/response.py0000644000175100001710000000301000000000000025627 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, ): """Return a HttpResponse with headers for Nginx X-Accel-Redirect.""" super(XAccelRedirectResponse, self).__init__(content_type=content_type) 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/settings.py0000644000175100001710000001164500000000000025646 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" def get_middlewares(): try: return settings.MIDDLEWARE except AttributeError: return settings.MIDDLEWARE_CLASSES if deprecated_middleware in get_middlewares(): 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/nginx/tests.py0000644000175100001710000001106200000000000025141 0ustar00runnerdocker00000000000000from django_downloadview.nginx.response import XAccelRedirectResponse class XAccelRedirectValidator(object): """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=1641894155.0 django-downloadview-2.3.0/django_downloadview/response.py0000644000175100001710000002037100000000000024515 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_filename = encode_basename_ascii(filename) 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(DownloadResponse, self).__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 items(self): """Return iterable of (header, value). This method is called by http handlers just before WSGI's start_response() is called... but it is not called by django.test.ClientHandler! :'( """ return super(DownloadResponse, self).items() def get_basename(self): """Return basename.""" if self.basename: 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/shortcuts.py0000644000175100001710000000121400000000000024710 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/storage.py0000644000175100001710000000124200000000000024317 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(SignedURLMixin, self).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=1641894155.0 django-downloadview-2.3.0/django_downloadview/test.py0000644000175100001710000001355500000000000023644 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(temporary_media_root, self).enable() def disable(self): """Remove directory settings.MEDIA_ROOT then restore original setting.""" shutil.rmtree(settings.MEDIA_ROOT) super(temporary_media_root, self).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=1641894155.0 django-downloadview-2.3.0/django_downloadview/utils.py0000644000175100001710000000237700000000000024025 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=1641894166.9284437 django-downloadview-2.3.0/django_downloadview/views/0000755000175100001710000000000000000000000023437 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/__init__.py0000644000175100001710000000074500000000000025556 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/base.py0000644000175100001710000001404000000000000024722 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 and a size. It is passed ``modified_time`` and ``size`` attributes from file wrapper. If file wrapper does not support these attributes (``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() ) size = file_instance.size except (AttributeError, NotImplementedError) as e: print("!=======!", e) return True else: return was_modified_since(since, modification_time, size) 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.META.get("HTTP_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=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/http.py0000644000175100001710000000246100000000000024773 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/object.py0000644000175100001710000000712000000000000025257 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(ObjectDownloadView, self).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(ObjectDownloadView, self).get(request, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/path.py0000644000175100001710000000234300000000000024747 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=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/storage.py0000644000175100001710000000165100000000000025460 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_path(self): """Return path of the file to serve, relative to storage. Default implementation simply returns view's :py:attr:`path` attribute. Override this method if you want custom implementation. """ return super(StorageDownloadView, self).get_path() def get_file(self): """Return :class:`~django_downloadview.files.StorageFile` instance.""" return StorageFile(self.storage, self.get_path()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/django_downloadview/views/virtual.py0000644000175100001710000000240500000000000025500 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=1641894166.9244437 django-downloadview-2.3.0/django_downloadview.egg-info/0000755000175100001710000000000000000000000023774 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/PKG-INFO0000644000175100001710000000705300000000000025076 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: django-downloadview Version: 2.3.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 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.1 Classifier: Framework :: Django :: 3.2 Classifier: Framework :: Django :: 4.0 Description-Content-Type: text/x-rst Provides-Extra: test License-File: LICENSE License-File: AUTHORS ################### 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=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/SOURCES.txt0000644000175100001710000001005700000000000025663 0ustar00runnerdocker00000000000000.gitignore .isort.cfg AUTHORS CHANGELOG 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=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/dependency_links.txt0000644000175100001710000000000100000000000030042 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/not-zip-safe0000644000175100001710000000000100000000000026222 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/requires.txt0000644000175100001710000000004100000000000026367 0ustar00runnerdocker00000000000000Django>=2.2 requests [test] tox ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894166.0 django-downloadview-2.3.0/django_downloadview.egg-info/top_level.txt0000644000175100001710000000002400000000000026522 0ustar00runnerdocker00000000000000django_downloadview ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9284437 django-downloadview-2.3.0/docs/0000755000175100001710000000000000000000000017206 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/Makefile0000644000175100001710000001276500000000000020661 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=1641894166.9284437 django-downloadview-2.3.0/docs/about/0000755000175100001710000000000000000000000020320 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/about/alternatives.txt0000644000175100001710000000524300000000000023566 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=1641894155.0 django-downloadview-2.3.0/docs/about/authors.txt0000644000175100001710000000003300000000000022542 0ustar00runnerdocker00000000000000.. include:: ../../AUTHORS ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/about/changelog.txt0000644000175100001710000000003500000000000023006 0ustar00runnerdocker00000000000000.. include:: ../../CHANGELOG ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/about/index.txt0000644000175100001710000000023200000000000022165 0ustar00runnerdocker00000000000000######################### About django-downloadview ######################### .. toctree:: vision alternatives license authors changelog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/about/license.txt0000644000175100001710000000003300000000000022477 0ustar00runnerdocker00000000000000.. include:: ../../LICENSE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/about/vision.txt0000644000175100001710000000122600000000000022371 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=1641894155.0 django-downloadview-2.3.0/docs/conf.py0000644000175100001710000001017000000000000020504 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- """django-downloadview documentation build configuration file.""" import os import re from pkg_resources import get_distribution # 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 = get_distribution("django-downloadview").version # 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=1641894155.0 django-downloadview-2.3.0/docs/contributing.txt0000644000175100001710000000004100000000000022451 0ustar00runnerdocker00000000000000.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/demo.txt0000644000175100001710000000004000000000000020665 0ustar00runnerdocker00000000000000.. include:: ../demo/README.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/django-sendfile.txt0000644000175100001710000000362700000000000023010 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_CLASSES``. 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=1641894155.0 django-downloadview-2.3.0/docs/files.txt0000644000175100001710000000610200000000000021050 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=1641894155.0 django-downloadview-2.3.0/docs/healthchecks.txt0000644000175100001710000000640200000000000022377 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=1641894155.0 django-downloadview-2.3.0/docs/index.txt0000644000175100001710000000043300000000000021056 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=1641894155.0 django-downloadview-2.3.0/docs/install.txt0000644000175100001710000000003000000000000021406 0ustar00runnerdocker00000000000000.. include:: ../INSTALL ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9324439 django-downloadview-2.3.0/docs/optimizations/0000755000175100001710000000000000000000000022117 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/optimizations/apache.txt0000644000175100001710000000663600000000000024114 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_CLASSES`` 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=1641894155.0 django-downloadview-2.3.0/docs/optimizations/index.txt0000644000175100001710000000712600000000000023775 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=1641894155.0 django-downloadview-2.3.0/docs/optimizations/lighttpd.txt0000644000175100001710000000753000000000000024504 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_CLASSES`` 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=1641894155.0 django-downloadview-2.3.0/docs/optimizations/nginx.txt0000644000175100001710000001222600000000000024006 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=1641894155.0 django-downloadview-2.3.0/docs/overview.txt0000644000175100001710000000630500000000000021621 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=1641894155.0 django-downloadview-2.3.0/docs/responses.txt0000644000175100001710000000114500000000000021771 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=1641894155.0 django-downloadview-2.3.0/docs/settings.txt0000644000175100001710000000717000000000000021614 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_CLASSES ****************** If you plan to setup :doc:`reverse-proxy optimizations `, add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``. 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_CLASSES``, 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_CLASSES``, this setting must be explicitely configured (no default value). Else, you can ignore this setting.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/testing.txt0000644000175100001710000000201000000000000021415 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=1641894166.9324439 django-downloadview-2.3.0/docs/views/0000755000175100001710000000000000000000000020343 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/docs/views/custom.txt0000644000175100001710000000564300000000000022426 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=1641894155.0 django-downloadview-2.3.0/docs/views/http.txt0000644000175100001710000000206200000000000022063 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=1641894155.0 django-downloadview-2.3.0/docs/views/index.txt0000644000175100001710000000105000000000000022207 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=1641894155.0 django-downloadview-2.3.0/docs/views/object.txt0000644000175100001710000000546700000000000022366 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=1641894155.0 django-downloadview-2.3.0/docs/views/path.txt0000644000175100001710000000317600000000000022047 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=1641894155.0 django-downloadview-2.3.0/docs/views/storage.txt0000644000175100001710000000313000000000000022545 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=1641894155.0 django-downloadview-2.3.0/docs/views/virtual.txt0000644000175100001710000000435000000000000022574 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=1641894166.9324439 django-downloadview-2.3.0/setup.cfg0000644000175100001710000000004600000000000020077 0ustar00runnerdocker00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/setup.py0000644000175100001710000000367600000000000020004 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.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', ], 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, install_requires=[ # BEGIN requirements "Django>=2.2", "requests", # END requirements ], extras_require={ "test": ["tox"], }, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1641894166.9324439 django-downloadview-2.3.0/tests/0000755000175100001710000000000000000000000017420 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/tests/__init__.py0000644000175100001710000000000000000000000021517 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/tests/api.py0000644000175100001710000001267600000000000020557 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_CLASSES=["django_downloadview.nginx.XAccelRedirectMiddleware"], 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 == 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=1641894155.0 django-downloadview-2.3.0/tests/io.py0000644000175100001710000000305300000000000020402 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=1641894155.0 django-downloadview-2.3.0/tests/packaging.py0000644000175100001710000000320700000000000021720 0ustar00runnerdocker00000000000000"""Tests around project's distribution and packaging.""" 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 pkg_resources info.""" try: import pkg_resources except ImportError: self.fail( "Cannot import pkg_resources module. It is part of " "setuptools, which is a dependency of " "django_downloadview." ) distribution = pkg_resources.get_distribution("django-downloadview") installed_version = distribution.version self.assertEqual( installed_version, self.get_version(), "Version mismatch: django_downloadview.__version__ " 'is "%s" whereas pkg_resources 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=1641894155.0 django-downloadview-2.3.0/tests/response.py0000644000175100001710000000132200000000000021626 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"] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1641894155.0 django-downloadview-2.3.0/tests/sendfile.py0000644000175100001710000000321500000000000021564 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=1641894155.0 django-downloadview-2.3.0/tests/signature.py0000644000175100001710000000314700000000000022000 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=1641894155.0 django-downloadview-2.3.0/tests/views.py0000644000175100001710000003237300000000000021137 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 ``size`` and ``modified_time`` to :func:`django.views.static import was_modified_since`. """ file_wrapper = mock.Mock() file_wrapper.was_modified_since = mock.Mock(side_effect=AttributeError) file_wrapper.size = mock.sentinel.size file_wrapper.modified_time = datetime.now() was_modified_since_mock = mock.Mock(return_value=mock.sentinel.was_modified) mixin = views.DownloadMixin() 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()), mock.sentinel.size, ) 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 ``size`` and ``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=1641894155.0 django-downloadview-2.3.0/tox.ini0000644000175100001710000000313000000000000017566 0ustar00runnerdocker00000000000000[tox] envlist = py{36,37,38,39,310}-dj{22,31,32} py{38,39,310}-dj{40,main} lint sphinx readme [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38, lint, sphinx, readme 3.9: py39 3.10: py310 [gh-actions:env] DJANGO = 2.2: dj22 3.1: dj31 3.2: dj32 4.0: dj40 main: djmain [testenv] deps = coverage dj22: Django>=2.2,<3.0 dj31: Django>=3.1,<3.2 dj32: Django>=3.2,<3.3 dj40: Django>=4.0,<4.1 djmain: https://github.com/django/django/archive/main.tar.gz pytest pytest-cov commands = pip install -e . pip install -e demo # doctests pytest --cov=django_downloadview --cov=demoproject {posargs} # all other test cases coverage run --append {envbindir}/demo test {posargs: tests 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/