summaryrefslogtreecommitdiffstats
path: root/google_appengine/lib/webob/webob/byterange.py
diff options
context:
space:
mode:
Diffstat (limited to 'google_appengine/lib/webob/webob/byterange.py')
-rwxr-xr-xgoogle_appengine/lib/webob/webob/byterange.py295
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)
+