普通文本  |  161行  |  4.63 KB

import re

__all__ = ['Range', 'ContentRange']

_rx_range = re.compile('bytes *= *(\d*) *- *(\d*)', flags=re.I)
_rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])')

class Range(object):
    """
        Represents the Range header.
    """

    def __init__(self, start, end):
        assert end is None or end >= 0, "Bad range end: %r" % end
        self.start = start
        self.end = end # non-inclusive

    def range_for_length(self, length):
        """
            *If* there is only one range, and *if* it is satisfiable by
            the given length, then return a (start, end) non-inclusive range
            of bytes to serve.  Otherwise return None
        """
        if length is None:
            return None
        start, end = self.start, self.end
        if end is None:
            end = length
            if start < 0:
                start += length
        if _is_content_range_valid(start, end, length):
            stop = min(end, length)
            return (start, stop)
        else:
            return None

    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):
        s,e = self.start, self.end
        if e is None:
            r = 'bytes=%s' % s
            if s >= 0:
                r += '-'
            return r
        return 'bytes=%s-%s' % (s, e-1)

    def __repr__(self):
        return '%s(%r, %r)' % (
            self.__class__.__name__,
            self.start, self.end)

    def __iter__(self):
        return iter((self.start, self.end))

    @classmethod
    def parse(cls, header):
        """
            Parse the header; may return None if header is invalid
        """
        m = _rx_range.match(header or '')
        if not m:
            return None
        start, end = m.groups()
        if not start:
            return cls(-int(end), None)
        start = int(start)
        if not end:
            return cls(start, None)
        end = int(end) + 1 # return val is non-inclusive
        if start >= end:
            return None
        return cls(start, end)


class ContentRange(object):

    """
    Represents the Content-Range header

    This header is ``start-stop/length``, where start-stop and length
    can be ``*`` (represented as None in the attributes).
    """

    def __init__(self, start, stop, length):
        if not _is_content_range_valid(start, stop, length):
            raise ValueError(
                "Bad start:stop/length: %r-%r/%r" % (start, stop, length))
        self.start = start
        self.stop = stop # this is python-style range end (non-inclusive)
        self.length = length

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, self)

    def __str__(self):
        if self.length is None:
            length = '*'
        else:
            length = self.length
        if self.start is None:
            assert self.stop is None
            return 'bytes */%s' % length
        stop = self.stop - 1 # from non-inclusive to HTTP-style
        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.
        """
        m = _rx_content_range.match(value or '')
        if not m:
            return None
        s, e, l = m.groups()
        if s:
            s = int(s)
            e = int(e) + 1
        l = l and int(l)
        if not _is_content_range_valid(s, e, l, response=True):
            return None
        return cls(s, e, l)


def _is_content_range_valid(start, stop, length, response=False):
    if (start is None) != (stop is None):
        return False
    elif start is None:
        return length is None or length >= 0
    elif length is None:
        return 0 <= start < stop
    elif start >= stop:
        return False
    elif response and stop > length:
        # "content-range: bytes 0-50/10" is invalid for a response
        # "range: bytes 0-50" is valid for a request to a 10-bytes entity
        return False
    else:
        return 0 <= start < length