__init__.py 4.9 KB

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