Commit c7abf477 authored by Diederik van der Boor's avatar Diederik van der Boor
Browse files

Support streaming responses from the storage class

This is needed to fetch content from S3 without needing a full_path
parent eecfc006
import mimetypes import mimetypes
import os from django.core.files.storage import Storage, File
from django.utils.functional import cached_property from django.utils.functional import cached_property
...@@ -11,7 +11,7 @@ class PrivateFile(object): ...@@ -11,7 +11,7 @@ class PrivateFile(object):
def __init__(self, request, storage, relative_name): def __init__(self, request, storage, relative_name):
self.request = request self.request = request
self.storage = storage self.storage = storage # type: Storage
self.relative_name = relative_name self.relative_name = relative_name
@cached_property @cached_property
...@@ -19,13 +19,38 @@ class PrivateFile(object): ...@@ -19,13 +19,38 @@ class PrivateFile(object):
# Not using self.storage.open() as the X-Sendfile needs a normal path. # Not using self.storage.open() as the X-Sendfile needs a normal path.
return self.storage.path(self.relative_name) return self.storage.path(self.relative_name)
def open(self, mode='rb'):
"""
Open the file for reading.
:rtype: django.core.files.storage.File
"""
file = self.storage.open(self.relative_name, mode=mode) # type: File
return file
def exists(self): def exists(self):
return os.path.exists(self.full_path) """
Check whether the file exists.
"""
return self.storage.exists(self.relative_name)
@cached_property @cached_property
def content_type(self): def content_type(self):
""" """
Return the HTTP ``Content-Type`` header value for a filename. Return the HTTP ``Content-Type`` header value for a filename.
""" """
mimetype, encoding = mimetypes.guess_type(self.full_path) mimetype, encoding = mimetypes.guess_type(self.relative_name)
return mimetype or 'application/octet-stream' return mimetype or 'application/octet-stream'
@cached_property
def size(self):
"""
Return the size of the file in bytes.
"""
return self.storage.size(self.relative_name)
@cached_property
def modified_time(self):
"""
Return the last-modified time
"""
return self.storage.get_modified_time(self.relative_name)
...@@ -5,7 +5,8 @@ import os ...@@ -5,7 +5,8 @@ import os
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse from django.http import FileResponse, HttpResponse
from django.utils.http import http_date
from django.utils.lru_cache import lru_cache from django.utils.lru_cache import lru_cache
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.views.static import serve from django.views.static import serve
...@@ -15,6 +16,8 @@ from django.views.static import serve ...@@ -15,6 +16,8 @@ from django.views.static import serve
def get_server_class(path): def get_server_class(path):
if '.' in path: if '.' in path:
return import_string(path) return import_string(path)
elif path == 'streaming':
return DjangoStreamingServer
elif path == 'django': elif path == 'django':
return DjangoServer return DjangoServer
elif path == 'apache': elif path == 'apache':
...@@ -27,7 +30,22 @@ def get_server_class(path): ...@@ -27,7 +30,22 @@ def get_server_class(path):
) )
class DjangoServer(object): class DjangoStreamingServer(object):
"""
Serve static files as streaming chunks in Django.
"""
@staticmethod
def serve(private_file):
# FileResponse will submit the file in 8KB chunks
response = FileResponse(private_file.open())
response['Content-Type'] = private_file.content_type
response['Content-Length'] = private_file.size
response["Last-Modified"] = http_date(private_file.modified_time.timestamp())
return response
class DjangoServer(DjangoStreamingServer):
""" """
Serve static files from the local filesystem through Django. Serve static files from the local filesystem through Django.
This is a bad idea for most situations other than testing. This is a bad idea for most situations other than testing.
...@@ -38,7 +56,14 @@ class DjangoServer(object): ...@@ -38,7 +56,14 @@ class DjangoServer(object):
@staticmethod @staticmethod
def serve(private_file): def serve(private_file):
# This supports If-Modified-Since and sends the file in 8KB chunks # This supports If-Modified-Since and sends the file in 8KB chunks
return serve(private_file.request, private_file.full_path, document_root='/', show_indexes=False) try:
full_path = private_file.full_path
except NotImplementedError:
# S3 files, fall back to streaming server
return DjangoStreamingServer.serve(private_file)
else:
# Using Django's serve gives If-Modified-Since support out of the box.
return serve(private_file.request, full_path, document_root='/', show_indexes=False)
class ApacheXSendfileServer(object): class ApacheXSendfileServer(object):
......
...@@ -85,7 +85,7 @@ class PrivateStorageDetailView(SingleObjectMixin, PrivateStorageView): ...@@ -85,7 +85,7 @@ class PrivateStorageDetailView(SingleObjectMixin, PrivateStorageView):
def get_path(self): def get_path(self):
file = getattr(self.object, 'file') file = getattr(self.object, 'file')
return file.path return file.name
def can_access_file(self, private_file): def can_access_file(self, private_file):
""" """
......
...@@ -40,7 +40,7 @@ setup( ...@@ -40,7 +40,7 @@ setup(
install_requires=[], install_requires=[],
requires=[ requires=[
'Django (>=1.7)', 'Django (>=1.7.4)',
], ],
description='Private media file storage for Django projects', description='Private media file storage for Django projects',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment