""" Views to send private files. """ import os from django.http import Http404, HttpResponseForbidden from django.utils.module_loading import import_string from django.views.generic import View from django.views.generic.detail import SingleObjectMixin from . import appconfig from .models import PrivateFile from .servers import get_server_class from .storage import private_storage try: from urllib.parse import quote except ImportError: from urllib import quote # Python 2 class PrivateStorageView(View): """ Return the uploaded files """ #: The storage class to retrieve files from storage = private_storage #: The authorisation rule for accessing can_access_file = staticmethod(import_string(appconfig.PRIVATE_STORAGE_AUTH_FUNCTION)) #: Import the server class once server_class = get_server_class(appconfig.PRIVATE_STORAGE_SERVER) #: Whether the file should be displayed ``inline`` or show a download box (``attachment``). content_disposition = None #: The filename to use when :attr:`content_disposition` is set. content_disposition_filename = None def get_path(self): """ Determine the path for the object to provide. This can be overwritten to combine the view with a different object retrieval. """ return self.kwargs['path'] def get_private_file(self): """ Return all relevant data in a single object, so this is easy to extend and server implementations can pick what they need. """ return PrivateFile( request=self.request, storage=self.storage, relative_name=self.get_path() ) def get(self, request, *args, **kwargs): """ Handle incoming GET requests """ private_file = self.get_private_file() if not self.can_access_file(private_file): return HttpResponseForbidden('Private storage access denied') if not private_file.exists(): raise Http404("File not found") return self.serve_file(private_file) def serve_file(self, private_file): """ Serve the file that was retrieved from the storage. The relative path can be found with ``private_file.relative_name``. :type private_file: :class:`private_storage.models.PrivateFile` :rtype: django.http.HttpResponse """ response = self.server_class().serve(private_file) if self.content_disposition: filename = self.get_content_disposition_filename(private_file) response['Content-Disposition'] = '{}; {}'.format( self.content_disposition, self._encode_filename_header(filename) ) return response def get_content_disposition_filename(self, private_file): """ Return the filename in the download header. """ return self.content_disposition_filename or os.path.basename(private_file.relative_name) def _encode_filename_header(self, filename): """ The filename, encoded to use in a ``Content-Disposition`` header. """ # Based on https://www.djangosnippets.org/snippets/1710/ user_agent = self.request.META.get('HTTP_USER_AGENT', None) if 'WebKit' in user_agent: # Support available for UTF-8 encoded strings. utf8_filename = filename.encode("utf-8") return 'filename={}'.format(utf8_filename) elif 'MSIE' in user_agent: # IE does not support internationalized filename at all. # It can only recognize internationalized URL, so we should perform a trick via URL names. return '' else: # For others like Firefox, we follow RFC2231 (encoding extension in HTTP headers). rfc2231_filename = quote(filename.encode("utf-8")) return "filename*=UTF-8''{}".format(rfc2231_filename) class PrivateStorageDetailView(SingleObjectMixin, PrivateStorageView): """ Download a document based on an object ID. This view can by used by third-party apps to implement their own download view. Implement access controls by overriding :meth`get_queryset` or redefining :meth:`can_access_file`. """ #: Define the model to fetch. model = None #: Define which field the file name is stored at. model_file_field = 'file' def get(self, request, *args, **kwargs): self.object = self.get_object() return super(PrivateStorageDetailView, self).get(request, *args, **kwargs) def get_path(self): file = getattr(self.object, self.model_file_field) return file.name def get_private_file(self): # Provide the parent object as well. return PrivateFile( request=self.request, storage=self.storage, relative_name=self.get_path(), parent_object=self.object ) def can_access_file(self, private_file): """ The authorization rule for this view. By default it reuses the ``PRIVATE_STORAGE_AUTH_FUNCTION`` setting, but this should likely be redefined. """ return PrivateStorageView.can_access_file(private_file)