Commit 6642cddd by Chris Dodge Committed by David Baumgold

support uploading and referencing assets as streams rather than having to read…

support uploading and referencing assets as streams rather than having to read everything into memory first
parent 1d7e15fc
...@@ -89,11 +89,11 @@ def asset_index(request, org, course, name): ...@@ -89,11 +89,11 @@ def asset_index(request, org, course, name):
}) })
@login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required
def upload_asset(request, org, course, coursename): def upload_asset(request, org, course, coursename):
''' '''
cdodge: this method allows for POST uploading of files into the course asset library, which will This method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB. be supported by GridFS in MongoDB.
''' '''
if request.method != 'POST': if request.method != 'POST':
...@@ -118,16 +118,25 @@ def upload_asset(request, org, course, coursename): ...@@ -118,16 +118,25 @@ def upload_asset(request, org, course, coursename):
# compute a 'filename' which is similar to the location formatting, we're using the 'filename' # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing # nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent # the Location string formatting expectations to keep things a bit more consistent
upload_file = request.FILES['file']
filename = request.FILES['file'].name filename = upload_file.name
mime_type = request.FILES['file'].content_type mime_type = upload_file.content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename) content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
chunked = upload_file.multiple_chunks()
if chunked:
content = StaticContent(content_loc, filename, mime_type, upload_file.chunks())
else:
content = StaticContent(content_loc, filename, mime_type, upload_file.read())
thumbnail_content = None
thumbnail_location = None
# first let's see if a thumbnail can be created # first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content) (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content,
tempfile_path=None if not chunked else
upload_file.temporary_file_path())
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show) # delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location) del_cached_content(thumbnail_location)
...@@ -208,7 +217,9 @@ def remove_asset(request, org, course, name): ...@@ -208,7 +217,9 @@ def remove_asset(request, org, course, name):
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
"""
This method will handle a POST request to upload and import a .tar.gz file into a specified course
"""
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST': if request.method == 'POST':
...@@ -282,6 +293,10 @@ def import_course(request, org, course, name): ...@@ -282,6 +293,10 @@ def import_course(request, org, course, name):
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def generate_export_course(request, org, course, name): def generate_export_course(request, org, course, name):
"""
This method will serialize out a course to a .tar.gz file which contains a XML-based representation of
the course
"""
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
loc = Location(location) loc = Location(location)
...@@ -312,7 +327,9 @@ def generate_export_course(request, org, course, name): ...@@ -312,7 +327,9 @@ def generate_export_course(request, org, course, name):
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def export_course(request, org, course, name): def export_course(request, org, course, name):
"""
This method serves up the 'Export Course' page
"""
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
......
...@@ -6,6 +6,7 @@ from xmodule.modulestore import InvalidLocationError ...@@ -6,6 +6,7 @@ from xmodule.modulestore import InvalidLocationError
from cache_toolbox.core import get_cached_content, set_cached_content from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
import logging
class StaticContentServer(object): class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
...@@ -24,17 +25,21 @@ class StaticContentServer(object): ...@@ -24,17 +25,21 @@ class StaticContentServer(object):
if content is None: if content is None:
# nope, not in cache, let's fetch from DB # nope, not in cache, let's fetch from DB
try: try:
content = contentstore().find(loc) content = contentstore().find(loc, as_stream=True)
except NotFoundError: except NotFoundError:
response = HttpResponse() response = HttpResponse()
response.status_code = 404 response.status_code = 404
return response return response
# since we fetched it from DB, let's cache it going forward # since we fetched it from DB, let's cache it going forward, but only if it's < 1MB
# this is because I haven't been able to find a means to stream data out of memcached
if content.length is not None:
if content.length < 1048576:
# since we've queried as a stream, let's read in the stream into memory to set in cache
content = content.copy_to_in_mem()
set_cached_content(content) set_cached_content(content)
else: else:
# @todo: we probably want to have 'cache hit' counters so we can # NOP here, but we may wish to add a "cache-hit" counter in the future
# measure the efficacy of our caches
pass pass
# see if the last-modified at hasn't changed, if not return a 302 (Not Modified) # see if the last-modified at hasn't changed, if not return a 302 (Not Modified)
...@@ -50,7 +55,7 @@ class StaticContentServer(object): ...@@ -50,7 +55,7 @@ class StaticContentServer(object):
if if_modified_since == last_modified_at_str: if if_modified_since == last_modified_at_str:
return HttpResponseNotModified() return HttpResponseNotModified()
response = HttpResponse(content.data, content_type=content.content_type) response = HttpResponse(content.stream_data(), content_type=content.content_type)
response['Last-Modified'] = last_modified_at_str response['Last-Modified'] = last_modified_at_str
return response return response
...@@ -14,11 +14,13 @@ from PIL import Image ...@@ -14,11 +14,13 @@ from PIL import Image
class StaticContent(object): class StaticContent(object):
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None): def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None,
length=None):
self.location = loc self.location = loc
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
self.content_type = content_type self.content_type = content_type
self.data = data self._data = data
self.length = length
self.last_modified_at = last_modified_at self.last_modified_at = last_modified_at
self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None
# optional information about where this file was imported from. This is needed to support import/export # optional information about where this file was imported from. This is needed to support import/export
...@@ -45,6 +47,10 @@ class StaticContent(object): ...@@ -45,6 +47,10 @@ class StaticContent(object):
def get_url_path(self): def get_url_path(self):
return StaticContent.get_url_path_from_location(self.location) return StaticContent.get_url_path_from_location(self.location)
@property
def data(self):
return self._data
@staticmethod @staticmethod
def get_url_path_from_location(location): def get_url_path_from_location(location):
if location is not None: if location is not None:
...@@ -80,6 +86,35 @@ class StaticContent(object): ...@@ -80,6 +86,35 @@ class StaticContent(object):
loc = StaticContent.compute_location(course_namespace.org, course_namespace.course, path) loc = StaticContent.compute_location(course_namespace.org, course_namespace.course, path)
return StaticContent.get_url_path_from_location(loc) return StaticContent.get_url_path_from_location(loc)
def stream_data(self):
yield self._data
class StaticContentStream(StaticContent):
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None,
length=None):
super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at,
thumbnail_location=thumbnail_location, import_path=import_path,
length=length)
self._stream = stream
def stream_data(self):
while True:
chunk = self._stream.read(1024)
if len(chunk) == 0:
break
yield chunk
def close(self):
self._stream.close()
def copy_to_in_mem(self):
self._stream.seek(0)
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location,
import_path=self.import_path, length=self.length)
return content
class ContentStore(object): class ContentStore(object):
''' '''
......
...@@ -8,7 +8,7 @@ from xmodule.contentstore.content import XASSET_LOCATION_TAG ...@@ -8,7 +8,7 @@ from xmodule.contentstore.content import XASSET_LOCATION_TAG
import logging import logging
from .content import StaticContent, ContentStore from .content import StaticContent, ContentStore, StaticContentStream
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from fs.osfs import OSFS from fs.osfs import OSFS
import os import os
...@@ -47,20 +47,42 @@ class MongoContentStore(ContentStore): ...@@ -47,20 +47,42 @@ class MongoContentStore(ContentStore):
if self.fs.exists({"_id": id}): if self.fs.exists({"_id": id}):
self.fs.delete(id) self.fs.delete(id)
def find(self, location, throw_on_not_found=True): def find(self, location, throw_on_not_found=True, as_stream=False):
id = StaticContent.get_id_from_location(location) id = StaticContent.get_id_from_location(location)
try: try:
if as_stream:
fp = self.fs.get(id)
return StaticContentStream(location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
else:
with self.fs.get(id) as fp: with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), return StaticContent(location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None) import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
except NoFile: except NoFile:
if throw_on_not_found: if throw_on_not_found:
raise NotFoundError() raise NotFoundError()
else: else:
return None return None
def get_stream(self, location):
id = StaticContent.get_id_from_location(location)
try:
handle = self.fs.get(id)
except NoFile:
raise NotFoundError()
return handle
def close_stream(self, handle):
try:
handle.close()
except:
pass
def export(self, location, output_directory): def export(self, location, output_directory):
content = self.find(location) content = self.find(location)
......
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