django-downloadview-1.9/0000755000175000017500000000000012671767433016127 5ustar benoitbenoit00000000000000django-downloadview-1.9/INSTALL0000644000175000017500000000336012671767432017161 0ustar benoitbenoit00000000000000####### Install ####### .. note:: If you want to install a development environment, please see :doc:`/contributing`. ************ Requirements ************ `django-downloadview` has been tested with `Python`_ 2.7, 3.3 and 3.4. 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/ django-downloadview-1.9/CHANGELOG0000644000175000017500000001500412671767432017340 0ustar benoitbenoit00000000000000Changelog ========= This document describes changes between past releases. For information about future releases, check `milestones`_ and :doc:`/about/vision`. 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/benoitbryon/django-downloadview/milestones django-downloadview-1.9/MANIFEST.in0000644000175000017500000000027312671767432017666 0ustar benoitbenoit00000000000000recursive-include django_downloadview * global-exclude *.pyc include AUTHORS include CHANGELOG include CONTRIBUTING.rst include INSTALL include LICENSE include README.rst include VERSION django-downloadview-1.9/setup.cfg0000644000175000017500000000007312671767433017750 0ustar benoitbenoit00000000000000[egg_info] tag_date = 0 tag_svn_revision = 0 tag_build = django-downloadview-1.9/AUTHORS0000644000175000017500000000075612671767432017206 0ustar benoitbenoit00000000000000###################### Authors & contributors ###################### Maintainer: Benoît Bryon Original code by `Novapost `_ team: * Nicolas Tobo * Lauréline Guérin * Gregory Tappero * Rémy Hubscher * Benoît Bryon Developers: https://github.com/benoitbryon/django-downloadview/graphs/contributors django-downloadview-1.9/django_downloadview.egg-info/0000755000175000017500000000000012671767433023645 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview.egg-info/not-zip-safe0000644000175000017500000000000112671767433026073 0ustar benoitbenoit00000000000000 django-downloadview-1.9/django_downloadview.egg-info/requires.txt0000644000175000017500000000006012671767433026241 0ustar benoitbenoit00000000000000Django>=1.5 requests setuptools six [test] tox django-downloadview-1.9/django_downloadview.egg-info/SOURCES.txt0000644000175000017500000000304112671767433025527 0ustar benoitbenoit00000000000000AUTHORS CHANGELOG CONTRIBUTING.rst INSTALL LICENSE MANIFEST.in README.rst VERSION setup.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/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.pydjango-downloadview-1.9/django_downloadview.egg-info/dependency_links.txt0000644000175000017500000000000112671767433027713 0ustar benoitbenoit00000000000000 django-downloadview-1.9/django_downloadview.egg-info/top_level.txt0000644000175000017500000000002412671767433026373 0ustar benoitbenoit00000000000000django_downloadview django-downloadview-1.9/django_downloadview.egg-info/PKG-INFO0000644000175000017500000000520012671767433024737 0ustar benoitbenoit00000000000000Metadata-Version: 1.1 Name: django-downloadview Version: 1.9 Summary: Serve files with Django and reverse-proxies. Home-page: https://django-downloadview.readthedocs.org/ Author: Benoît Bryon Author-email: benoit@marmelune.net License: BSD Description: ################### django-downloadview ################### `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: http://django-downloadview.readthedocs.org * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/benoitbryon/django-downloadview * Bugtracker: https://github.com/benoitbryon/django-downloadview/issues * Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview * Roadmap: https://github.com/benoitbryon/django-downloadview/milestones .. _`Django`: https://djangoproject.com 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: Framework :: Django Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 django-downloadview-1.9/LICENSE0000644000175000017500000000276112671767432017141 0ustar benoitbenoit00000000000000####### 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. django-downloadview-1.9/CONTRIBUTING.rst0000644000175000017500000000457712671767432020604 0ustar benoitbenoit00000000000000############ Contributing ############ 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:benoitbryon/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/benoitbryon/django-downloadview/issues .. _`rebase`: http://git-scm.com/book/en/Git-Branching-Rebasing .. _`merge-based rebase`: http://tech.novapost.fr/psycho-rebasing-en.html .. _`pip`: https://pypi.python.org/pypi/pip/ .. _`tox`: http://tox.testrun.org .. _`Sphinx`: https://pypi.python.org/pypi/Sphinx/ .. _`zest.releaser`: https://pypi.python.org/pypi/zest.releaser/ django-downloadview-1.9/setup.py0000644000175000017500000000521412671767432017642 0ustar benoitbenoit00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Python packaging.""" import os import sys from setuptools import setup from setuptools.command.test import test as TestCommand class Tox(TestCommand): """Test command that runs tox.""" def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): import tox # import here, cause outside the eggs aren't loaded. errno = tox.cmdline(self.test_args) sys.exit(errno) #: Absolute path to directory containing setup.py file. here = os.path.abspath(os.path.dirname(__file__)) #: Boolean, ``True`` if environment is running Python version 2. IS_PYTHON2 = sys.version_info[0] == 2 NAME = 'django-downloadview' DESCRIPTION = 'Serve files with Django and reverse-proxies.' README = open(os.path.join(here, 'README.rst')).read() VERSION = open(os.path.join(here, 'VERSION')).read().strip() AUTHOR = u'Benoît Bryon' EMAIL = 'benoit@marmelune.net' LICENSE = 'BSD' URL = 'https://{name}.readthedocs.org/'.format(name=NAME) CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ] KEYWORDS = ['file', 'stream', 'download', 'FileField', 'ImageField', 'x-accel', 'x-accel-redirect', 'x-sendfile', 'sendfile', 'mod_xsendfile', 'offload'] PACKAGES = [NAME.replace('-', '_')] REQUIREMENTS = [ # BEGIN requirements 'Django>=1.5', 'requests', 'setuptools', 'six', # END requirements ] ENTRY_POINTS = {} SETUP_REQUIREMENTS = ['setuptools'] TEST_REQUIREMENTS = ['tox'] CMDCLASS = {'test': Tox} EXTRA_REQUIREMENTS = { 'test': TEST_REQUIREMENTS, } if __name__ == '__main__': # Don't run setup() when we import this module. setup( name=NAME, version=VERSION, description=DESCRIPTION, long_description=README, classifiers=CLASSIFIERS, keywords=' '.join(KEYWORDS), author=AUTHOR, author_email=EMAIL, url=URL, license=LICENSE, packages=PACKAGES, include_package_data=True, zip_safe=False, install_requires=REQUIREMENTS, entry_points=ENTRY_POINTS, tests_require=TEST_REQUIREMENTS, cmdclass=CMDCLASS, setup_requires=SETUP_REQUIREMENTS, extras_require=EXTRA_REQUIREMENTS, ) django-downloadview-1.9/django_downloadview/0000755000175000017500000000000012671767433022153 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview/middlewares.py0000644000175000017500000002033512671767432025027 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Base material for download middlewares. Download middlewares capture :py:class:`django_downloadview.DownloadResponse` responses and may replace them with optimized download responses. """ import copy import collections 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(object): """Base (abstract) Django middleware that handles download responses. Subclasses **must** implement :py:meth:`process_download_response` method. """ 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 DownloadDispatcherMiddleware(BaseDownloadMiddleware): "Download middleware that dispatches job to several middleware instances." 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 process_download_response(self, request, response): """Dispatches job to children middlewares.""" for (key, middleware) in self.middlewares: response = middleware.process_response(request, response) return response class SmartDownloadMiddleware(BaseDownloadMiddleware): """Easy to configure download middleware.""" def __init__(self, backend_factory=AUTO_CONFIGURE, backend_options=AUTO_CONFIGURE): """Constructor.""" #: :class:`DownloadDispatcher` instance that can hold multiple #: backend instances. self.dispatcher = DownloadDispatcherMiddleware(middlewares=[]) #: Callable (typically a class) to instanciate 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 instanciate 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.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)) def process_download_response(self, request, response): """Use :attr:`dispatcher` to process download response.""" return self.dispatcher.process_download_response(request, response) class NoRedirectionMatch(Exception): """Response object does not match redirection rules.""" class ProxiedDownloadMiddleware(RealDownloadMiddleware): """Base class for middlewares that use optimizations of reverse proxies.""" def __init__(self, source_dir=None, source_url=None, destination_url=None): """Constructor.""" 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('/'))) django-downloadview-1.9/django_downloadview/files.py0000644000175000017500000001572512671767432023640 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """File wrappers for use as exchange data between views and responses.""" from __future__ import absolute_import from io import BytesIO from six.moves.urllib.parse import urlparse from django.core.files.base import File from django.utils.encoding import force_bytes import requests from django_downloadview.io import BytesIteratorIO 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). """ return self.storage.accessed(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). """ 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). """ return self.storage.modified_time(self.name) class VirtualFile(File): """Wrapper for files that live in memory.""" def __init__(self, file=None, name=u'', 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=u'', **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'] django-downloadview-1.9/django_downloadview/__init__.py0000644000175000017500000000050012671767432024256 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Serve files with Django and reverse proxies.""" import pkg_resources #: Module version, as defined in PEP-0396. __version__ = pkg_resources.get_distribution(__package__.replace('-', '_')) \ .version # API shortcuts. from django_downloadview.api import * # NoQA django-downloadview-1.9/django_downloadview/test.py0000644000175000017500000001372712671767432023515 0ustar benoitbenoit00000000000000"""Testing utilities.""" import shutil from six import iteritems 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 iteritems(assertions): 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('filename="{name}"'.format( name=ascii_name), response['Content-Disposition']) if check_utf8: test_case.assertIn( "filename*=UTF-8''{name}".format(name=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) django-downloadview-1.9/django_downloadview/apache/0000755000175000017500000000000012671767433023374 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview/apache/middlewares.py0000644000175000017500000000263212671767432026250 0ustar benoitbenoit00000000000000from django_downloadview.apache.response import XSendfileResponse from django_downloadview.middlewares import (ProxiedDownloadMiddleware, NoRedirectionMatch) 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, source_dir=None, source_url=None, destination_dir=None): """Constructor.""" super(XSendfileMiddleware, self).__init__(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) django-downloadview-1.9/django_downloadview/apache/__init__.py0000644000175000017500000000104112671767432025500 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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.response import XSendfileResponse # NoQA from django_downloadview.apache.tests import assert_x_sendfile # NoQA from django_downloadview.apache.middlewares import XSendfileMiddleware # NoQA django-downloadview-1.9/django_downloadview/apache/tests.py0000644000175000017500000000416312671767432025113 0ustar benoitbenoit00000000000000from six import iteritems from 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 iteritems(assertions): 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) django-downloadview-1.9/django_downloadview/apache/decorators.py0000644000175000017500000000105112671767432026107 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Decorators to apply Apache X-Sendfile on a specific view.""" from django_downloadview.decorators import DownloadDecorator from django_downloadview.apache.middlewares import XSendfileMiddleware 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) django-downloadview-1.9/django_downloadview/apache/response.py0000644000175000017500000000140312671767432025601 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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 django-downloadview-1.9/django_downloadview/lighttpd/0000755000175000017500000000000012671767433023772 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview/lighttpd/middlewares.py0000644000175000017500000000263412671767432026650 0ustar benoitbenoit00000000000000from django_downloadview.lighttpd.response import XSendfileResponse from django_downloadview.middlewares import (ProxiedDownloadMiddleware, NoRedirectionMatch) 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, source_dir=None, source_url=None, destination_dir=None): """Constructor.""" super(XSendfileMiddleware, self).__init__(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) django-downloadview-1.9/django_downloadview/lighttpd/__init__.py0000644000175000017500000000112512671767432026101 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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.response import XSendfileResponse # NoQA from django_downloadview.lighttpd.tests import assert_x_sendfile # NoQA from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA django-downloadview-1.9/django_downloadview/lighttpd/tests.py0000644000175000017500000000166712671767432025517 0ustar benoitbenoit00000000000000import 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) django-downloadview-1.9/django_downloadview/lighttpd/decorators.py0000644000175000017500000000105712671767432026513 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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) django-downloadview-1.9/django_downloadview/lighttpd/response.py0000644000175000017500000000141112671767432026176 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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 django-downloadview-1.9/django_downloadview/views/0000755000175000017500000000000012671767433023310 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview/views/__init__.py0000644000175000017500000000110712671767432025417 0ustar benoitbenoit00000000000000# coding=utf-8 """Views.""" # -*- coding: utf-8 -*- """Views to stream files.""" # API shortcuts. from django_downloadview.views.base import (DownloadMixin, # NoQA BaseDownloadView) from django_downloadview.views.path import PathDownloadView # NoQA from django_downloadview.views.storage import StorageDownloadView # NoQA from django_downloadview.views.object import ObjectDownloadView # NoQA from django_downloadview.views.http import HTTPDownloadView # NoQA from django_downloadview.views.virtual import VirtualDownloadView # NoQA django-downloadview-1.9/django_downloadview/views/path.py0000644000175000017500000000240312671767432024614 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """: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('File "{0}" does not exists'.format(filename)) return File(open(filename, 'rb')) django-downloadview-1.9/django_downloadview/views/virtual.py0000644000175000017500000000243412671767432025352 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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 django-downloadview-1.9/django_downloadview/views/object.py0000644000175000017500000000735212671767432025136 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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('Field="{field}" on object="{object}" is ' 'empty'.format( field=self.file_field, object=self.object)) 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) django-downloadview-1.9/django_downloadview/views/http.py0000644000175000017500000000252712671767432024646 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Stream files given an URL, i.e. files you want to proxy.""" import requests from django_downloadview.files import HTTPFile from django_downloadview.views.base import BaseDownloadView class HTTPDownloadView(BaseDownloadView): """Proxy files that live on remote servers.""" #: URL to download (the one we are proxying). url = u'' #: 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()) django-downloadview-1.9/django_downloadview/views/base.py0000644000175000017500000001377212671767432024605 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Base material for download views: :class:`DownloadMixin` and :class:`BaseDownloadView`""" import calendar from django.http import HttpResponseNotModified, Http404 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): 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() django-downloadview-1.9/django_downloadview/views/storage.py0000644000175000017500000000170012671767432025323 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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()) django-downloadview-1.9/django_downloadview/exceptions.py0000644000175000017500000000041312671767432024703 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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). """ django-downloadview-1.9/django_downloadview/decorators.py0000644000175000017500000000244012671767432024671 0ustar benoitbenoit00000000000000"""View decorators. See also decorators provided by server-specific modules, such as :func:`django_downloadview.nginx.x_accel_redirect`. """ 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 django-downloadview-1.9/django_downloadview/io.py0000644000175000017500000000735512671767432023145 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Low-level IO operations, for use with file wrappers.""" from __future__ import absolute_import import io from django.utils.encoding import force_text, force_bytes 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 = u'' 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_text(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.""" l = [] if n is None or n < 0: while True: m = self._read1() if not m: break l.append(m) else: while n > 0: m = self._read1(n) if not m: break n -= len(m) l.append(m) return u''.join(l) def readline(self): l = [] while True: i = self._left.find(u'\n') if i == -1: l.append(self._left) try: self._left = next(self._iter) except StopIteration: self._left = u'' break else: l.append(self._left[:i + 1]) self._left = self._left[i + 1:] break return u''.join(l) 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.""" l = [] if n is None or n < 0: while True: m = self._read1() if not m: break l.append(m) else: while n > 0: m = self._read1(n) if not m: break n -= len(m) l.append(m) return b''.join(l) def readline(self): l = [] while True: i = self._left.find(b'\n') if i == -1: l.append(self._left) try: self._left = next(self._iter) except StopIteration: self._left = b'' break else: l.append(self._left[:i + 1]) self._left = self._left[i + 1:] break return b''.join(l) django-downloadview-1.9/django_downloadview/shortcuts.py0000644000175000017500000000136712671767432024571 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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) django-downloadview-1.9/django_downloadview/utils.py0000644000175000017500000000243012671767432023663 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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) django-downloadview-1.9/django_downloadview/nginx/0000755000175000017500000000000012671767433023276 5ustar benoitbenoit00000000000000django-downloadview-1.9/django_downloadview/nginx/settings.py0000644000175000017500000001167312671767432025517 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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. middleware = 'django_downloadview.nginx.XAccelRedirectMiddleware' if middleware in settings.MIDDLEWARE_CLASSES: raise ImproperlyConfigured( '{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) django-downloadview-1.9/django_downloadview/nginx/middlewares.py0000644000175000017500000001206412671767432026152 0ustar benoitbenoit00000000000000import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django_downloadview.middlewares import (ProxiedDownloadMiddleware, NoRedirectionMatch) 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, 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__(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): """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__( 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) django-downloadview-1.9/django_downloadview/nginx/__init__.py0000644000175000017500000000104612671767432025407 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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.response import XAccelRedirectResponse # NoQA from django_downloadview.nginx.tests import assert_x_accel_redirect # NoQA from django_downloadview.nginx.middlewares import ( # NoQA XAccelRedirectMiddleware) django-downloadview-1.9/django_downloadview/nginx/tests.py0000644000175000017500000001111612671767432025011 0ustar benoitbenoit00000000000000from six import iteritems from 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 iteritems(assertions): 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) django-downloadview-1.9/django_downloadview/nginx/decorators.py0000644000175000017500000000107012671767432026012 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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) django-downloadview-1.9/django_downloadview/nginx/response.py0000644000175000017500000000313412671767432025506 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """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' django-downloadview-1.9/django_downloadview/api.py0000644000175000017500000000257612671767432023307 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """Declaration of API shortcuts.""" from django_downloadview.io import (BytesIteratorIO, # NoQA TextIteratorIO) from django_downloadview.files import (StorageFile, # NoQA VirtualFile, HTTPFile) from django_downloadview.response import (DownloadResponse, # NoQA ProxiedDownloadResponse) from django_downloadview.middlewares import (BaseDownloadMiddleware, # NoQA DownloadDispatcherMiddleware, SmartDownloadMiddleware) from django_downloadview.views import (PathDownloadView, # NoQA ObjectDownloadView, StorageDownloadView, HTTPDownloadView, VirtualDownloadView, BaseDownloadView, DownloadMixin) from django_downloadview.shortcuts import sendfile # NoQA from django_downloadview.test import (assert_download_response, # NoQA setup_view, temporary_media_root) # Backward compatibility. StringIteratorIO = TextIteratorIO django-downloadview-1.9/django_downloadview/response.py0000644000175000017500000002105412671767432024364 0ustar benoitbenoit00000000000000# -*- coding: utf-8 -*- """:py:class:`django.http.HttpResponse` subclasses.""" import os import mimetypes import re import unicodedata import six from six.moves import urllib from django.conf import settings from django.http import HttpResponse, StreamingHttpResponse from django.utils.encoding import force_str def encode_basename_ascii(value): u"""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, six.binary_type): value = value.decode('utf-8') ascii_basename = six.text_type(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): u"""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 urllib.parse.quote(force_str(value)) def content_disposition(filename): u"""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 "attachment; filename=\"{ascii}\"".format(ascii=ascii_filename) else: return "attachment; filename=\"{ascii}\"; filename*=UTF-8''{utf8}" \ .format(ascii=ascii_filename, utf8=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: content_type_template = '{mime_type}; charset={charset}' return content_type_template.format(mime_type=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`. """ django-downloadview-1.9/README.rst0000644000175000017500000000302512671767432017615 0ustar benoitbenoit00000000000000################### django-downloadview ################### `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: http://django-downloadview.readthedocs.org * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/benoitbryon/django-downloadview * Bugtracker: https://github.com/benoitbryon/django-downloadview/issues * Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview * Roadmap: https://github.com/benoitbryon/django-downloadview/milestones .. _`Django`: https://djangoproject.com django-downloadview-1.9/PKG-INFO0000644000175000017500000000520012671767433017221 0ustar benoitbenoit00000000000000Metadata-Version: 1.1 Name: django-downloadview Version: 1.9 Summary: Serve files with Django and reverse-proxies. Home-page: https://django-downloadview.readthedocs.org/ Author: Benoît Bryon Author-email: benoit@marmelune.net License: BSD Description: ################### django-downloadview ################### `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: http://django-downloadview.readthedocs.org * PyPI page: http://pypi.python.org/pypi/django-downloadview * Code repository: https://github.com/benoitbryon/django-downloadview * Bugtracker: https://github.com/benoitbryon/django-downloadview/issues * Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview * Roadmap: https://github.com/benoitbryon/django-downloadview/milestones .. _`Django`: https://djangoproject.com 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: Framework :: Django Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 django-downloadview-1.9/VERSION0000644000175000017500000000000412671767432017170 0ustar benoitbenoit000000000000001.9