Example #1
0
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)
Example #2
0
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()
Example #3
0
    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))
Example #4
0
    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
Example #5
0
    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
Example #6
0
 def content_range(self, value: ContentRange) -> None:
     self._set_or_pop_header("Content-Range", value.to_header())
Example #7
0
 def on_update(rng: ContentRange) -> None:
     if not rng:
         del self.headers["content-range"]
     else:
         self.headers["Content-Range"] = rng.to_header()