├── tox.ini ├── tests ├── __init__.py └── test_fileprovider.py ├── requirements.txt ├── setup.cfg ├── fileprovider ├── __init__.py ├── models.py ├── tests.py ├── admin.py ├── views.py ├── decorators.py ├── utils.py └── middleware.py ├── MANIFEST.in ├── dev-requirements.txt ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── setup.py ├── LICENSE └── README.md /tox.ini: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six>=1.10.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | -------------------------------------------------------------------------------- /fileprovider/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1' 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest 3 | tox 4 | tox-docker 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | _build 3 | .*.sw[po] 4 | *.egg-info 5 | dist 6 | build 7 | .eggs 8 | -------------------------------------------------------------------------------- /fileprovider/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /fileprovider/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /fileprovider/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /fileprovider/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_fileprovider.py: -------------------------------------------------------------------------------- 1 | def test_version(): 2 | # starting django,python pair version test 3 | pass 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "pypy" 9 | install: "pip install -r dev-requirements.txt" 10 | script: python setup.py test 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Guidelines For Contributing Code 2 | ================================ 3 | 4 | If you're ready to contribute back some code/docs, the 5 | process should look like: 6 | 7 | * Fork the project on GitHub into your own account. 8 | * Clone your copy of Tastypie. 9 | * Make a new branch in git & commit your changes there. 10 | * Push your new branch up to GitHub. 11 | * Ensure there isn't already an issue or pull request out there on it. 12 | If you feel you have a better fix, please take note of the issue 13 | number & mention it in your pull request. 14 | * Create a new pull request (based on your branch). 15 | -------------------------------------------------------------------------------- /fileprovider/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from django.http import HttpResponse 4 | 5 | def file_response(func): 6 | @wraps(func) 7 | def provider_response(*args, **kwargs): 8 | result = func(*args, **kwargs) 9 | if isinstance(result, basestring): 10 | response = HttpResponse() 11 | response['X-File'] = result 12 | return response 13 | return provider_response 14 | 15 | 16 | def file_response_cached(func): 17 | @wraps(func) 18 | def provider_response(*args, **kwargs): 19 | result = func(*args, **kwargs) 20 | if isinstance(result, basestring): 21 | response = HttpResponse() 22 | response['X-File'] = result 23 | return response 24 | return provider_response 25 | 26 | -------------------------------------------------------------------------------- /fileprovider/utils.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def sendfile(path, view_mode=False, download_mode=False, download_fname=None): 5 | """ 6 | Create a xfile compatible Django response 7 | :param str path: absolute path to file 8 | :param view_mode: inform web browser/client the file can be displayed 9 | :param download_mode: inform web browser/client the file should be downloaded 10 | :param download_fname: provide a different filename for better UX ( when `download_only` option is set) 11 | """ 12 | response = HttpResponse() 13 | response["X-File"] = path 14 | content_dispos_optn = {"v_mod": "inline", "d_mod": "attachment"} 15 | content_dispos = content_dispos_optn.get(view_mode and "v_mod", None) 16 | content_dispos = content_dispos or content_dispose_optn.get( 17 | download_mode and "d_mod", None 18 | ) 19 | content_dispos = ( 20 | (content_dispos + '; filename="%s"' % (download_fname)) 21 | if (download_mode and download_fname) 22 | else content_dispos 23 | ) 24 | if content_dispos is not None: 25 | response["Content-Disposition"] = content_dispos 26 | return response 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from ez_setup import use_setuptools 5 | use_setuptools() 6 | from setuptools import setup 7 | 8 | __version__ = '0.1.4' 9 | 10 | setup( 11 | name='django-fileprovider', 12 | version=__version__, 13 | description='django middleware for serving media files ( or protect them )', 14 | author='Renjith Thankachan', 15 | author_email='mail3renjith@gmail.com', 16 | url='https://github.com/instapk/django-fileprovider.git', 17 | download_url='https://github.com/instapk/django-fileprovider/archive/0.1.4.tar.gz', 18 | long_description=open('README.md', 'r').read(), 19 | packages=[ 20 | 'fileprovider', 21 | ], 22 | zip_safe=False, 23 | requires=[ 24 | ], 25 | install_requires=[ 26 | 'six>=1.10.0', 27 | ], 28 | setup_requires=[ 29 | 'pytest-runner', 30 | ], 31 | tests_require=[ 32 | 'pytest', 33 | ], 34 | classifiers=[ 35 | 'Environment :: Web Environment', 36 | 'Framework :: Django', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: BSD License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 2', 42 | 'Topic :: Utilities' 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Renjith Thankachan 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the software nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL Renjith Thankachan BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://pypip.in/download/django-fileprovider/badge.svg)](https://pypi.python.org/pypi/django-fileprovider/) 2 | 3 | File handler for django, useful when you want to add control or logic to served files. 4 | It uses `sendfile` API supports on different servers and uses pythonic solution on django development server. 5 | Currently it supports, 6 | 7 | * Apache 8 | * Nginx 9 | * LightHttpd 10 | * Caddy 11 | * Proxygen 12 | * H2O/Reproxy 13 | * Hiawatha 14 | 15 | # INSTALLATION 16 | 17 | use pip to install package: 18 | `pip install django-fileprovider` 19 | 20 | NOTE: if you are installing from github version consider checking [releases](https://github.com/sideffect0/django-fileprovider/releases) 21 | 22 | * add `fileprovider` to django `INSTALLED_APPS` section. 23 | * add `fileprovider.middleware.FileProviderMiddleware` to `MIDDLEWARE_CLASSES` section 24 | * set django `settings` file with `FILEPROVIDER_NAME` any of available providers `python`, `nginx`, `apache`, `lighthttpd`, 25 | `caddy`, `hiawatha`, `xsendfile`, `xaccel`. 26 | 27 | ```python 28 | 29 | # or you can put FILEPROVIDER_NAME as python in your local settings file 30 | if settings.DEBUG: 31 | FILEPROVIDER_NAME = "python" 32 | else: 33 | # or apache, lighthttpd, caddy 34 | FILEPROVIDER_NAME = "nginx" 35 | 36 | ``` 37 | 38 | # USAGE 39 | 40 | on django views where file response is required, fill response header `X-File` with absolute file path or use `sendfile` wrapper 41 | for example, 42 | 43 | ```python 44 | 45 | from fileprovider.utils import sendfile 46 | def hello(request): 47 | return sendfile('/absolute/path/to/file') 48 | 49 | # can be used protecting file access from unauthorized users 50 | @login_required 51 | def hello(request, file_id): 52 | file = get_object_or_404(FileModel, pk=file_id) 53 | return sendfile(file.path) 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /fileprovider/middleware.py: -------------------------------------------------------------------------------- 1 | import os 2 | import six 3 | 4 | from django.conf import settings 5 | from django.core.files.base import File 6 | from django.http import HttpResponse, HttpResponseNotFound, FileResponse 7 | 8 | try: 9 | from django.utils.deprecation import MiddlewareMixin 10 | except ImportError: 11 | MiddlewareMixin = object 12 | 13 | class FileProvider(object): 14 | def get_response(self, response, **options): 15 | return self._get_response(response, **options) 16 | 17 | def _get_response(self, response, **options): 18 | raise NotImplemented 19 | 20 | class XAccelFileProvider(FileProvider): 21 | def _get_response(self, response, **options): 22 | response['X-Accel-Redirect'] = response['X-File'] 23 | del response['X-File'] 24 | return response 25 | 26 | class XSendFileProvider(FileProvider): 27 | def _get_response(self, response, **options): 28 | response['X-Sendfile'] = response['X-File'] 29 | del response['X-File'] 30 | return response 31 | 32 | class XReproxyFileProvider(FileProvider): 33 | def _get_response(self, response, **options): 34 | response['X-Reproxy-URL'] = response['X-File'] 35 | del response['X-File'] 36 | return response 37 | 38 | class PythonFileProvider(FileProvider): 39 | def _get_response(self, response, **options): 40 | # only need this checking when python file provider is given 41 | if not os.path.exists(response['X-File']): 42 | return HttpResponseNotFound("file not found") 43 | return FileResponse(open(response['X-File'], 'rb')) 44 | 45 | # Uses X-Sendfile 46 | ApacheFileProvider = XSendFileProvider 47 | LightHttpdFileProvider = XSendFileProvider 48 | HiawathaFileProvider = XSendFileProvider 49 | # Uses X-Accel-Redirect 50 | NginxFileProvider = XAccelFileProvider 51 | CaddyFileProvider = XAccelFileProvider 52 | ProxygenFileProvider = XAccelFileProvider 53 | ReproxyFileProvider = XReproxyFileProvider 54 | H2OFileProvider = ReproxyFileProvider 55 | 56 | PROVIDERS = { 57 | 'python': PythonFileProvider, 58 | 'nginx': NginxFileProvider, 59 | 'apache': ApacheFileProvider, 60 | 'lighthttpd': LightHttpdFileProvider, 61 | 'caddy': CaddyFileProvider, 62 | 'hiawatha': HiawathaFileProvider, 63 | 'proxygen': ProxygenFileProvider, 64 | 'reproxy': ReproxyFileProvider, 65 | 'h2o': H2OFileProvider, 66 | 'uwsgi': XSendFileProvider, 67 | 'xaccel': XAccelFileProvider, 68 | 'xsendfile': XSendFileProvider, 69 | } 70 | 71 | class FileProviderMiddleware(MiddlewareMixin): 72 | def process_response(self, request, response): 73 | filepath = response.get('X-File', "") 74 | if filepath != "" and isinstance(filepath, six.string_types): 75 | provider_name = getattr(settings, "FILEPROVIDER_NAME", "python") 76 | provider = getattr(settings, "FILEPROVIDER_BACKEND", None) 77 | if not provider: 78 | provider = PROVIDERS[provider_name] 79 | response = provider().get_response(response) 80 | return response 81 | --------------------------------------------------------------------------------