123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- 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
|