django-ranged-response-0.2.0/0000755000372000037200000000000013133351333016703 5ustar travistravis00000000000000django-ranged-response-0.2.0/django_ranged_response.egg-info/0000755000372000037200000000000013133351333025075 5ustar travistravis00000000000000django-ranged-response-0.2.0/django_ranged_response.egg-info/PKG-INFO0000644000372000037200000000047113133351333026174 0ustar travistravis00000000000000Metadata-Version: 1.0 Name: django-ranged-response Version: 0.2.0 Summary: Modified Django FileResponse that adds Content-Range headers. Home-page: https://github.com/wearespindle/django-ranged-fileresponse Author: Spindle Author-email: jeroen@wearespindle.com License: MIT Description: UNKNOWN Platform: UNKNOWN django-ranged-response-0.2.0/django_ranged_response.egg-info/SOURCES.txt0000644000372000037200000000051413133351333026761 0ustar travistravis00000000000000setup.py django_ranged_response.egg-info/PKG-INFO django_ranged_response.egg-info/SOURCES.txt django_ranged_response.egg-info/dependency_links.txt django_ranged_response.egg-info/not-zip-safe django_ranged_response.egg-info/requires.txt django_ranged_response.egg-info/top_level.txt ranged_response/__init__.py test/test_response.pydjango-ranged-response-0.2.0/django_ranged_response.egg-info/dependency_links.txt0000644000372000037200000000000113133351333031143 0ustar travistravis00000000000000 django-ranged-response-0.2.0/django_ranged_response.egg-info/not-zip-safe0000644000372000037200000000000113133351333027323 0ustar travistravis00000000000000 django-ranged-response-0.2.0/django_ranged_response.egg-info/requires.txt0000644000372000037200000000000713133351333027472 0ustar travistravis00000000000000django django-ranged-response-0.2.0/django_ranged_response.egg-info/top_level.txt0000644000372000037200000000002013133351333027617 0ustar travistravis00000000000000ranged_response django-ranged-response-0.2.0/ranged_response/0000755000372000037200000000000013133351333022061 5ustar travistravis00000000000000django-ranged-response-0.2.0/ranged_response/__init__.py0000644000372000037200000001170113133351261024172 0ustar travistravis00000000000000from 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 django-ranged-response-0.2.0/test/0000755000372000037200000000000013133351333017662 5ustar travistravis00000000000000django-ranged-response-0.2.0/test/test_response.py0000644000372000037200000000414213133351261023132 0ustar travistravis00000000000000import io from django.test.client import RequestFactory from django.test.testcases import TestCase from ranged_response import RangedFileResponse class testResponse(TestCase): def setUp(self): self.factory = RequestFactory() def test_begin(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=0-3' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertContent(回應, b'sui2') def test_middle(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=4-9' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertContent(回應, b'khiau2') def test_end(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=10-16' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertContent(回應, b'tsiang5') def test_failing(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=17-20' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertEqual(回應.status_code, 416) def test_overlapping(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=10-20' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertContent(回應, b'tsiang5') def test_more_one_char(self): request = self.factory.get( '/path', HTTP_RANGE='bytes=10-17' ) 回應 = RangedFileResponse( request, io.BytesIO(b'sui2khiau2tsiang5'), content_type='audio/wav' ) self.assertContent(回應, b'tsiang5') def assertContent(self, response, except_response): self.assertEqual(list(response.streaming_content)[0], except_response) django-ranged-response-0.2.0/setup.py0000644000372000037200000000071413133351261020417 0ustar travistravis00000000000000from setuptools import setup description = 'Modified Django FileResponse that adds Content-Range headers.' setup( name='django-ranged-response', version='0.2.0', description=description, url='https://github.com/wearespindle/django-ranged-fileresponse', author='Spindle', author_email='jeroen@wearespindle.com', license='MIT', packages=['ranged_response'], install_requires=[ 'django', ], zip_safe=False, ) django-ranged-response-0.2.0/PKG-INFO0000644000372000037200000000047113133351333020002 0ustar travistravis00000000000000Metadata-Version: 1.0 Name: django-ranged-response Version: 0.2.0 Summary: Modified Django FileResponse that adds Content-Range headers. Home-page: https://github.com/wearespindle/django-ranged-fileresponse Author: Spindle Author-email: jeroen@wearespindle.com License: MIT Description: UNKNOWN Platform: UNKNOWN django-ranged-response-0.2.0/setup.cfg0000644000372000037200000000004613133351333020524 0ustar travistravis00000000000000[egg_info] tag_build = tag_date = 0