def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden('Unauthorized') course_partial_id = "/".join([loc.org, loc.course]) if not request.user.is_staff and not CourseEnrollment.is_enrolled_by_partial( request.user, course_partial_id): return HttpResponseForbidden('Unauthorized') # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime( "%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() response = HttpResponse(content.stream_data(), content_type=content.content_type) response['Last-Modified'] = last_modified_at_str return response
def test_delete(self): set_cached_content(self.mockAsset) del_cached_content(self.nonUnicodeLocation) self.assertEqual(None, get_cached_content(self.unicodeLocation), 'should not be stored in cache with unicodeLocation') self.assertEqual(None, get_cached_content(self.nonUnicodeLocation), 'should not be stored in cache with nonUnicodeLocation')
def test_put_and_get(self): set_cached_content(self.mockAsset) self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content, 'should be stored in cache with unicodeLocation') self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content, 'should be stored in cache with nonUnicodeLocation')
def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith("/" + XASSET_LOCATION_TAG + "/"): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden("Unauthorized") course_partial_id = "/".join([loc.org, loc.course]) if not request.user.is_staff and not CourseEnrollment.is_enrolled_by_partial( request.user, course_partial_id ): return HttpResponseForbidden("Unauthorized") # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime("%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if "HTTP_IF_MODIFIED_SINCE" in request.META: if_modified_since = request.META["HTTP_IF_MODIFIED_SINCE"] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() response = HttpResponse(content.stream_data(), content_type=content.content_type) response["Last-Modified"] = last_modified_at_str return response
def test_put_and_get(self): set_cached_content(self.mockAsset) self.assertEqual( self.mockAsset.content, get_cached_content( self.unicodeLocation).content, 'should be stored in cache with unicodeLocation') self.assertEqual( self.mockAsset.content, get_cached_content( self.nonUnicodeLocation).content, 'should be stored in cache with nonUnicodeLocation')
def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: # return a 'Bad Request' to browser as we have a malformed # Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # since we fetched it from DB, let's cache it going forward set_cached_content(content) else: # @todo: we probably want to have 'cache hit' counters so we can # measure the efficacy of our caches pass # see if the last-modified at hasn't changed, if not return a 302 # (Not Modified) # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime( "%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not # Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() response = HttpResponse( content.data, content_type=content.content_type) response['Last-Modified'] = last_modified_at_str return response
def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: # return a 'Bad Request' to browser as we have a malformed # Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # since we fetched it from DB, let's cache it going forward set_cached_content(content) else: # @todo: we probably want to have 'cache hit' counters so we can # measure the efficacy of our caches pass # see if the last-modified at hasn't changed, if not return a 302 # (Not Modified) # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime( "%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not # Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() response = HttpResponse(content.data, content_type=content.content_type) response['Last-Modified'] = last_modified_at_str return response
def process_request(self, request): # look to see if the request is prefixed with 'c4x' tag if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): try: loc = StaticContent.get_location_from_path(request.path) except InvalidLocationError: # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # see if the last-modified at hasn't changed, if not return a 302 (Not Modified) # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime("%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() response = HttpResponse(content.stream_data(), content_type=content.content_type) response['Last-Modified'] = last_modified_at_str return response
def load_asset_from_location(self, location): """ Loads an asset based on its location, either retrieving it from a cache or loading it directly from the contentstore. """ # See if we can load this item from cache. content = get_cached_content(location) if content is None: # Not in cache, so just try and load it from the asset manager. try: content = AssetManager.find(location, as_stream=True) except (ItemNotFoundError, NotFoundError): raise # Now that we fetched it, let's go ahead and try to cache it. We cap this at 1MB # because it's the default for memcached and also we don't want to do too much # buffering in memory when we're serving an actual request. if content.length is not None and content.length < 1048576: content = content.copy_to_in_mem() set_cached_content(content) return content
def process_request(self, request): # look to see if the request is prefixed with an asset prefix tag if (request.path.startswith('/' + XASSET_LOCATION_TAG + '/') or request.path.startswith('/' + AssetLocator.CANONICAL_NAMESPACE)): if AssetLocator.CANONICAL_NAMESPACE in request.path: request.path = request.path.replace('block/', 'block@', 1) try: loc = StaticContent.get_location_from_path(request.path) except (InvalidLocationError, InvalidKeyError): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = AssetManager.find(loc, as_stream=True) except (ItemNotFoundError, NotFoundError): response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden('Unauthorized') if not request.user.is_staff: if getattr( loc, 'deprecated', False ) and not CourseEnrollment.is_enrolled_by_partial( request.user, loc.course_key): return HttpResponseForbidden('Unauthorized') if not getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled( request.user, loc.course_key): return HttpResponseForbidden('Unauthorized') # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime( "%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() # *** File streaming within a byte range *** # If a Range is provided, parse Range attribute of the request # Add Content-Range in the response if Range is structurally correct # Request -> Range attribute structure: "Range: bytes=first-[last]" # Response -> Content-Range attribute structure: "Content-Range: bytes first-last/totalLength" # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 response = None if request.META.get('HTTP_RANGE'): # Data from cache (StaticContent) has no easy byte management, so we use the DB instead (StaticContentStream) if type(content) == StaticContent: content = AssetManager.find(loc, as_stream=True) header_value = request.META['HTTP_RANGE'] try: unit, ranges = parse_range_header(header_value, content.length) except ValueError as exception: # If the header field is syntactically invalid it should be ignored. log.exception(u"%s in Range header: %s for content: %s", exception.message, header_value, unicode(loc)) else: if unit != 'bytes': # Only accept ranges in bytes log.warning( u"Unknown unit in Range header: %s for content: %s", header_value, unicode(loc)) elif len(ranges) > 1: # According to Http/1.1 spec content for multiple ranges should be sent as a multipart message. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 # But we send back the full content. log.warning( u"More than 1 ranges in Range header: %s for content: %s", header_value, unicode(loc)) else: first, last = ranges[0] if 0 <= first <= last < content.length: # If the byte range is satisfiable response = HttpResponse( content.stream_data_in_range(first, last)) response[ 'Content-Range'] = 'bytes {first}-{last}/{length}'.format( first=first, last=last, length=content.length) response['Content-Length'] = str(last - first + 1) response.status_code = 206 # Partial Content else: log.warning( u"Cannot satisfy ranges in Range header: %s for content: %s", header_value, unicode(loc)) return HttpResponse( status=416) # Requested Range Not Satisfiable # If Range header is absent or syntactically invalid return a full content response. if response is None: response = HttpResponse(content.stream_data()) response['Content-Length'] = content.length # "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed response['Accept-Ranges'] = 'bytes' response['Content-Type'] = content.content_type response['Last-Modified'] = last_modified_at_str return response
def process_request(self, request): # look to see if the request is prefixed with an asset prefix tag if ( request.path.startswith('/' + XASSET_LOCATION_TAG + '/') or request.path.startswith('/' + AssetLocator.CANONICAL_NAMESPACE) ): try: loc = StaticContent.get_location_from_path(request.path) except (InvalidLocationError, InvalidKeyError): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden('Unauthorized') if not request.user.is_staff: if getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled_by_partial( request.user, loc.course_key ): return HttpResponseForbidden('Unauthorized') if not getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled( request.user, loc.course_key ): return HttpResponseForbidden('Unauthorized') # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime("%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() # *** File streaming within a byte range *** # If a Range is provided, parse Range attribute of the request # Add Content-Range in the response if Range is structurally correct # Request -> Range attribute structure: "Range: bytes=first-[last]" # Response -> Content-Range attribute structure: "Content-Range: bytes first-last/totalLength" # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 response = None if request.META.get('HTTP_RANGE'): # Data from cache (StaticContent) has no easy byte management, so we use the DB instead (StaticContentStream) if type(content) == StaticContent: content = contentstore().find(loc, as_stream=True) header_value = request.META['HTTP_RANGE'] try: unit, ranges = parse_range_header(header_value, content.length) except ValueError as exception: # If the header field is syntactically invalid it should be ignored. log.exception( u"%s in Range header: %s for content: %s", exception.message, header_value, unicode(loc) ) else: if unit != 'bytes': # Only accept ranges in bytes log.warning(u"Unknown unit in Range header: %s for content: %s", header_value, unicode(loc)) elif len(ranges) > 1: # According to Http/1.1 spec content for multiple ranges should be sent as a multipart message. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 # But we send back the full content. log.warning( u"More than 1 ranges in Range header: %s for content: %s", header_value, unicode(loc) ) else: first, last = ranges[0] if 0 <= first <= last < content.length: # If the byte range is satisfiable response = HttpResponse(content.stream_data_in_range(first, last)) response['Content-Range'] = 'bytes {first}-{last}/{length}'.format( first=first, last=last, length=content.length ) response['Content-Length'] = str(last - first + 1) response.status_code = 206 # Partial Content else: log.warning( u"Cannot satisfy ranges in Range header: %s for content: %s", header_value, unicode(loc) ) return HttpResponse(status=416) # Requested Range Not Satisfiable # If Range header is absent or syntactically invalid return a full content response. if response is None: response = HttpResponse(content.stream_data()) response['Content-Length'] = content.length # "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed response['Accept-Ranges'] = 'bytes' response['Content-Type'] = content.content_type response['Last-Modified'] = last_modified_at_str return response
def process_request(self, request): # look to see if the request is prefixed with an asset prefix tag if request.path.startswith("/" + XASSET_LOCATION_TAG + "/") or request.path.startswith( "/" + AssetLocator.CANONICAL_NAMESPACE ): try: loc = StaticContent.get_location_from_path(request.path) except (InvalidLocationError, InvalidKeyError): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden("Unauthorized") if not request.user.is_staff: if getattr(loc, "deprecated", False) and not CourseEnrollment.is_enrolled_by_partial( request.user, loc.course_key ): return HttpResponseForbidden("Unauthorized") if not getattr(loc, "deprecated", False) and not CourseEnrollment.is_enrolled( request.user, loc.course_key ): return HttpResponseForbidden("Unauthorized") # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime("%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if "HTTP_IF_MODIFIED_SINCE" in request.META: if_modified_since = request.META["HTTP_IF_MODIFIED_SINCE"] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() # *** File streaming within a byte range *** # If a Range is provided, parse Range attribute of the request # Add Content-Range in the response if Range is structurally correct # Request -> Range attribute structure: "Range: bytes=first-[last]" # Response -> Content-Range attribute structure: "Content-Range: bytes first-last/totalLength" response = None if request.META.get("HTTP_RANGE"): # Data from cache (StaticContent) has no easy byte management, so we use the DB instead (StaticContentStream) if type(content) == StaticContent: content = contentstore().find(loc, as_stream=True) # Let's parse the Range header, bytes=first-[last] range_header = request.META["HTTP_RANGE"] if "=" in range_header: unit, byte_range = range_header.split("=") # "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed if unit == "bytes" and "-" in byte_range: first, last = byte_range.split("-") # "first" must be a valid integer try: first = int(first) except ValueError: pass if type(first) is int: # "last" default value is the last byte of the file # Users can ask "bytes=0-" to request the whole file when they don't know the length try: last = int(last) except ValueError: last = content.length - 1 if 0 <= first <= last < content.length: # Valid Range attribute response = HttpResponse(content.stream_data_in_range(first, last)) response["Content-Range"] = "bytes {first}-{last}/{length}".format( first=first, last=last, length=content.length ) response["Content-Length"] = str(last - first + 1) response.status_code = 206 # HTTP_206_PARTIAL_CONTENT if not response: # Malformed Range attribute response = HttpResponse() response.status_code = 400 # HTTP_400_BAD_REQUEST return response else: # No Range attribute response = HttpResponse(content.stream_data()) response["Content-Length"] = content.length response["Accept-Ranges"] = "bytes" response["Content-Type"] = content.content_type response["Last-Modified"] = last_modified_at_str return response
def process_request(self, request): # look to see if the request is prefixed with an asset prefix tag if (request.path.startswith('/' + XASSET_LOCATION_TAG + '/') or request.path.startswith('/' + AssetLocator.CANONICAL_NAMESPACE)): try: loc = StaticContent.get_location_from_path(request.path) except (InvalidLocationError, InvalidKeyError): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 return response # first look in our cache so we don't have to round-trip to the DB content = get_cached_content(loc) if content is None: # nope, not in cache, let's fetch from DB try: content = contentstore().find(loc, as_stream=True) except NotFoundError: response = HttpResponse() response.status_code = 404 return response # 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) else: # NOP here, but we may wish to add a "cache-hit" counter in the future pass # Check that user has access to content if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden('Unauthorized') if not request.user.is_staff: if getattr( loc, 'deprecated', False ) and not CourseEnrollment.is_enrolled_by_partial( request.user, loc.course_key): return HttpResponseForbidden('Unauthorized') if not getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled( request.user, loc.course_key): return HttpResponseForbidden('Unauthorized') # convert over the DB persistent last modified timestamp to a HTTP compatible # timestamp, so we can simply compare the strings last_modified_at_str = content.last_modified_at.strftime( "%a, %d-%b-%Y %H:%M:%S GMT") # see if the client has cached this content, if so then compare the # timestamps, if they are the same then just return a 304 (Not Modified) if 'HTTP_IF_MODIFIED_SINCE' in request.META: if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] if if_modified_since == last_modified_at_str: return HttpResponseNotModified() # *** File streaming within a byte range *** # If a Range is provided, parse Range attribute of the request # Add Content-Range in the response if Range is structurally correct # Request -> Range attribute structure: "Range: bytes=first-[last]" # Response -> Content-Range attribute structure: "Content-Range: bytes first-last/totalLength" response = None if request.META.get('HTTP_RANGE'): # Data from cache (StaticContent) has no easy byte management, so we use the DB instead (StaticContentStream) if type(content) == StaticContent: content = contentstore().find(loc, as_stream=True) # Let's parse the Range header, bytes=first-[last] range_header = request.META['HTTP_RANGE'] if '=' in range_header: unit, byte_range = range_header.split('=') # "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed if unit == 'bytes' and '-' in byte_range: first, last = byte_range.split('-') # "first" must be a valid integer try: first = int(first) except ValueError: pass if type(first) is int: # "last" default value is the last byte of the file # Users can ask "bytes=0-" to request the whole file when they don't know the length try: last = int(last) except ValueError: last = content.length - 1 if 0 <= first <= last < content.length: # Valid Range attribute response = HttpResponse( content.stream_data_in_range(first, last)) response[ 'Content-Range'] = 'bytes {first}-{last}/{length}'.format( first=first, last=last, length=content.length) response['Content-Length'] = str(last - first + 1) response.status_code = 206 # HTTP_206_PARTIAL_CONTENT if not response: # Malformed Range attribute response = HttpResponse() response.status_code = 400 # HTTP_400_BAD_REQUEST return response else: # No Range attribute response = HttpResponse(content.stream_data()) response['Content-Length'] = content.length response['Accept-Ranges'] = 'bytes' response['Content-Type'] = content.content_type response['Last-Modified'] = last_modified_at_str return response