def _process_recover_response(self, response): """Process the response from an HTTP request to recover from failure. This is everything that must be done after a request that doesn't require network I/O (or other I/O). This is based on the `sans-I/O`_ philosophy. Args: response (object): The HTTP response object. Raises: ~google.resumable_media.exceptions.InvalidResponse: If the status code is not 308. ~google.resumable_media.exceptions.InvalidResponse: If the status code is 308 and the ``range`` header is not of the form ``bytes 0-{end}``. .. _sans-I/O: https://sans-io.readthedocs.io/ """ _helpers.require_status_code(response, (PERMANENT_REDIRECT, )) headers = _helpers.get_headers(response) if _helpers.RANGE_HEADER in headers: bytes_range = headers[_helpers.RANGE_HEADER] match = _BYTES_RANGE_RE.match(bytes_range) if match is None: raise exceptions.InvalidResponse( response, u'Unexpected "range" header', bytes_range, u'Expected to be of the form "bytes=0-{end}"') self._bytes_uploaded = int(match.group(u'end_byte')) + 1 else: # In this case, the upload has not "begun". self._bytes_uploaded = 0 self._stream.seek(self._bytes_uploaded) self._invalid = False
def require_status_code(response, status_codes, get_status_code, callback=do_nothing): """Require a response has a status code among a list. Args: response (object): The HTTP response object. status_codes (tuple): The acceptable status codes. get_status_code (Callable[Any, int]): Helper to get a status code from a response. callback (Optional[Callable]): A callback that takes no arguments, to be executed when an exception is being raised. Returns: int: The status code. Raises: ~google.resumable_media.exceptions.InvalidResponse: If the status code is not one of the values in ``status_codes``. """ status_code = get_status_code(response) if status_code not in status_codes: callback() raise exceptions.InvalidResponse(response, u'Request failed with status code', status_code, u'Expected one of', *status_codes) return status_code
def header_required(response, name, get_headers, callback=do_nothing): """Checks that a specific header is in a headers dictionary. Args: response (object): An HTTP response object, expected to have a ``headers`` attribute that is a ``Mapping[str, str]``. name (str): The name of a required header. get_headers (Callable[Any, Mapping[str, str]]): Helper to get headers from an HTTP response. callback (Optional[Callable]): A callback that takes no arguments, to be executed when an exception is being raised. Returns: str: The desired header. Raises: ~google.resumable_media.exceptions.InvalidResponse: If the header is missing. """ headers = get_headers(response) if name not in headers: callback() raise exceptions.InvalidResponse( response, u'Response headers must contain header', name) return headers[name]
def _get_range_info(response, callback=_helpers.do_nothing): """Get the start, end and total bytes from a content range header. Args: response (object): An HTTP response object. callback (Optional[Callable]): A callback that takes no arguments, to be executed when an exception is being raised. Returns: Tuple[int, int, int]: The start byte, end byte and total bytes. Raises: ~google.resumable_media.exceptions.InvalidResponse: If the ``Content-Range`` header is not of the form ``bytes {start}-{end}/{total}``. """ content_range = _helpers.header_required(response, _helpers.CONTENT_RANGE_HEADER) match = _CONTENT_RANGE_RE.match(content_range) if match is None: callback() raise exceptions.InvalidResponse( response, u'Unexpected content-range header', content_range, u'Expected to be of the form "bytes {start}-{end}/{total}"') return (int(match.group(u'start_byte')), int(match.group(u'end_byte')), int(match.group(u'total_bytes')))
def _process_response(self, response): """Process the response from an HTTP request. This is everything that must be done after a request that doesn't require network I/O (or other I/O). This is based on the `sans-I/O`_ philosophy. Updates the current state after consuming a chunk. First, increments ``bytes_downloaded`` by the number of bytes in the ``content-length`` header. If ``total_bytes`` is already set, this assumes (but does not check) that we already have the correct value and doesn't bother to check that it agrees with the headers. We expect the **total** length to be in the ``content-range`` header, but this header is only present on requests which sent the ``range`` header. This response header should be of the form ``bytes {start}-{end}/{total}`` and ``{end} - {start} + 1`` should be the same as the ``Content-Length``. Args: response (object): The HTTP response object (need headers). Raises: ~google.resumable_media.exceptions.InvalidResponse: If the number of bytes in the body doesn't match the content length header. .. _sans-I/O: https://sans-io.readthedocs.io/ """ # Verify the response before updating the current instance. _helpers.require_status_code(response, _ACCEPTABLE_STATUS_CODES, callback=self._make_invalid) content_length = _helpers.header_required(response, u'content-length', callback=self._make_invalid) num_bytes = int(content_length) _, end_byte, total_bytes = _get_range_info(response, callback=self._make_invalid) response_body = _helpers.get_body(response) if len(response_body) != num_bytes: self._make_invalid() raise exceptions.InvalidResponse( response, u'Response is different size than content-length', u'Expected', num_bytes, u'Received', len(response_body)) # First update ``bytes_downloaded``. self._bytes_downloaded += num_bytes # If the end byte is past ``end`` or ``total_bytes - 1`` we are done. if self.end is not None and end_byte >= self.end: self._finished = True elif end_byte >= total_bytes - 1: self._finished = True # NOTE: We only use ``total_bytes`` if not already known. if self.total_bytes is None: self._total_bytes = total_bytes # Write the response body to the stream. self._stream.write(response_body)
def _process_response(self, response): """Process the response from an HTTP request. This is everything that must be done after a request that doesn't require network I/O (or other I/O). This is based on the `sans-I/O`_ philosophy. Args: response (object): The HTTP response object. Raises: ~google.resumable_media.exceptions.InvalidResponse: If the status code is 308 and the ``range`` header is not of the form ``bytes 0-{end}``. ~google.resumable_media.exceptions.InvalidResponse: If the status code is not 200 or 308. .. _sans-I/O: https://sans-io.readthedocs.io/ """ status_code = _helpers.require_status_code( response, (http_client.OK, resumable_media.PERMANENT_REDIRECT), self._get_status_code, callback=self._make_invalid) if status_code == http_client.OK: json_response = json.loads(self._get_body(response)) self._bytes_uploaded = int(json_response[u'size']) # Tombstone the current upload so it cannot be used again. self._finished = True else: bytes_range = _helpers.header_required(response, _helpers.RANGE_HEADER, self._get_headers, callback=self._make_invalid) match = _BYTES_RANGE_RE.match(bytes_range) if match is None: self._make_invalid() raise exceptions.InvalidResponse( response, u'Unexpected "range" header', bytes_range, u'Expected to be of the form "bytes=0-{end}"') self._bytes_uploaded = int(match.group(u'end_byte')) + 1