181 lines
5.5 KiB
Python
181 lines
5.5 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
|
||
|
class IfRange:
|
||
|
"""Very simple object that represents the `If-Range` header in parsed
|
||
|
form. It will either have neither a etag or date or one of either but
|
||
|
never both.
|
||
|
|
||
|
.. versionadded:: 0.7
|
||
|
"""
|
||
|
|
||
|
def __init__(self, etag=None, date=None):
|
||
|
#: The etag parsed and unquoted. Ranges always operate on strong
|
||
|
#: etags so the weakness information is not necessary.
|
||
|
self.etag = etag
|
||
|
#: The date in parsed format or `None`.
|
||
|
self.date = date
|
||
|
|
||
|
def to_header(self):
|
||
|
"""Converts the object back into an HTTP header."""
|
||
|
if self.date is not None:
|
||
|
return http.http_date(self.date)
|
||
|
if self.etag is not None:
|
||
|
return http.quote_etag(self.etag)
|
||
|
return ""
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.to_header()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return f"<{type(self).__name__} {str(self)!r}>"
|
||
|
|
||
|
|
||
|
class Range:
|
||
|
"""Represents a ``Range`` header. All methods only support only
|
||
|
bytes as the unit. Stores a list of ranges if given, but the methods
|
||
|
only work if only one range is provided.
|
||
|
|
||
|
:raise ValueError: If the ranges provided are invalid.
|
||
|
|
||
|
.. versionchanged:: 0.15
|
||
|
The ranges passed in are validated.
|
||
|
|
||
|
.. versionadded:: 0.7
|
||
|
"""
|
||
|
|
||
|
def __init__(self, units, ranges):
|
||
|
#: The units of this range. Usually "bytes".
|
||
|
self.units = units
|
||
|
#: A list of ``(begin, end)`` tuples for the range header provided.
|
||
|
#: The ranges are non-inclusive.
|
||
|
self.ranges = ranges
|
||
|
|
||
|
for start, end in ranges:
|
||
|
if start is None or (end is not None and (start < 0 or start >= end)):
|
||
|
raise ValueError(f"{(start, end)} is not a valid range.")
|
||
|
|
||
|
def range_for_length(self, length):
|
||
|
"""If the range is for bytes, the length is not None and there is
|
||
|
exactly one range and it is satisfiable it returns a ``(start, stop)``
|
||
|
tuple, otherwise `None`.
|
||
|
"""
|
||
|
if self.units != "bytes" or length is None or len(self.ranges) != 1:
|
||
|
return None
|
||
|
start, end = self.ranges[0]
|
||
|
if end is None:
|
||
|
end = length
|
||
|
if start < 0:
|
||
|
start += length
|
||
|
if http.is_byte_range_valid(start, end, length):
|
||
|
return start, min(end, length)
|
||
|
return None
|
||
|
|
||
|
def make_content_range(self, length):
|
||
|
"""Creates a :class:`~werkzeug.datastructures.ContentRange` object
|
||
|
from the current range and given content length.
|
||
|
"""
|
||
|
rng = self.range_for_length(length)
|
||
|
if rng is not None:
|
||
|
return ContentRange(self.units, rng[0], rng[1], length)
|
||
|
return None
|
||
|
|
||
|
def to_header(self):
|
||
|
"""Converts the object back into an HTTP header."""
|
||
|
ranges = []
|
||
|
for begin, end in self.ranges:
|
||
|
if end is None:
|
||
|
ranges.append(f"{begin}-" if begin >= 0 else str(begin))
|
||
|
else:
|
||
|
ranges.append(f"{begin}-{end - 1}")
|
||
|
return f"{self.units}={','.join(ranges)}"
|
||
|
|
||
|
def to_content_range_header(self, length):
|
||
|
"""Converts the object into `Content-Range` HTTP header,
|
||
|
based on given length
|
||
|
"""
|
||
|
range = self.range_for_length(length)
|
||
|
if range is not None:
|
||
|
return f"{self.units} {range[0]}-{range[1] - 1}/{length}"
|
||
|
return None
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.to_header()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return f"<{type(self).__name__} {str(self)!r}>"
|
||
|
|
||
|
|
||
|
def _callback_property(name):
|
||
|
def fget(self):
|
||
|
return getattr(self, name)
|
||
|
|
||
|
def fset(self, value):
|
||
|
setattr(self, name, value)
|
||
|
if self.on_update is not None:
|
||
|
self.on_update(self)
|
||
|
|
||
|
return property(fget, fset)
|
||
|
|
||
|
|
||
|
class ContentRange:
|
||
|
"""Represents the content range header.
|
||
|
|
||
|
.. versionadded:: 0.7
|
||
|
"""
|
||
|
|
||
|
def __init__(self, units, start, stop, length=None, on_update=None):
|
||
|
assert http.is_byte_range_valid(start, stop, length), "Bad range provided"
|
||
|
self.on_update = on_update
|
||
|
self.set(start, stop, length, units)
|
||
|
|
||
|
#: The units to use, usually "bytes"
|
||
|
units = _callback_property("_units")
|
||
|
#: The start point of the range or `None`.
|
||
|
start = _callback_property("_start")
|
||
|
#: The stop point of the range (non-inclusive) or `None`. Can only be
|
||
|
#: `None` if also start is `None`.
|
||
|
stop = _callback_property("_stop")
|
||
|
#: The length of the range or `None`.
|
||
|
length = _callback_property("_length")
|
||
|
|
||
|
def set(self, start, stop, length=None, units="bytes"):
|
||
|
"""Simple method to update the ranges."""
|
||
|
assert http.is_byte_range_valid(start, stop, length), "Bad range provided"
|
||
|
self._units = units
|
||
|
self._start = start
|
||
|
self._stop = stop
|
||
|
self._length = length
|
||
|
if self.on_update is not None:
|
||
|
self.on_update(self)
|
||
|
|
||
|
def unset(self):
|
||
|
"""Sets the units to `None` which indicates that the header should
|
||
|
no longer be used.
|
||
|
"""
|
||
|
self.set(None, None, units=None)
|
||
|
|
||
|
def to_header(self):
|
||
|
if self.units is None:
|
||
|
return ""
|
||
|
if self.length is None:
|
||
|
length = "*"
|
||
|
else:
|
||
|
length = self.length
|
||
|
if self.start is None:
|
||
|
return f"{self.units} */{length}"
|
||
|
return f"{self.units} {self.start}-{self.stop - 1}/{length}"
|
||
|
|
||
|
def __bool__(self):
|
||
|
return self.units is not None
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.to_header()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return f"<{type(self).__name__} {str(self)!r}>"
|
||
|
|
||
|
|
||
|
# circular dependencies
|
||
|
from .. import http
|