def parse_content_range_header(value, on_update=None): """Parses a range header into a :class:`~werkzeug.datastructures.ContentRange` object or `None` if parsing is not possible. .. versionadded:: 0.7 :param value: a content range header to be parsed. :param on_update: an optional callable that is called every time a value on the :class:`~werkzeug.datastructures.ContentRange` object is changed. """ if value is None: return None try: units, rangedef = (value or '').strip().split(None, 1) except ValueError: return None if '/' not in rangedef: return None rng, length = rangedef.split('/', 1) if length == '*': length = None elif length.isdigit(): length = int(length) else: return None if rng == '*': return ContentRange(units, None, None, length, on_update=on_update) elif '-' not in rng: return None start, stop = rng.split('-', 1) try: start = int(start) stop = int(stop) + 1 except ValueError: return None if is_byte_range_valid(start, stop, length): return ContentRange(units, start, stop, length, on_update=on_update)
def read_range(request, read_data: Callable[[int, int], bytes]) -> None: """ Read an optional ``Range`` header, reads data appropriately via the given callable, writes the data to the request. Only parses a subset of ``Range`` headers that we support: must be set, bytes only, only a single range, the end must be explicitly specified. Raises a ``_HTTPError(http.REQUESTED_RANGE_NOT_SATISFIABLE)`` if parsing is not possible or the header isn't set. Takes a function that will do the actual reading given the start offset and a length to read. The resulting data is written to the request. """ if request.getHeader("range") is None: # Return the whole thing. start = 0 while True: # TODO should probably yield to event loop occasionally... # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3872 data = read_data(start, start + 65536) if not data: request.finish() return request.write(data) start += len(data) range_header = parse_range_header(request.getHeader("range")) if ( range_header is None # failed to parse or range_header.units != "bytes" or len(range_header.ranges) > 1 # more than one range or range_header.ranges[0][1] is None # range without end ): raise _HTTPError(http.REQUESTED_RANGE_NOT_SATISFIABLE) offset, end = range_header.ranges[0] # TODO limit memory usage # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3872 data = read_data(offset, end - offset) request.setResponseCode(http.PARTIAL_CONTENT) if len(data): # For empty bodies the content-range header makes no sense since # the end of the range is inclusive. request.setHeader( "content-range", ContentRange("bytes", offset, offset + len(data)).to_header(), ) request.write(data) request.finish()
def write_share_chunk( self, storage_index, share_number, upload_secret, offset, data ): # type: (bytes, int, bytes, int, bytes) -> Deferred[UploadProgress] """ Upload a chunk of data for a specific share. TODO https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3857 The implementation should retry failed uploads transparently a number of times, so that if a failure percolates up, the caller can assume the failure isn't a short-term blip. Result fires when the upload succeeded, with a boolean indicating whether the _complete_ share (i.e. all chunks, not just this one) has been uploaded. """ url = self._client.relative_url("/v1/immutable/{}/{}".format( _encode_si(storage_index), share_number)) response = yield self._client.request( "PATCH", url, upload_secret=upload_secret, data=data, headers=Headers({ "content-range": [ ContentRange("bytes", offset, offset + len(data)).to_header() ] }), ) if response.code == http.OK: # Upload is still unfinished. finished = False elif response.code == http.CREATED: # Upload is done! finished = True else: raise ClientException(response.code, ) body = yield _decode_cbor(response, _SCHEMAS["immutable_write_share_chunk"]) remaining = RangeMap() for chunk in body["required"]: remaining.set(True, chunk["begin"], chunk["end"]) returnValue(UploadProgress(finished=finished, required=remaining))
def content_range(self) -> ContentRange: """The ``Content-Range`` header as a :class:`~werkzeug.datastructures.ContentRange` object. Available even if the header is not set. .. versionadded:: 0.7 """ def on_update(rng: ContentRange) -> None: if not rng: del self.headers["content-range"] else: self.headers["Content-Range"] = rng.to_header() rv = parse_content_range_header(self.headers.get("content-range"), on_update) # always provide a content range object to make the descriptor # more user friendly. It provides an unset() method that can be # used to remove the header quickly. if rv is None: rv = ContentRange(None, None, None, on_update=on_update) return rv
async def make_conditional(self, request_range: Optional[Range], max_partial_size: Optional[int] = None) -> None: """Make the response conditional to the Arguments: request_range: The range as requested by the request. max_partial_size: The maximum length the server is willing to serve in a single response. Defaults to unlimited. """ self.accept_ranges = "bytes" # Advertise this ability if request_range is None or len( request_range.ranges) == 0: # Not a conditional request return if request_range.units != "bytes" or len(request_range.ranges) > 1: from ..exceptions import RequestRangeNotSatisfiable raise RequestRangeNotSatisfiable() begin, end = request_range.ranges[0] try: complete_length = await self.response.make_conditional( # type: ignore begin, end, max_partial_size) except AttributeError: self.response = self.data_body_class( await self.response.convert_to_sequence()) return await self.make_conditional(request_range, max_partial_size) else: self.content_length = self.response.end - self.response.begin # type: ignore if self.content_length != complete_length: self.content_range = ContentRange( request_range.units, self.response.begin, # type: ignore self.response.end - 1, # type: ignore complete_length, ) self.status_code = 206
def content_range(self, value: ContentRange) -> None: self._set_or_pop_header("Content-Range", value.to_header())
def on_update(rng: ContentRange) -> None: if not rng: del self.headers["content-range"] else: self.headers["Content-Range"] = rng.to_header()