diff options
Diffstat (limited to 'google_appengine/lib/webob/webob/byterange.py')
-rwxr-xr-x | google_appengine/lib/webob/webob/byterange.py | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/google_appengine/lib/webob/webob/byterange.py b/google_appengine/lib/webob/webob/byterange.py new file mode 100755 index 0000000..24641c9 --- /dev/null +++ b/google_appengine/lib/webob/webob/byterange.py @@ -0,0 +1,295 @@ +class Range(object): + + """ + Represents the Range header. + + This only represents ``bytes`` ranges, which are the only kind + specified in HTTP. This can represent multiple sets of ranges, + but no place else is this multi-range facility supported. + """ + + def __init__(self, ranges): + for begin, end in ranges: + assert end is None or end >= 0, "Bad ranges: %r" % ranges + self.ranges = ranges + + def satisfiable(self, length): + """ + Returns true if this range can be satisfied by the resource + with the given byte length. + """ + for begin, end in self.ranges: + if end is not None and end >= length: + return False + return True + + def range_for_length(self, length): + """ + *If* there is only one range, and *if* it is satisfiable by + the given length, then return a (begin, end) non-inclusive range + of bytes to serve. Otherwise return None + + If length is None (unknown length), then the resulting range + may be (begin, None), meaning it should be served from that + point. If it's a range with a fixed endpoint we won't know if + it is satisfiable, so this will return None. + """ + if len(self.ranges) != 1: + return None + begin, end = self.ranges[0] + if length is None: + # Unknown; only works with ranges with no end-point + if end is None: + return (begin, end) + return None + if end >= length: + # Overshoots the end + return None + return (begin, end) + + def content_range(self, length): + """ + Works like range_for_length; returns None or a ContentRange object + + You can use it like:: + + response.content_range = req.range.content_range(response.content_length) + + Though it's still up to you to actually serve that content range! + """ + range = self.range_for_length(length) + if range is None: + return None + return ContentRange(range[0], range[1], length) + + def __str__(self): + return self.serialize_bytes('bytes', self.python_ranges_to_bytes(self.ranges)) + + def __repr__(self): + return '<%s ranges=%s>' % ( + self.__class__.__name__, + ', '.join(map(repr, self.ranges))) + + #@classmethod + def parse(cls, header): + """ + Parse the header; may return None if header is invalid + """ + bytes = cls.parse_bytes(header) + if bytes is None: + return None + units, ranges = bytes + if units.lower() != 'bytes': + return None + ranges = cls.bytes_to_python_ranges(ranges) + if ranges is None: + return None + return cls(ranges) + parse = classmethod(parse) + + #@staticmethod + def parse_bytes(header): + """ + Parse a Range header into (bytes, list_of_ranges). Note that the + ranges are *inclusive* (like in HTTP, not like in Python + typically). + + Will return None if the header is invalid + """ + if not header: + raise TypeError( + "The header must not be empty") + ranges = [] + last_end = 0 + try: + (units, range) = header.split("=", 1) + units = units.strip().lower() + for item in range.split(","): + if '-' not in item: + raise ValueError() + if item.startswith('-'): + # This is a range asking for a trailing chunk + if last_end < 0: + raise ValueError('too many end ranges') + begin = int(item) + end = None + last_end = -1 + else: + (begin, end) = item.split("-", 1) + begin = int(begin) + if begin < last_end or last_end < 0: + print begin, last_end + raise ValueError('begin<last_end, or last_end<0') + if not end.strip(): + end = None + else: + end = int(end) + if end is not None and begin > end: + raise ValueError('begin>end') + last_end = end + ranges.append((begin, end)) + except ValueError, e: + # In this case where the Range header is malformed, + # section 14.16 says to treat the request as if the + # Range header was not present. How do I log this? + print e + return None + return (units, ranges) + parse_bytes = staticmethod(parse_bytes) + + #@staticmethod + def serialize_bytes(units, ranges): + """ + Takes the output of parse_bytes and turns it into a header + """ + parts = [] + for begin, end in ranges: + if end is None: + if begin >= 0: + parts.append('%s-' % begin) + else: + parts.append(str(begin)) + else: + if begin < 0: + raise ValueError( + "(%r, %r) should have a non-negative first value" % (begin, end)) + if end < 0: + raise ValueError( + "(%r, %r) should have a non-negative second value" % (begin, end)) + parts.append('%s-%s' % (begin, end)) + return '%s=%s' % (units, ','.join(parts)) + serialize_bytes = staticmethod(serialize_bytes) + + #@staticmethod + def bytes_to_python_ranges(ranges, length=None): + """ + Converts the list-of-ranges from parse_bytes() to a Python-style + list of ranges (non-inclusive end points) + + In the list of ranges, the last item can be None to indicate that + it should go to the end of the file, and the first item can be + negative to indicate that it should start from an offset from the + end. If you give a length then this will not occur (negative + numbers and offsets will be resolved). + + If length is given, and any range is not value, then None is + returned. + """ + result = [] + for begin, end in ranges: + if begin < 0: + if length is None: + result.append((begin, None)) + continue + else: + begin = length - begin + end = length + if begin is None: + begin = 0 + if end is None and length is not None: + end = length + if length is not None and end is not None and end > length: + return None + if end is not None: + end -= 1 + result.append((begin, end)) + return result + bytes_to_python_ranges = staticmethod(bytes_to_python_ranges) + + #@staticmethod + def python_ranges_to_bytes(ranges): + """ + Converts a Python-style list of ranges to what serialize_bytes + expects. + + This is the inverse of bytes_to_python_ranges + """ + result = [] + for begin, end in ranges: + if end is None: + result.append((begin, None)) + else: + result.append((begin, end+1)) + return result + python_ranges_to_bytes = staticmethod(python_ranges_to_bytes) + +class ContentRange(object): + + """ + Represents the Content-Range header + + This header is ``start-stop/length``, where stop and length can be + ``*`` (represented as None in the attributes). + """ + + def __init__(self, start, stop, length): + assert start >= 0, "Bad start: %r" % start + assert stop is None or (stop >= 0 and stop >= start), ( + "Bad stop: %r" % stop) + self.start = start + self.stop = stop + self.length = length + + def __repr__(self): + return '<%s %s>' % ( + self.__class__.__name__, + self) + + def __str__(self): + if self.stop is None: + stop = '*' + else: + stop = self.stop + 1 + if self.length is None: + length = '*' + else: + length = self.length + return 'bytes %s-%s/%s' % (self.start, stop, length) + + def __iter__(self): + """ + Mostly so you can unpack this, like: + + start, stop, length = res.content_range + """ + return iter([self.start, self.stop, self.length]) + + #@classmethod + def parse(cls, value): + """ + Parse the header. May return None if it cannot parse. + """ + if value is None: + return None + value = value.strip() + if not value.startswith('bytes '): + # Unparseable + return None + value = value[len('bytes '):].strip() + if '/' not in value: + # Invalid, no length given + return None + range, length = value.split('/', 1) + if '-' not in range: + # Invalid, no range + return None + start, end = range.split('-', 1) + try: + start = int(start) + if end == '*': + end = None + else: + end = int(end) + if length == '*': + length = None + else: + length = int(length) + except ValueError: + # Parse problem + return None + if end is None: + return cls(start, None, length) + else: + return cls(start, end-1, length) + parse = classmethod(parse) + |