1import re 2 3__all__ = ['Range', 'ContentRange'] 4 5_rx_range = re.compile('bytes *= *(\d*) *- *(\d*)', flags=re.I) 6_rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])') 7 8class Range(object): 9 """ 10 Represents the Range header. 11 """ 12 13 def __init__(self, start, end): 14 assert end is None or end >= 0, "Bad range end: %r" % end 15 self.start = start 16 self.end = end # non-inclusive 17 18 def range_for_length(self, length): 19 """ 20 *If* there is only one range, and *if* it is satisfiable by 21 the given length, then return a (start, end) non-inclusive range 22 of bytes to serve. Otherwise return None 23 """ 24 if length is None: 25 return None 26 start, end = self.start, self.end 27 if end is None: 28 end = length 29 if start < 0: 30 start += length 31 if _is_content_range_valid(start, end, length): 32 stop = min(end, length) 33 return (start, stop) 34 else: 35 return None 36 37 def content_range(self, length): 38 """ 39 Works like range_for_length; returns None or a ContentRange object 40 41 You can use it like:: 42 43 response.content_range = req.range.content_range(response.content_length) 44 45 Though it's still up to you to actually serve that content range! 46 """ 47 range = self.range_for_length(length) 48 if range is None: 49 return None 50 return ContentRange(range[0], range[1], length) 51 52 def __str__(self): 53 s,e = self.start, self.end 54 if e is None: 55 r = 'bytes=%s' % s 56 if s >= 0: 57 r += '-' 58 return r 59 return 'bytes=%s-%s' % (s, e-1) 60 61 def __repr__(self): 62 return '%s(%r, %r)' % ( 63 self.__class__.__name__, 64 self.start, self.end) 65 66 def __iter__(self): 67 return iter((self.start, self.end)) 68 69 @classmethod 70 def parse(cls, header): 71 """ 72 Parse the header; may return None if header is invalid 73 """ 74 m = _rx_range.match(header or '') 75 if not m: 76 return None 77 start, end = m.groups() 78 if not start: 79 return cls(-int(end), None) 80 start = int(start) 81 if not end: 82 return cls(start, None) 83 end = int(end) + 1 # return val is non-inclusive 84 if start >= end: 85 return None 86 return cls(start, end) 87 88 89class ContentRange(object): 90 91 """ 92 Represents the Content-Range header 93 94 This header is ``start-stop/length``, where start-stop and length 95 can be ``*`` (represented as None in the attributes). 96 """ 97 98 def __init__(self, start, stop, length): 99 if not _is_content_range_valid(start, stop, length): 100 raise ValueError( 101 "Bad start:stop/length: %r-%r/%r" % (start, stop, length)) 102 self.start = start 103 self.stop = stop # this is python-style range end (non-inclusive) 104 self.length = length 105 106 def __repr__(self): 107 return '<%s %s>' % (self.__class__.__name__, self) 108 109 def __str__(self): 110 if self.length is None: 111 length = '*' 112 else: 113 length = self.length 114 if self.start is None: 115 assert self.stop is None 116 return 'bytes */%s' % length 117 stop = self.stop - 1 # from non-inclusive to HTTP-style 118 return 'bytes %s-%s/%s' % (self.start, stop, length) 119 120 def __iter__(self): 121 """ 122 Mostly so you can unpack this, like: 123 124 start, stop, length = res.content_range 125 """ 126 return iter([self.start, self.stop, self.length]) 127 128 @classmethod 129 def parse(cls, value): 130 """ 131 Parse the header. May return None if it cannot parse. 132 """ 133 m = _rx_content_range.match(value or '') 134 if not m: 135 return None 136 s, e, l = m.groups() 137 if s: 138 s = int(s) 139 e = int(e) + 1 140 l = l and int(l) 141 if not _is_content_range_valid(s, e, l, response=True): 142 return None 143 return cls(s, e, l) 144 145 146def _is_content_range_valid(start, stop, length, response=False): 147 if (start is None) != (stop is None): 148 return False 149 elif start is None: 150 return length is None or length >= 0 151 elif length is None: 152 return 0 <= start < stop 153 elif start >= stop: 154 return False 155 elif response and stop > length: 156 # "content-range: bytes 0-50/10" is invalid for a response 157 # "range: bytes 0-50" is valid for a request to a 10-bytes entity 158 return False 159 else: 160 return 0 <= start < length 161