From 33ad5d492ed1407b37b1729fdffdb3b90252a0be Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Tue, 28 Aug 2012 15:47:26 +0200 Subject: [PATCH] Refactored generic download views. Added storage argument to DownloadView. Renamed DownloadView's 'file' argument to 'filename'. --- demo/demoproject/download/tests.py | 4 +- demo/demoproject/download/views.py | 4 +- django_downloadview/views.py | 118 +++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 20 deletions(-) diff --git a/demo/demoproject/download/tests.py b/demo/demoproject/download/tests.py index 91d5925..fd8fa11 100644 --- a/demo/demoproject/download/tests.py +++ b/demo/demoproject/download/tests.py @@ -71,7 +71,7 @@ class DownloadViewTestCase(DownloadTestCase): """Download_hello_world view returns hello-world.txt as attachement.""" response = self.client.get(self.download_hello_world_url) self.assertEquals(response.status_code, 200) - self.assertEquals(response['Content-Type'], 'application/octet-stream') + self.assertEquals(response['Content-Type'], 'text/plain') self.assertEquals(response['Content-Disposition'], 'attachment; filename=hello-world.txt') self.assertEqual(open(self.files['hello-world.txt']).read(), @@ -89,7 +89,7 @@ class ObjectDownloadViewTestCase(DownloadTestCase): ) response = self.client.get(self.download_document_url) self.assertEquals(response.status_code, 200) - self.assertEquals(response['Content-Type'], 'application/octet-stream') + self.assertEquals(response['Content-Type'], 'text/plain') self.assertEquals(response['Content-Disposition'], 'attachment; filename=hello-world.txt') self.assertEqual(open(self.files['hello-world.txt']).read(), diff --git a/demo/demoproject/download/views.py b/demo/demoproject/download/views.py index 55151f0..6ba9854 100644 --- a/demo/demoproject/download/views.py +++ b/demo/demoproject/download/views.py @@ -10,7 +10,7 @@ fixtures_dir = join(app_dir, 'fixtures') hello_world_file = join(fixtures_dir, 'hello-world.txt') -download_hello_world = DownloadView.as_view(file=hello_world_file) - +download_hello_world = DownloadView.as_view(filename=hello_world_file, + storage=None) download_document = ObjectDownloadView.as_view(model=Document) diff --git a/django_downloadview/views.py b/django_downloadview/views.py index e83d4e9..6df46bc 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -1,43 +1,117 @@ +import mimetypes import os import shutil from wsgiref.util import FileWrapper from django.core.files import File +from django.core.files.storage import DefaultStorage from django.http import HttpResponse from django.views.generic.base import View from django.views.generic.detail import BaseDetailView +from django.views.static import was_modified_since class DownloadMixin(object): - file = None response_class = HttpResponse + def get_file(self): + """Return a django.core.files.File object, which is to be served.""" + raise NotImplementedError() + + def get_filename(self): + """Return absolute filename.""" + file_obj = self.get_file() + return file_obj.name + + def get_file_wrapper(self): + """Return a wsgiref.util.FileWrapper instance for the file to serve.""" + try: + return self.file_wrapper + except AttributeError: + self.file_wrapper = FileWrapper(self.get_file()) + return self.file_wrapper + def get_mime_type(self): - """Return mime-type of self.file.""" - return 'application/octet-stream' + """Return mime-type.""" + try: + return self.mime_type + except AttributeError: + filename = self.get_filename() + self.mime_type, self.encoding = mimetypes.guess_type(filename) + if not self.mime_type: + self.mime_type = 'application/octet-stream' + return self.mime_type + + def get_encoding(self): + """Return encoding of self.file.""" + try: + return self.encoding + except AttributeError: + filename = self.get_filename() + self.mime_type, self.encoding = mimetypes.guess_type(filename) + return self.encoding + + def get_modification_time(self): + """Return last modification time of self.file.""" + try: + return self.modification_time + except AttributeError: + self.stat = os.stat(self.get_filename()) + self.modification_time = self.stat.st_mtime + return self.modification_time + + def get_size(self): + """Return the size (in bytes) of the file to serve.""" + try: + return self.size + except AttributeError: + try: + self.size = self.stat.st_size + except AttributeError: + self.size = os.path.getsize(self.get_filename()) + return self.size def render_to_response(self, **response_kwargs): """Returns a response with a file as attachment.""" mime_type = self.get_mime_type() - if isinstance(self.file, File): - absolute_filename = self.file.path - wrapper = FileWrapper(self.file.file) - size = self.file.size - else: - absolute_filename = os.path.abspath(self.file) - wrapper = FileWrapper(file(absolute_filename)) - size = os.path.getsize(absolute_filename) - basename = os.path.basename(absolute_filename) + modification_time = self.get_modification_time() + size = self.get_size() + # Respect the If-Modified-Since header. + if_modified_since = self.request.META.get('HTTP_IF_MODIFIED_SINCE', None) + if not was_modified_since(if_modified_since, modification_time, size): + return HttpResponseNotModified(mimetype=mime_type) + # Stream the file. + filename = self.get_filename() + basename = os.path.basename(filename) + encoding = self.get_encoding() + wrapper = self.get_file_wrapper() response = self.response_class(wrapper, content_type=mime_type, mimetype=mime_type) response['Content-Length'] = size - # Do not call fsock.close() as HttpResponse needs it open - # Garbage collector will close it + # Do not call fsock.close() as HttpResponse needs it open. + # Garbage collector will close it. response['Content-Disposition'] = 'attachment; filename=%s' % basename return response class DownloadView(DownloadMixin, View): + filename = None + storage = DefaultStorage() + + def get_file(self): + """Return a file object for the file to serve.""" + try: + return self._file + except AttributeError: + try: + if self.storage: + self._file = self.storage.open(self.filename) + else: + self._file = File(open(self.filename)) + return self._file + except IOError: + raise Http404 + def get(self, request, *args, **kwargs): return self.render_to_response() @@ -45,7 +119,19 @@ class DownloadView(DownloadMixin, View): class ObjectDownloadView(DownloadMixin, BaseDetailView): file_field = 'file' - def get(self, request, *args, **kwargs): + def get_fieldfile(self): self.object = self.get_object() - self.file = getattr(self.object, self.file_field) + try: + return self.fieldfile + except AttributeError: + self.fieldfile = getattr(self.object, self.file_field) + return self.fieldfile + + def get_file(self): + return self.get_fieldfile().file + + def get_size(self): + return self.get_fieldfile().size + + def get(self, request, *args, **kwargs): return self.render_to_response()