Home | History | Annotate | Download | only in webob
      1 import 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 
      8 class 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 
     89 class 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 
    146 def _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