diff --git a/assetkit/files/servers.py b/assetkit/files/servers.py new file mode 100644 index 0000000..aded43a --- /dev/null +++ b/assetkit/files/servers.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# from django-protected-media + +import mimetypes +import os +import stat +from six.moves.urllib.parse import quote +from django.conf import settings +from django.http import HttpResponse, Http404, HttpResponseNotModified +from django.utils.encoding import smart_str +from django.utils.http import http_date +from django.views.static import was_modified_since + + +class BaseProtectedMediaServer(object): + INTERNAL_URL = None + ROOT = None + FORCE_DOWNLOAD = True + + def get_url(self, relative_path): + return os.path.join(self.INTERNAL_URL or settings.PROTECTED_MEDIA_INTERNAL_URL, relative_path).encode('utf-8') + + def get_full_path(self, relative_path): + return os.path.join(self.ROOT or settings.PROTECTED_MEDIA_ROOT, relative_path) + + def get_mimetype(self, relative_path): + return mimetypes.guess_type(self.get_full_path(relative_path))[0] or 'application/octet-stream' + + def get_force_download(self, overwrite=None): + if overwrite is not None: + return overwrite + else: + return getattr(settings, 'PROTECTED_MEDIA_FORCE_DOWNLOAD', self.FORCE_DOWNLOAD) + + def get_filename(self, relative_path): + return smart_str(os.path.basename(self.get_full_path(relative_path))) + + def add_attachment_header(self, response, relative_path): + """ + Add header to force the browser to save the file instead of possibly displaying it. + """ + filename = self.get_filename(relative_path) + response['Content-Disposition'] = "attachment; filename*=UTF=8''{0}".format(quote(filename)) + return response + + def prepare_response(self, request, response, relative_path): + if self.get_force_download(): + response = self.add_attachment_header(response, relative_path) + return response + + def serve(self, request, relative_path): + response = HttpResponse() + response = self.prepare_response(request, response, relative_path) + return response + + + +from django.conf.urls import patterns, url +from django.conf import settings + + def get_urls(self): + return ( + ) + url(r'^{0}(?P.*)$'.format(settings.PRIVATE_MEDIA_URL.lstrip('/')), self.serve_protected_path,), +) + + + def get_urls(self): + from django.conf.urls import url, include + # Since this module gets imported in the application's root package, + # it cannot import models from other applications at the module level, + # and django.contrib.contenttypes.views imports ContentType. + from django.contrib.contenttypes import views as contenttype_views + + if settings.DEBUG: + self.check_dependencies() + + def wrap(view, cacheable=False): + def wrapper(*args, **kwargs): + return self.admin_view(view, cacheable)(*args, **kwargs) + wrapper.admin_site = self + return update_wrapper(wrapper, view) + + # Admin-site-wide views. + urlpatterns = [ + url(r'^$', wrap(self.index), name='index'), + url(r'^login/$', self.login, name='login'), + url(r'^logout/$', wrap(self.logout), name='logout'), + url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'), + url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), + name='password_change_done'), + url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), + url(r'^r/(?P\d+)/(?P.+)/$', wrap(contenttype_views.shortcut), + name='view_on_site'), + ] + + # Add in each model's views, and create a list of valid URLS for the + # app_index + valid_app_labels = [] + for model, model_admin in self._registry.items(): + urlpatterns += [ + url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), + ] + if model._meta.app_label not in valid_app_labels: + valid_app_labels.append(model._meta.app_label) + + # If there were ModelAdmins registered, we should have a list of app + # labels for which we need to allow access to the app_index view, + if valid_app_labels: + regex = r'^(?P' + '|'.join(valid_app_labels) + ')/$' + urlpatterns += [ + url(regex, wrap(self.app_index), name='app_list'), + ] + return urlpatterns + + @property + def urls(self): + return self.get_urls(), 'admin', self.name + + + + + def serve_protected_path(self, request, path): + """ + Serve private files to users with read permission. + """ + if not permissions.has_read_permission(request, path): + if settings.DEBUG: + raise PermissionDenied + else: + raise Http404('File not found') + return server.serve(request, relative_path=path) + + +class LocalDevelopmentServer(BaseProtectedMediaServer): + """ + Serve static files from the local filesystem through django. + This is a bad idea for most situations other than testing. + + This will only work for files that can be accessed in the local filesystem. + """ + + def serve(self, request, relative_path): + # the following code is largely borrowed from `django.views.static.serve` + # and django-filetransfers: filetransfers.backends.default + + full_path = self.get_full_path(relative_path) + + if not os.path.exists(full_path): + raise Http404('"{0}" does not exist'.format(full_path)) + + # Respect the If-Modified-Since header. + content_type = self.get_mimetype(relative_path) + statobj = os.stat(full_path) + + if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), + statobj[stat.ST_MTIME], + statobj[stat.ST_SIZE]): + response = HttpResponseNotModified(content_type=content_type) + else: + response = HttpResponse(open(full_path, 'rb').read(), content_type=content_type) + response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) + response = self.prepare_response(request, response, relative_path) + + return response + + +class ApacheXSendfileServer(BaseProtectedMediaServer): + def prepare_response(self, request, response, relative_path): + response = super(ApacheXSendfileServer, self).prepare_response(request, response, relative_path) + + # Apache expects a 'X-Sendfile' header an the full filesystem path + response['X-Sendfile'] = self.get_full_path(request, relative_path) + + # From django-filer (https://github.com/stefanfoulis/django-filer/): + # This is needed for lighttpd, hopefully this will + # not be needed after this is fixed: + # http://redmine.lighttpd.net/issues/2076 + response['Content-Type'] = self.get_mimetype(relative_path) + return response + + +class NginxXAccelRedirectServer(BaseProtectedMediaServer): + def prepare_response(self, request, response, relative_path): + response = super(NginxXAccelRedirectServer, self).prepare_response(request, response, relative_path) + + # Nginx expects a 'X-Accel-Redirect' header to a protected url; + # the actual filesystem path is set in the nginx configuration. + response['X-Accel-Redirect'] = self.get_url(relative_path) + response['Content-Type'] = self.get_mimetype(relative_path) + return response diff --git a/assetkit/files/views.py b/assetkit/files/views.py new file mode 100644 index 0000000..68fce77 --- /dev/null +++ b/assetkit/files/views.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from importlib import import_module +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.http import Http404 + +from . import servers + + +def get_class(import_path=None): + """ + Largely based on django.core.files.storage's get_storage_class + """ + from django.core.exceptions import ImproperlyConfigured + if import_path is None: + raise ImproperlyConfigured('No class path specified.') + try: + dot = import_path.rindex('.') + except ValueError: + raise ImproperlyConfigured("%s isn't a module." % import_path) + module, classname = import_path[:dot], import_path[dot + 1:] + try: + mod = import_module(module) + except ImportError as e: + raise ImproperlyConfigured('Error importing module %s: "%s"' % (module, e)) + try: + return getattr(mod, classname) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define a "%s" class.' % (module, classname)) + + +server = get_class(settings.PROTECTED_MEDIA_SERVER)(**getattr(settings, 'PROTECTED_MEDIA_SERVER_OPTIONS', {})) +if hasattr(settings,'PROTECTED_MEDIA_PERMISSIONS'): + permissions = get_class(settings.PROTECTED_MEDIA_PERMISSIONS)(**getattr(settings, 'PROTECTED_MEDIA_PERMISSIONS_OPTIONS', {})) +else: + from .permissions import DefaultProtectedPermissions + permissions = DefaultProtectedPermissions() + + +def serve_protected_file(request, path): + """ + Serve private files to users with read permission. + """ + if not permissions.has_read_permission(request, path): + if settings.DEBUG: + raise PermissionDenied + else: + raise Http404('File not found') + return server.serve(request, relative_path=path)