middleware.py 6.82 KB
Newer Older
1 2 3
from django.http import (
    HttpResponse, HttpResponseNotModified, HttpResponseForbidden
)
4
from student.models import CourseEnrollment
5 6

from xmodule.contentstore.django import contentstore
7
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
8 9
from xmodule.modulestore import InvalidLocationError
from opaque_keys import InvalidKeyError
10
from opaque_keys.edx.locator import AssetLocator
11 12 13
from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError

14 15
# TODO: Soon as we have a reasonable way to serialize/deserialize AssetKeys, we need
# to change this file so instead of using course_id_partial, we're just using asset keys
16

17

18 19
class StaticContentServer(object):
    def process_request(self, request):
20 21 22 23 24
        # 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)
        ):
25 26
            try:
                loc = StaticContent.get_location_from_path(request.path)
27
            except (InvalidLocationError, InvalidKeyError):
28
                # return a 'Bad Request' to browser as we have a malformed Location
29
                response = HttpResponse()
30
                response.status_code = 400
31
                return response
32

33
            # first look in our cache so we don't have to round-trip to the DB
Julian Arni committed
34
            content = get_cached_content(loc)
35 36 37
            if content is None:
                # nope, not in cache, let's fetch from DB
                try:
38
                    content = contentstore().find(loc, as_stream=True)
39
                except NotFoundError:
40 41 42
                    response = HttpResponse()
                    response.status_code = 404
                    return response
43

44 45 46 47 48 49 50
                # 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)
51
            else:
52
                # NOP here, but we may wish to add a "cache-hit" counter in the future
53
                pass
54

55 56 57
            # Check that user has access to content
            if getattr(content, "locked", False):
                if not hasattr(request, "user") or not request.user.is_authenticated():
Julian Arni committed
58
                    return HttpResponseForbidden('Unauthorized')
Don Mitchell committed
59 60
                if not request.user.is_staff:
                    if getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled_by_partial(
61
                        request.user, loc.course_key
Don Mitchell committed
62 63 64 65 66 67
                    ):
                        return HttpResponseForbidden('Unauthorized')
                    if not getattr(loc, 'deprecated', False) and not CourseEnrollment.is_enrolled(
                        request.user, loc.course_key
                    ):
                        return HttpResponseForbidden('Unauthorized')
68 69

            # convert over the DB persistent last modified timestamp to a HTTP compatible
70
            # timestamp, so we can simply compare the strings
71 72 73 74 75 76 77 78 79
            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()

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
            # *** 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)
118
                                response.status_code = 206  # HTTP_206_PARTIAL_CONTENT
119 120 121
                if not response:
                    # Malformed Range attribute
                    response = HttpResponse()
122
                    response.status_code = 400  # HTTP_400_BAD_REQUEST
123 124 125 126 127 128 129 130 131
                    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
132 133
            response['Last-Modified'] = last_modified_at_str

134
            return response