1from __future__ import annotations
2
3
4class IfRange:
5 """Very simple object that represents the `If-Range` header in parsed
6 form. It will either have neither a etag or date or one of either but
7 never both.
8
9 .. versionadded:: 0.7
10 """
11
12 def __init__(self, etag=None, date=None):
13 #: The etag parsed and unquoted. Ranges always operate on strong
14 #: etags so the weakness information is not necessary.
15 self.etag = etag
16 #: The date in parsed format or `None`.
17 self.date = date
18
19 def to_header(self):
20 """Converts the object back into an HTTP header."""
21 if self.date is not None:
22 return http.http_date(self.date)
23 if self.etag is not None:
24 return http.quote_etag(self.etag)
25 return ""
26
27 def __str__(self):
28 return self.to_header()
29
30 def __repr__(self):
31 return f"<{type(self).__name__} {str(self)!r}>"
32
33
34class Range:
35 """Represents a ``Range`` header. All methods only support only
36 bytes as the unit. Stores a list of ranges if given, but the methods
37 only work if only one range is provided.
38
39 :raise ValueError: If the ranges provided are invalid.
40
41 .. versionchanged:: 0.15
42 The ranges passed in are validated.
43
44 .. versionadded:: 0.7
45 """
46
47 def __init__(self, units, ranges):
48 #: The units of this range. Usually "bytes".
49 self.units = units
50 #: A list of ``(begin, end)`` tuples for the range header provided.
51 #: The ranges are non-inclusive.
52 self.ranges = ranges
53
54 for start, end in ranges:
55 if start is None or (end is not None and (start < 0 or start >= end)):
56 raise ValueError(f"{(start, end)} is not a valid range.")
57
58 def range_for_length(self, length):
59 """If the range is for bytes, the length is not None and there is
60 exactly one range and it is satisfiable it returns a ``(start, stop)``
61 tuple, otherwise `None`.
62 """
63 if self.units != "bytes" or length is None or len(self.ranges) != 1:
64 return None
65 start, end = self.ranges[0]
66 if end is None:
67 end = length
68 if start < 0:
69 start += length
70 if http.is_byte_range_valid(start, end, length):
71 return start, min(end, length)
72 return None
73
74 def make_content_range(self, length):
75 """Creates a :class:`~werkzeug.datastructures.ContentRange` object
76 from the current range and given content length.
77 """
78 rng = self.range_for_length(length)
79 if rng is not None:
80 return ContentRange(self.units, rng[0], rng[1], length)
81 return None
82
83 def to_header(self):
84 """Converts the object back into an HTTP header."""
85 ranges = []
86 for begin, end in self.ranges:
87 if end is None:
88 ranges.append(f"{begin}-" if begin >= 0 else str(begin))
89 else:
90 ranges.append(f"{begin}-{end - 1}")
91 return f"{self.units}={','.join(ranges)}"
92
93 def to_content_range_header(self, length):
94 """Converts the object into `Content-Range` HTTP header,
95 based on given length
96 """
97 range = self.range_for_length(length)
98 if range is not None:
99 return f"{self.units} {range[0]}-{range[1] - 1}/{length}"
100 return None
101
102 def __str__(self):
103 return self.to_header()
104
105 def __repr__(self):
106 return f"<{type(self).__name__} {str(self)!r}>"
107
108
109def _callback_property(name):
110 def fget(self):
111 return getattr(self, name)
112
113 def fset(self, value):
114 setattr(self, name, value)
115 if self.on_update is not None:
116 self.on_update(self)
117
118 return property(fget, fset)
119
120
121class ContentRange:
122 """Represents the content range header.
123
124 .. versionadded:: 0.7
125 """
126
127 def __init__(self, units, start, stop, length=None, on_update=None):
128 assert http.is_byte_range_valid(start, stop, length), "Bad range provided"
129 self.on_update = on_update
130 self.set(start, stop, length, units)
131
132 #: The units to use, usually "bytes"
133 units = _callback_property("_units")
134 #: The start point of the range or `None`.
135 start = _callback_property("_start")
136 #: The stop point of the range (non-inclusive) or `None`. Can only be
137 #: `None` if also start is `None`.
138 stop = _callback_property("_stop")
139 #: The length of the range or `None`.
140 length = _callback_property("_length")
141
142 def set(self, start, stop, length=None, units="bytes"):
143 """Simple method to update the ranges."""
144 assert http.is_byte_range_valid(start, stop, length), "Bad range provided"
145 self._units = units
146 self._start = start
147 self._stop = stop
148 self._length = length
149 if self.on_update is not None:
150 self.on_update(self)
151
152 def unset(self):
153 """Sets the units to `None` which indicates that the header should
154 no longer be used.
155 """
156 self.set(None, None, units=None)
157
158 def to_header(self):
159 if self.units is None:
160 return ""
161 if self.length is None:
162 length = "*"
163 else:
164 length = self.length
165 if self.start is None:
166 return f"{self.units} */{length}"
167 return f"{self.units} {self.start}-{self.stop - 1}/{length}"
168
169 def __bool__(self):
170 return self.units is not None
171
172 def __str__(self):
173 return self.to_header()
174
175 def __repr__(self):
176 return f"<{type(self).__name__} {str(self)!r}>"
177
178
179# circular dependencies
180from .. import http