1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
|
from django.http.response import FileResponse
class RangedFileReader(object):
"""
Wraps a file like object with an iterator that runs over part (or all) of
the file defined by start and stop. Blocks of block_size will be returned
from the starting position, up to, but not including the stop point.
"""
block_size = 8192
def __init__(self, file_like, start=0, stop=float('inf'), block_size=None):
"""
Args:
file_like (File): A file-like object.
start (int): Where to start reading the file.
stop (Optional[int]:float): Where to end reading the file.
Defaults to infinity.
block_size (Optional[int]): The block_size to read with.
"""
self.f = file_like
self.size = len(self.f.read())
self.block_size = block_size or RangedFileReader.block_size
self.start = start
self.stop = stop
def __iter__(self):
"""
Reads the data in chunks.
"""
self.f.seek(self.start)
position = self.start
while position < self.stop:
data = self.f.read(min(self.block_size, self.stop - position))
if not data:
break
yield data
position += self.block_size
def parse_range_header(self, header, resource_size):
"""
Parses a range header into a list of two-tuples (start, stop) where
`start` is the starting byte of the range (inclusive) and
`stop` is the ending byte position of the range (exclusive).
Args:
header (str): The HTTP_RANGE request header.
resource_size (int): The size of the file in bytes.
Returns:
None if the value of the header is not syntatically valid.
"""
if not header or '=' not in header:
return None
ranges = []
units, range_ = header.split('=', 1)
units = units.strip().lower()
if units != 'bytes':
return None
for val in range_.split(','):
val = val.strip()
if '-' not in val:
return None
if val.startswith('-'):
# suffix-byte-range-spec: this form specifies the last N bytes
# of an entity-body.
start = resource_size + int(val)
if start < 0:
start = 0
stop = resource_size
else:
# byte-range-spec: first-byte-pos "-" [last-byte-pos].
start, stop = val.split('-', 1)
start = int(start)
# The +1 is here since we want the stopping point to be
# exclusive, whereas in the HTTP spec, the last-byte-pos
# is inclusive.
stop = int(stop) + 1 if stop else resource_size
if start >= stop:
return None
ranges.append((start, stop))
return ranges
class RangedFileResponse(FileResponse):
"""
This is a modified FileResponse that returns `Content-Range` headers with
the response, so browsers that request the file, can stream the response
properly.
"""
def __init__(self, request, file, *args, **kwargs):
"""
RangedFileResponse constructor also requires a request, which
checks whether range headers should be added to the response.
Args:
request(WGSIRequest): The Django request object.
file (File): A file-like object.
"""
self.ranged_file = RangedFileReader(file)
super(RangedFileResponse, self).__init__(
self.ranged_file, *args, **kwargs
)
if 'HTTP_RANGE' in request.META:
self.add_range_headers(request.META['HTTP_RANGE'])
def add_range_headers(self, range_header):
"""
Adds several headers that are necessary for a streaming file
response, in order for Safari to play audio files. Also
sets the HTTP status_code to 206 (partial content).
Args:
range_header (str): Browser HTTP_RANGE request header.
"""
self['Accept-Ranges'] = 'bytes'
size = self.ranged_file.size
try:
ranges = self.ranged_file.parse_range_header(range_header, size)
except ValueError:
ranges = None
# Only handle syntactically valid headers, that are simple (no
# multipart byteranges).
if ranges is not None and len(ranges) == 1:
start, stop = ranges[0]
if start >= size:
# Requested range not satisfiable.
self.status_code = 416
return
if stop >= size:
stop = size
self.ranged_file.start = start
self.ranged_file.stop = stop
self['Content-Range'] = 'bytes %d-%d/%d' % (start, stop - 1, size)
self['Content-Length'] = stop - start
self.status_code = 206
|