diff --git a/.gitignore b/.gitignore index 2267b36..55e9f48 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /lib /build .Python +.tox diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..641085a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +dist: trusty +sudo: false +language: python +cache: pip +matrix: + include: + - python: '2.7' + env: DJANGO=django18 + - python: '2.7' + env: DJANGO=django110 + - python: '2.7' + env: DJANGO=django111 + - python: '3.2' + env: DJANGO=django18 + - python: '3.3' + env: DJANGO=django18 + - python: '3.4' + env: DJANGO=django110 + - python: '3.4' + env: DJANGO=django111 + - python: '3.5' + env: DJANGO=django110 + - python: '3.5' + env: DJANGO=django111 + - python: '3.6' + env: DJANGO=django111 +install: + - pip install tox +script: + - tox -e py${TRAVIS_PYTHON_VERSION//.}-${DJANGO} -- -v 2 diff --git a/README.rst b/README.rst index 76ccf99..61a2451 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ Django Sendfile =============== +.. image:: https://img.shields.io/travis/lexfo/django-sendfile.svg + :target: https://travis-ci.org/lexfo/django-sendfile + This is a wrapper around web-server specific methods for sending files to web clients. This is useful when Django needs to check permissions associated files, but does not want to serve the actual bytes of the file itself. i.e. as serving large files is not what Django is made for. Note this should not be used for regular file serving (e.g. css etc), only for cases where you need Django to do some work before serving the actual file. diff --git a/examples/protected_downloads/download/apps.py b/examples/protected_downloads/download/apps.py new file mode 100644 index 0000000..b340dce --- /dev/null +++ b/examples/protected_downloads/download/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DownloadAppConfig(AppConfig): + name = 'download' diff --git a/examples/protected_downloads/download/tests.py b/examples/protected_downloads/download/tests.py deleted file mode 100644 index 501deb7..0000000 --- a/examples/protected_downloads/download/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/examples/protected_downloads/download/urls.py b/examples/protected_downloads/download/urls.py index fd4bb28..8188e4c 100644 --- a/examples/protected_downloads/download/urls.py +++ b/examples/protected_downloads/download/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls.defaults import * +from django.conf.urls import url from .views import download, download_list -urlpatterns = patterns('', +urlpatterns = [ url(r'^$', download_list), url(r'(?P\d+)/$', download, name='download'), -) +] diff --git a/examples/protected_downloads/example/__init__.py b/examples/protected_downloads/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/protected_downloads/settings.py b/examples/protected_downloads/example/settings.py similarity index 98% rename from examples/protected_downloads/settings.py rename to examples/protected_downloads/example/settings.py index 245739c..639a11e 100644 --- a/examples/protected_downloads/settings.py +++ b/examples/protected_downloads/example/settings.py @@ -67,7 +67,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', ) -ROOT_URLCONF = 'protected_downloads.urls' +ROOT_URLCONF = 'example.urls' TEMPLATE_DIRS = ( os.path.join(PROJECT_ROOT, 'templates'), diff --git a/examples/protected_downloads/example/urls.py b/examples/protected_downloads/example/urls.py new file mode 100644 index 0000000..80b96e1 --- /dev/null +++ b/examples/protected_downloads/example/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url, include + +from django.contrib import admin +admin.autodiscover() + +urlpatterns = [ + url(r'^', include('download.urls')), + url(r'^admin/', include(admin.site.urls)), +] diff --git a/examples/protected_downloads/manage.py b/examples/protected_downloads/manage.py index 3e098b0..2605e37 100644 --- a/examples/protected_downloads/manage.py +++ b/examples/protected_downloads/manage.py @@ -1,14 +1,10 @@ #!/usr/bin/env python +import os +import sys -from __future__ import absolute_import +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") -from django.core.management import execute_manager -try: - from . import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) + from django.core.management import execute_from_command_line -if __name__ == "__main__": - execute_manager(settings) + execute_from_command_line(sys.argv) diff --git a/examples/protected_downloads/sendfile b/examples/protected_downloads/sendfile deleted file mode 120000 index fca7c70..0000000 --- a/examples/protected_downloads/sendfile +++ /dev/null @@ -1 +0,0 @@ -../../sendfile \ No newline at end of file diff --git a/examples/protected_downloads/urls.py b/examples/protected_downloads/urls.py deleted file mode 100644 index 238d8c7..0000000 --- a/examples/protected_downloads/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.conf.urls.defaults import * - -from django.contrib import admin -admin.autodiscover() - -urlpatterns = patterns('', - (r'^', include('protected_downloads.download.urls')), - (r'^admin/', include(admin.site.urls)), -) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 1cc9809..1f30983 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -5,6 +5,8 @@ from mimetypes import guess_type import unicodedata +import six + def _lazy_load(fn): _cached = [] @@ -77,8 +79,10 @@ def sendfile(request, filename, attachment=False, attachment_filename=None, mime # Django 1.3 from django.utils.encoding import force_unicode as force_text attachment_filename = force_text(attachment_filename) - ascii_filename = unicodedata.normalize('NFKD', attachment_filename).encode('ascii','ignore') - parts.append('filename="%s"' % ascii_filename) + ascii_filename = unicodedata.normalize('NFKD', attachment_filename).encode('ascii', 'ignore') + if six.PY3: + ascii_filename = ascii_filename.decode() + parts.append('filename="%s"' % str(ascii_filename)) if ascii_filename != attachment_filename: from django.utils.http import urlquote quoted_filename = urlquote(attachment_filename) diff --git a/sendfile/backends/xsendfile.py b/sendfile/backends/xsendfile.py index a87aa83..3c603e1 100644 --- a/sendfile/backends/xsendfile.py +++ b/sendfile/backends/xsendfile.py @@ -1,8 +1,12 @@ from django.http import HttpResponse +import six def sendfile(request, filename, **kwargs): response = HttpResponse() - response['X-Sendfile'] = unicode(filename).encode('utf-8') + if six.PY2: + response['X-Sendfile'] = unicode(filename).encode('utf-8') + else: + response['X-Sendfile'] = filename return response diff --git a/sendfile/tests.py b/sendfile/tests.py index 0643cae..02ad369 100644 --- a/sendfile/tests.py +++ b/sendfile/tests.py @@ -1,5 +1,7 @@ # coding=utf-8 +from __future__ import unicode_literals + from django.conf import settings from django.test import TestCase from django.http import HttpResponse, Http404, HttpRequest @@ -8,6 +10,7 @@ from tempfile import mkdtemp import shutil from sendfile import sendfile as real_sendfile, _get_sendfile +import six try: from urllib.parse import unquote @@ -107,7 +110,7 @@ def test_correct_file_in_xsendfile_header(self): self.assertEqual(filepath, response['X-Sendfile']) def test_xsendfile_header_containing_unicode(self): - filepath = self.ensure_file(u'péter_là_gueule.txt') + filepath = self.ensure_file('péter_là_gueule.txt') response = real_sendfile(HttpRequest(), filepath) self.assertTrue(response is not None) self.assertEqual(smart_str(filepath), response['X-Sendfile']) @@ -129,10 +132,13 @@ def test_correct_url_in_xaccelredirect_header(self): self.assertEqual('/private/readme.txt', response['X-Accel-Redirect']) def test_xaccelredirect_header_containing_unicode(self): - filepath = self.ensure_file(u'péter_là_gueule.txt') + filepath = self.ensure_file('péter_là_gueule.txt') response = real_sendfile(HttpRequest(), filepath) self.assertTrue(response is not None) - self.assertEqual(u'/private/péter_là_gueule.txt'.encode('utf-8'), unquote(response['X-Accel-Redirect'])) + path = '/private/péter_là_gueule.txt' + if six.PY2: + path = path.encode('utf-8') + self.assertEqual(path, unquote(response['X-Accel-Redirect'])) class TestModWsgiBackend(TempFileTestCase): @@ -151,7 +157,10 @@ def test_correct_url_in_location_header(self): self.assertEqual('/private/readme.txt', response['Location']) def test_location_header_containing_unicode(self): - filepath = self.ensure_file(u'péter_là_gueule.txt') + filepath = self.ensure_file('péter_là_gueule.txt') response = real_sendfile(HttpRequest(), filepath) self.assertTrue(response is not None) - self.assertEqual(u'/private/péter_là_gueule.txt'.encode('utf-8'), unquote(response['Location'])) + path = '/private/péter_là_gueule.txt' + if six.PY2: + path = path.encode('utf-8') + self.assertEqual(path, unquote(response['Location'])) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..412d336 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[wheel] +# create "py2.py3-none-any.whl" package +universal = 1 diff --git a/setup.py b/setup.py index 406b127..fb33b70 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,4 @@ -from distutils.core import setup - -try: - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - from distutils.command.build_py import build_py +from setuptools import setup version = __import__('sendfile').__version__ @@ -19,8 +14,8 @@ url='https://github.com/johnsensible/django-sendfile', license='BSD', - requires=['Django (>=1.3)'], - install_requires=['Django>=1.3'], + requires=['Django (>=1.8)'], + install_requires=['Django>=1.8', 'six'], packages=['sendfile', 'sendfile.backends'], package_dir={ @@ -36,12 +31,19 @@ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.10', + 'Framework :: Django :: 1.11', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', ], - - cmdclass={'build_py': build_py}, ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7ca720c --- /dev/null +++ b/tox.ini @@ -0,0 +1,21 @@ +# Tox (http://codespeak.net/~hpk/tox/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +minversion = 2.2 +envlist = + py{27,32,33,34}-django18 + py{27,34,35}-django110 + py{27,34,35,36}-django111 +skip_missing_interpreters = True + +[testenv] +changedir = examples/protected_downloads +commands = python manage.py test sendfile +deps = + six + django18: django >=1.8,<1.9 + django110: django >=1.10,<1.11 + django111: django >=1.11,<2