Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/resumable_media/_upload.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

364 statements  

1# Copyright 2017 Google Inc. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Virtual bases classes for uploading media via Google APIs. 

16 

17Supported here are: 

18 

19* simple (media) uploads 

20* multipart uploads that contain both metadata and a small file as payload 

21* resumable uploads (with metadata as well) 

22""" 

23 

24import http.client 

25import json 

26import os 

27import random 

28import re 

29import sys 

30import urllib.parse 

31from xml.etree import ElementTree 

32 

33from google import resumable_media 

34from google.resumable_media import _helpers, common 

35 

36_CONTENT_TYPE_HEADER = "content-type" 

37_CONTENT_RANGE_TEMPLATE = "bytes {:d}-{:d}/{:d}" 

38_RANGE_UNKNOWN_TEMPLATE = "bytes {:d}-{:d}/*" 

39_EMPTY_RANGE_TEMPLATE = "bytes */{:d}" 

40_BOUNDARY_WIDTH = len(str(sys.maxsize - 1)) 

41_BOUNDARY_FORMAT = "==============={{:0{:d}d}}==".format(_BOUNDARY_WIDTH) 

42_MULTIPART_SEP = b"--" 

43_CRLF = b"\r\n" 

44_MULTIPART_BEGIN = b"\r\ncontent-type: application/json; charset=UTF-8\r\n\r\n" 

45_RELATED_HEADER = b'multipart/related; boundary="' 

46_BYTES_RANGE_RE = re.compile(r"bytes=0-(?P<end_byte>\d+)", flags=re.IGNORECASE) 

47_STREAM_ERROR_TEMPLATE = ( 

48 "Bytes stream is in unexpected state. " 

49 "The local stream has had {:d} bytes read from it while " 

50 "{:d} bytes have already been updated (they should match)." 

51) 

52_STREAM_READ_PAST_TEMPLATE = ( 

53 "{:d} bytes have been read from the stream, which exceeds the expected total {:d}." 

54) 

55_DELETE = "DELETE" 

56_POST = "POST" 

57_PUT = "PUT" 

58_UPLOAD_CHECKSUM_MISMATCH_MESSAGE = ( 

59 "The computed ``{}`` checksum, ``{}``, and the checksum reported by the " 

60 "remote host, ``{}``, did not match." 

61) 

62_UPLOAD_METADATA_NO_APPROPRIATE_CHECKSUM_MESSAGE = ( 

63 "Response metadata had no ``{}`` value; checksum could not be validated." 

64) 

65_UPLOAD_HEADER_NO_APPROPRIATE_CHECKSUM_MESSAGE = ( 

66 "Response headers had no ``{}`` value; checksum could not be validated." 

67) 

68_MPU_INITIATE_QUERY = "?uploads" 

69_MPU_PART_QUERY_TEMPLATE = "?partNumber={part}&uploadId={upload_id}" 

70_S3_COMPAT_XML_NAMESPACE = "{http://s3.amazonaws.com/doc/2006-03-01/}" 

71_UPLOAD_ID_NODE = "UploadId" 

72_MPU_FINAL_QUERY_TEMPLATE = "?uploadId={upload_id}" 

73 

74 

75class UploadBase(object): 

76 """Base class for upload helpers. 

77 

78 Defines core shared behavior across different upload types. 

79 

80 Args: 

81 upload_url (str): The URL where the content will be uploaded. 

82 headers (Optional[Mapping[str, str]]): Extra headers that should 

83 be sent with the request, e.g. headers for encrypted data. 

84 

85 Attributes: 

86 upload_url (str): The URL where the content will be uploaded. 

87 """ 

88 

89 def __init__(self, upload_url, headers=None): 

90 self.upload_url = upload_url 

91 if headers is None: 

92 headers = {} 

93 self._headers = headers 

94 self._finished = False 

95 self._retry_strategy = common.RetryStrategy() 

96 

97 @property 

98 def finished(self): 

99 """bool: Flag indicating if the upload has completed.""" 

100 return self._finished 

101 

102 def _process_response(self, response): 

103 """Process the response from an HTTP request. 

104 

105 This is everything that must be done after a request that doesn't 

106 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

107 philosophy. 

108 

109 Args: 

110 response (object): The HTTP response object. 

111 

112 Raises: 

113 ~google.resumable_media.common.InvalidResponse: If the status 

114 code is not 200. 

115 

116 .. _sans-I/O: https://sans-io.readthedocs.io/ 

117 """ 

118 # Tombstone the current upload so it cannot be used again (in either 

119 # failure or success). 

120 self._finished = True 

121 _helpers.require_status_code(response, (http.client.OK,), self._get_status_code) 

122 

123 @staticmethod 

124 def _get_status_code(response): 

125 """Access the status code from an HTTP response. 

126 

127 Args: 

128 response (object): The HTTP response object. 

129 

130 Raises: 

131 NotImplementedError: Always, since virtual. 

132 """ 

133 raise NotImplementedError("This implementation is virtual.") 

134 

135 @staticmethod 

136 def _get_headers(response): 

137 """Access the headers from an HTTP response. 

138 

139 Args: 

140 response (object): The HTTP response object. 

141 

142 Raises: 

143 NotImplementedError: Always, since virtual. 

144 """ 

145 raise NotImplementedError("This implementation is virtual.") 

146 

147 @staticmethod 

148 def _get_body(response): 

149 """Access the response body from an HTTP response. 

150 

151 Args: 

152 response (object): The HTTP response object. 

153 

154 Raises: 

155 NotImplementedError: Always, since virtual. 

156 """ 

157 raise NotImplementedError("This implementation is virtual.") 

158 

159 

160class SimpleUpload(UploadBase): 

161 """Upload a resource to a Google API. 

162 

163 A **simple** media upload sends no metadata and completes the upload 

164 in a single request. 

165 

166 Args: 

167 upload_url (str): The URL where the content will be uploaded. 

168 headers (Optional[Mapping[str, str]]): Extra headers that should 

169 be sent with the request, e.g. headers for encrypted data. 

170 

171 Attributes: 

172 upload_url (str): The URL where the content will be uploaded. 

173 """ 

174 

175 def _prepare_request(self, data, content_type): 

176 """Prepare the contents of an HTTP request. 

177 

178 This is everything that must be done before a request that doesn't 

179 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

180 philosophy. 

181 

182 .. note: 

183 

184 This method will be used only once, so ``headers`` will be 

185 mutated by having a new key added to it. 

186 

187 Args: 

188 data (bytes): The resource content to be uploaded. 

189 content_type (str): The content type for the request. 

190 

191 Returns: 

192 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

193 

194 * HTTP verb for the request (always POST) 

195 * the URL for the request 

196 * the body of the request 

197 * headers for the request 

198 

199 Raises: 

200 ValueError: If the current upload has already finished. 

201 TypeError: If ``data`` isn't bytes. 

202 

203 .. _sans-I/O: https://sans-io.readthedocs.io/ 

204 """ 

205 if self.finished: 

206 raise ValueError("An upload can only be used once.") 

207 

208 if not isinstance(data, bytes): 

209 raise TypeError("`data` must be bytes, received", type(data)) 

210 self._headers[_CONTENT_TYPE_HEADER] = content_type 

211 return _POST, self.upload_url, data, self._headers 

212 

213 def transmit(self, transport, data, content_type, timeout=None): 

214 """Transmit the resource to be uploaded. 

215 

216 Args: 

217 transport (object): An object which can make authenticated 

218 requests. 

219 data (bytes): The resource content to be uploaded. 

220 content_type (str): The content type of the resource, e.g. a JPEG 

221 image has content type ``image/jpeg``. 

222 timeout (Optional[Union[float, Tuple[float, float]]]): 

223 The number of seconds to wait for the server response. 

224 Depending on the retry strategy, a request may be repeated 

225 several times using the same timeout each time. 

226 

227 Can also be passed as a tuple (connect_timeout, read_timeout). 

228 See :meth:`requests.Session.request` documentation for details. 

229 

230 Raises: 

231 NotImplementedError: Always, since virtual. 

232 """ 

233 raise NotImplementedError("This implementation is virtual.") 

234 

235 

236class MultipartUpload(UploadBase): 

237 """Upload a resource with metadata to a Google API. 

238 

239 A **multipart** upload sends both metadata and the resource in a single 

240 (multipart) request. 

241 

242 Args: 

243 upload_url (str): The URL where the content will be uploaded. 

244 headers (Optional[Mapping[str, str]]): Extra headers that should 

245 be sent with the request, e.g. headers for encrypted data. 

246 checksum (Optional([str])): The type of checksum to compute to verify 

247 the integrity of the object. The request metadata will be amended 

248 to include the computed value. Using this option will override a 

249 manually-set checksum value. Supported values are "md5", "crc32c" 

250 and None. The default is None. 

251 

252 Attributes: 

253 upload_url (str): The URL where the content will be uploaded. 

254 """ 

255 

256 def __init__(self, upload_url, headers=None, checksum=None): 

257 super(MultipartUpload, self).__init__(upload_url, headers=headers) 

258 self._checksum_type = checksum 

259 

260 def _prepare_request(self, data, metadata, content_type): 

261 """Prepare the contents of an HTTP request. 

262 

263 This is everything that must be done before a request that doesn't 

264 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

265 philosophy. 

266 

267 .. note: 

268 

269 This method will be used only once, so ``headers`` will be 

270 mutated by having a new key added to it. 

271 

272 Args: 

273 data (bytes): The resource content to be uploaded. 

274 metadata (Mapping[str, str]): The resource metadata, such as an 

275 ACL list. 

276 content_type (str): The content type of the resource, e.g. a JPEG 

277 image has content type ``image/jpeg``. 

278 

279 Returns: 

280 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

281 

282 * HTTP verb for the request (always POST) 

283 * the URL for the request 

284 * the body of the request 

285 * headers for the request 

286 

287 Raises: 

288 ValueError: If the current upload has already finished. 

289 TypeError: If ``data`` isn't bytes. 

290 

291 .. _sans-I/O: https://sans-io.readthedocs.io/ 

292 """ 

293 if self.finished: 

294 raise ValueError("An upload can only be used once.") 

295 

296 if not isinstance(data, bytes): 

297 raise TypeError("`data` must be bytes, received", type(data)) 

298 

299 checksum_object = _helpers._get_checksum_object(self._checksum_type) 

300 if checksum_object is not None: 

301 checksum_object.update(data) 

302 actual_checksum = _helpers.prepare_checksum_digest(checksum_object.digest()) 

303 metadata_key = _helpers._get_metadata_key(self._checksum_type) 

304 metadata[metadata_key] = actual_checksum 

305 

306 content, multipart_boundary = construct_multipart_request( 

307 data, metadata, content_type 

308 ) 

309 multipart_content_type = _RELATED_HEADER + multipart_boundary + b'"' 

310 self._headers[_CONTENT_TYPE_HEADER] = multipart_content_type 

311 

312 return _POST, self.upload_url, content, self._headers 

313 

314 def transmit(self, transport, data, metadata, content_type, timeout=None): 

315 """Transmit the resource to be uploaded. 

316 

317 Args: 

318 transport (object): An object which can make authenticated 

319 requests. 

320 data (bytes): The resource content to be uploaded. 

321 metadata (Mapping[str, str]): The resource metadata, such as an 

322 ACL list. 

323 content_type (str): The content type of the resource, e.g. a JPEG 

324 image has content type ``image/jpeg``. 

325 timeout (Optional[Union[float, Tuple[float, float]]]): 

326 The number of seconds to wait for the server response. 

327 Depending on the retry strategy, a request may be repeated 

328 several times using the same timeout each time. 

329 

330 Can also be passed as a tuple (connect_timeout, read_timeout). 

331 See :meth:`requests.Session.request` documentation for details. 

332 

333 Raises: 

334 NotImplementedError: Always, since virtual. 

335 """ 

336 raise NotImplementedError("This implementation is virtual.") 

337 

338 

339class ResumableUpload(UploadBase): 

340 """Initiate and fulfill a resumable upload to a Google API. 

341 

342 A **resumable** upload sends an initial request with the resource metadata 

343 and then gets assigned an upload ID / upload URL to send bytes to. 

344 Using the upload URL, the upload is then done in chunks (determined by 

345 the user) until all bytes have been uploaded. 

346 

347 Args: 

348 upload_url (str): The URL where the resumable upload will be initiated. 

349 chunk_size (int): The size of each chunk used to upload the resource. 

350 headers (Optional[Mapping[str, str]]): Extra headers that should 

351 be sent with every request. 

352 checksum (Optional([str])): The type of checksum to compute to verify 

353 the integrity of the object. After the upload is complete, the 

354 server-computed checksum of the resulting object will be read 

355 and google.resumable_media.common.DataCorruption will be raised on 

356 a mismatch. The corrupted file will not be deleted from the remote 

357 host automatically. Supported values are "md5", "crc32c" and None. 

358 The default is None. 

359 

360 Attributes: 

361 upload_url (str): The URL where the content will be uploaded. 

362 

363 Raises: 

364 ValueError: If ``chunk_size`` is not a multiple of 

365 :data:`.UPLOAD_CHUNK_SIZE`. 

366 """ 

367 

368 def __init__(self, upload_url, chunk_size, checksum=None, headers=None): 

369 super(ResumableUpload, self).__init__(upload_url, headers=headers) 

370 if chunk_size % resumable_media.UPLOAD_CHUNK_SIZE != 0: 

371 raise ValueError( 

372 "{} KB must divide chunk size".format( 

373 resumable_media.UPLOAD_CHUNK_SIZE / 1024 

374 ) 

375 ) 

376 self._chunk_size = chunk_size 

377 self._stream = None 

378 self._content_type = None 

379 self._bytes_uploaded = 0 

380 self._bytes_checksummed = 0 

381 self._checksum_type = checksum 

382 self._checksum_object = None 

383 self._total_bytes = None 

384 self._resumable_url = None 

385 self._invalid = False 

386 

387 @property 

388 def invalid(self): 

389 """bool: Indicates if the upload is in an invalid state. 

390 

391 This will occur if a call to :meth:`transmit_next_chunk` fails. 

392 To recover from such a failure, call :meth:`recover`. 

393 """ 

394 return self._invalid 

395 

396 @property 

397 def chunk_size(self): 

398 """int: The size of each chunk used to upload the resource.""" 

399 return self._chunk_size 

400 

401 @property 

402 def resumable_url(self): 

403 """Optional[str]: The URL of the in-progress resumable upload.""" 

404 return self._resumable_url 

405 

406 @property 

407 def bytes_uploaded(self): 

408 """int: Number of bytes that have been uploaded.""" 

409 return self._bytes_uploaded 

410 

411 @property 

412 def total_bytes(self): 

413 """Optional[int]: The total number of bytes to be uploaded. 

414 

415 If this upload is initiated (via :meth:`initiate`) with 

416 ``stream_final=True``, this value will be populated based on the size 

417 of the ``stream`` being uploaded. (By default ``stream_final=True``.) 

418 

419 If this upload is initiated with ``stream_final=False``, 

420 :attr:`total_bytes` will be :data:`None` since it cannot be 

421 determined from the stream. 

422 """ 

423 return self._total_bytes 

424 

425 def _prepare_initiate_request( 

426 self, stream, metadata, content_type, total_bytes=None, stream_final=True 

427 ): 

428 """Prepare the contents of HTTP request to initiate upload. 

429 

430 This is everything that must be done before a request that doesn't 

431 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

432 philosophy. 

433 

434 Args: 

435 stream (IO[bytes]): The stream (i.e. file-like object) that will 

436 be uploaded. The stream **must** be at the beginning (i.e. 

437 ``stream.tell() == 0``). 

438 metadata (Mapping[str, str]): The resource metadata, such as an 

439 ACL list. 

440 content_type (str): The content type of the resource, e.g. a JPEG 

441 image has content type ``image/jpeg``. 

442 total_bytes (Optional[int]): The total number of bytes to be 

443 uploaded. If specified, the upload size **will not** be 

444 determined from the stream (even if ``stream_final=True``). 

445 stream_final (Optional[bool]): Indicates if the ``stream`` is 

446 "final" (i.e. no more bytes will be added to it). In this case 

447 we determine the upload size from the size of the stream. If 

448 ``total_bytes`` is passed, this argument will be ignored. 

449 

450 Returns: 

451 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

452 

453 * HTTP verb for the request (always POST) 

454 * the URL for the request 

455 * the body of the request 

456 * headers for the request 

457 

458 Raises: 

459 ValueError: If the current upload has already been initiated. 

460 ValueError: If ``stream`` is not at the beginning. 

461 

462 .. _sans-I/O: https://sans-io.readthedocs.io/ 

463 """ 

464 if self.resumable_url is not None: 

465 raise ValueError("This upload has already been initiated.") 

466 if stream.tell() != 0: 

467 raise ValueError("Stream must be at beginning.") 

468 

469 self._stream = stream 

470 self._content_type = content_type 

471 

472 # Signed URL requires content type set directly - not through x-upload-content-type 

473 parse_result = urllib.parse.urlparse(self.upload_url) 

474 parsed_query = urllib.parse.parse_qs(parse_result.query) 

475 if "x-goog-signature" in parsed_query or "X-Goog-Signature" in parsed_query: 

476 # Deconstruct **self._headers first so that content type defined here takes priority 

477 headers = {**self._headers, _CONTENT_TYPE_HEADER: content_type} 

478 else: 

479 # Deconstruct **self._headers first so that content type defined here takes priority 

480 headers = { 

481 **self._headers, 

482 _CONTENT_TYPE_HEADER: "application/json; charset=UTF-8", 

483 "x-upload-content-type": content_type, 

484 } 

485 # Set the total bytes if possible. 

486 if total_bytes is not None: 

487 self._total_bytes = total_bytes 

488 elif stream_final: 

489 self._total_bytes = get_total_bytes(stream) 

490 # Add the total bytes to the headers if set. 

491 if self._total_bytes is not None: 

492 content_length = "{:d}".format(self._total_bytes) 

493 headers["x-upload-content-length"] = content_length 

494 

495 payload = json.dumps(metadata).encode("utf-8") 

496 return _POST, self.upload_url, payload, headers 

497 

498 def _process_initiate_response(self, response): 

499 """Process the response from an HTTP request that initiated upload. 

500 

501 This is everything that must be done after a request that doesn't 

502 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

503 philosophy. 

504 

505 This method takes the URL from the ``Location`` header and stores it 

506 for future use. Within that URL, we assume the ``upload_id`` query 

507 parameter has been included, but we do not check. 

508 

509 Args: 

510 response (object): The HTTP response object (need headers). 

511 

512 .. _sans-I/O: https://sans-io.readthedocs.io/ 

513 """ 

514 _helpers.require_status_code( 

515 response, 

516 (http.client.OK, http.client.CREATED), 

517 self._get_status_code, 

518 callback=self._make_invalid, 

519 ) 

520 self._resumable_url = _helpers.header_required( 

521 response, "location", self._get_headers 

522 ) 

523 

524 def initiate( 

525 self, 

526 transport, 

527 stream, 

528 metadata, 

529 content_type, 

530 total_bytes=None, 

531 stream_final=True, 

532 timeout=None, 

533 ): 

534 """Initiate a resumable upload. 

535 

536 By default, this method assumes your ``stream`` is in a "final" 

537 state ready to transmit. However, ``stream_final=False`` can be used 

538 to indicate that the size of the resource is not known. This can happen 

539 if bytes are being dynamically fed into ``stream``, e.g. if the stream 

540 is attached to application logs. 

541 

542 If ``stream_final=False`` is used, :attr:`chunk_size` bytes will be 

543 read from the stream every time :meth:`transmit_next_chunk` is called. 

544 If one of those reads produces strictly fewer bites than the chunk 

545 size, the upload will be concluded. 

546 

547 Args: 

548 transport (object): An object which can make authenticated 

549 requests. 

550 stream (IO[bytes]): The stream (i.e. file-like object) that will 

551 be uploaded. The stream **must** be at the beginning (i.e. 

552 ``stream.tell() == 0``). 

553 metadata (Mapping[str, str]): The resource metadata, such as an 

554 ACL list. 

555 content_type (str): The content type of the resource, e.g. a JPEG 

556 image has content type ``image/jpeg``. 

557 total_bytes (Optional[int]): The total number of bytes to be 

558 uploaded. If specified, the upload size **will not** be 

559 determined from the stream (even if ``stream_final=True``). 

560 stream_final (Optional[bool]): Indicates if the ``stream`` is 

561 "final" (i.e. no more bytes will be added to it). In this case 

562 we determine the upload size from the size of the stream. If 

563 ``total_bytes`` is passed, this argument will be ignored. 

564 timeout (Optional[Union[float, Tuple[float, float]]]): 

565 The number of seconds to wait for the server response. 

566 Depending on the retry strategy, a request may be repeated 

567 several times using the same timeout each time. 

568 

569 Can also be passed as a tuple (connect_timeout, read_timeout). 

570 See :meth:`requests.Session.request` documentation for details. 

571 

572 Raises: 

573 NotImplementedError: Always, since virtual. 

574 """ 

575 raise NotImplementedError("This implementation is virtual.") 

576 

577 def _prepare_request(self): 

578 """Prepare the contents of HTTP request to upload a chunk. 

579 

580 This is everything that must be done before a request that doesn't 

581 require network I/O. This is based on the `sans-I/O`_ philosophy. 

582 

583 For the time being, this **does require** some form of I/O to read 

584 a chunk from ``stream`` (via :func:`get_next_chunk`). However, this 

585 will (almost) certainly not be network I/O. 

586 

587 Returns: 

588 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

589 

590 * HTTP verb for the request (always PUT) 

591 * the URL for the request 

592 * the body of the request 

593 * headers for the request 

594 

595 The headers incorporate the ``_headers`` on the current instance. 

596 

597 Raises: 

598 ValueError: If the current upload has finished. 

599 ValueError: If the current upload is in an invalid state. 

600 ValueError: If the current upload has not been initiated. 

601 ValueError: If the location in the stream (i.e. ``stream.tell()``) 

602 does not agree with ``bytes_uploaded``. 

603 

604 .. _sans-I/O: https://sans-io.readthedocs.io/ 

605 """ 

606 if self.finished: 

607 raise ValueError("Upload has finished.") 

608 if self.invalid: 

609 raise ValueError( 

610 "Upload is in an invalid state. To recover call `recover()`." 

611 ) 

612 if self.resumable_url is None: 

613 raise ValueError( 

614 "This upload has not been initiated. Please call " 

615 "initiate() before beginning to transmit chunks." 

616 ) 

617 

618 start_byte, payload, content_range = get_next_chunk( 

619 self._stream, self._chunk_size, self._total_bytes 

620 ) 

621 if start_byte != self.bytes_uploaded: 

622 msg = _STREAM_ERROR_TEMPLATE.format(start_byte, self.bytes_uploaded) 

623 raise ValueError(msg) 

624 

625 self._update_checksum(start_byte, payload) 

626 

627 headers = { 

628 **self._headers, 

629 _CONTENT_TYPE_HEADER: self._content_type, 

630 _helpers.CONTENT_RANGE_HEADER: content_range, 

631 } 

632 return _PUT, self.resumable_url, payload, headers 

633 

634 def _update_checksum(self, start_byte, payload): 

635 """Update the checksum with the payload if not already updated. 

636 

637 Because error recovery can result in bytes being transmitted more than 

638 once, the checksum tracks the number of bytes checked in 

639 self._bytes_checksummed and skips bytes that have already been summed. 

640 """ 

641 if not self._checksum_type: 

642 return 

643 

644 if not self._checksum_object: 

645 self._checksum_object = _helpers._get_checksum_object(self._checksum_type) 

646 

647 if start_byte < self._bytes_checksummed: 

648 offset = self._bytes_checksummed - start_byte 

649 data = payload[offset:] 

650 else: 

651 data = payload 

652 

653 self._checksum_object.update(data) 

654 self._bytes_checksummed += len(data) 

655 

656 def _make_invalid(self): 

657 """Simple setter for ``invalid``. 

658 

659 This is intended to be passed along as a callback to helpers that 

660 raise an exception so they can mark this instance as invalid before 

661 raising. 

662 """ 

663 self._invalid = True 

664 

665 def _process_resumable_response(self, response, bytes_sent): 

666 """Process the response from an HTTP request. 

667 

668 This is everything that must be done after a request that doesn't 

669 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

670 philosophy. 

671 

672 Args: 

673 response (object): The HTTP response object. 

674 bytes_sent (int): The number of bytes sent in the request that 

675 ``response`` was returned for. 

676 

677 Raises: 

678 ~google.resumable_media.common.InvalidResponse: If the status 

679 code is 308 and the ``range`` header is not of the form 

680 ``bytes 0-{end}``. 

681 ~google.resumable_media.common.InvalidResponse: If the status 

682 code is not 200 or 308. 

683 

684 .. _sans-I/O: https://sans-io.readthedocs.io/ 

685 """ 

686 status_code = _helpers.require_status_code( 

687 response, 

688 (http.client.OK, http.client.PERMANENT_REDIRECT), 

689 self._get_status_code, 

690 callback=self._make_invalid, 

691 ) 

692 if status_code == http.client.OK: 

693 # NOTE: We use the "local" information of ``bytes_sent`` to update 

694 # ``bytes_uploaded``, but do not verify this against other 

695 # state. However, there may be some other information: 

696 # 

697 # * a ``size`` key in JSON response body 

698 # * the ``total_bytes`` attribute (if set) 

699 # * ``stream.tell()`` (relying on fact that ``initiate()`` 

700 # requires stream to be at the beginning) 

701 self._bytes_uploaded = self._bytes_uploaded + bytes_sent 

702 # Tombstone the current upload so it cannot be used again. 

703 self._finished = True 

704 # Validate the checksum. This can raise an exception on failure. 

705 self._validate_checksum(response) 

706 else: 

707 bytes_range = _helpers.header_required( 

708 response, 

709 _helpers.RANGE_HEADER, 

710 self._get_headers, 

711 callback=self._make_invalid, 

712 ) 

713 match = _BYTES_RANGE_RE.match(bytes_range) 

714 if match is None: 

715 self._make_invalid() 

716 raise common.InvalidResponse( 

717 response, 

718 'Unexpected "range" header', 

719 bytes_range, 

720 'Expected to be of the form "bytes=0-{end}"', 

721 ) 

722 self._bytes_uploaded = int(match.group("end_byte")) + 1 

723 

724 def _validate_checksum(self, response): 

725 """Check the computed checksum, if any, against the recieved metadata. 

726 

727 Args: 

728 response (object): The HTTP response object. 

729 

730 Raises: 

731 ~google.resumable_media.common.DataCorruption: If the checksum 

732 computed locally and the checksum reported by the remote host do 

733 not match. 

734 """ 

735 if self._checksum_type is None: 

736 return 

737 metadata_key = _helpers._get_metadata_key(self._checksum_type) 

738 metadata = response.json() 

739 remote_checksum = metadata.get(metadata_key) 

740 if remote_checksum is None: 

741 raise common.InvalidResponse( 

742 response, 

743 _UPLOAD_METADATA_NO_APPROPRIATE_CHECKSUM_MESSAGE.format(metadata_key), 

744 self._get_headers(response), 

745 ) 

746 local_checksum = _helpers.prepare_checksum_digest( 

747 self._checksum_object.digest() 

748 ) 

749 if local_checksum != remote_checksum: 

750 raise common.DataCorruption( 

751 response, 

752 _UPLOAD_CHECKSUM_MISMATCH_MESSAGE.format( 

753 self._checksum_type.upper(), local_checksum, remote_checksum 

754 ), 

755 ) 

756 

757 def transmit_next_chunk(self, transport, timeout=None): 

758 """Transmit the next chunk of the resource to be uploaded. 

759 

760 If the current upload was initiated with ``stream_final=False``, 

761 this method will dynamically determine if the upload has completed. 

762 The upload will be considered complete if the stream produces 

763 fewer than :attr:`chunk_size` bytes when a chunk is read from it. 

764 

765 Args: 

766 transport (object): An object which can make authenticated 

767 requests. 

768 timeout (Optional[Union[float, Tuple[float, float]]]): 

769 The number of seconds to wait for the server response. 

770 Depending on the retry strategy, a request may be repeated 

771 several times using the same timeout each time. 

772 

773 Can also be passed as a tuple (connect_timeout, read_timeout). 

774 See :meth:`requests.Session.request` documentation for details. 

775 

776 Raises: 

777 NotImplementedError: Always, since virtual. 

778 """ 

779 raise NotImplementedError("This implementation is virtual.") 

780 

781 def _prepare_recover_request(self): 

782 """Prepare the contents of HTTP request to recover from failure. 

783 

784 This is everything that must be done before a request that doesn't 

785 require network I/O. This is based on the `sans-I/O`_ philosophy. 

786 

787 We assume that the :attr:`resumable_url` is set (i.e. the only way 

788 the upload can end up :attr:`invalid` is if it has been initiated. 

789 

790 Returns: 

791 Tuple[str, str, NoneType, Mapping[str, str]]: The quadruple 

792 

793 * HTTP verb for the request (always PUT) 

794 * the URL for the request 

795 * the body of the request (always :data:`None`) 

796 * headers for the request 

797 

798 The headers **do not** incorporate the ``_headers`` on the 

799 current instance. 

800 

801 .. _sans-I/O: https://sans-io.readthedocs.io/ 

802 """ 

803 headers = {_helpers.CONTENT_RANGE_HEADER: "bytes */*"} 

804 return _PUT, self.resumable_url, None, headers 

805 

806 def _process_recover_response(self, response): 

807 """Process the response from an HTTP request to recover from failure. 

808 

809 This is everything that must be done after a request that doesn't 

810 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

811 philosophy. 

812 

813 Args: 

814 response (object): The HTTP response object. 

815 

816 Raises: 

817 ~google.resumable_media.common.InvalidResponse: If the status 

818 code is not 308. 

819 ~google.resumable_media.common.InvalidResponse: If the status 

820 code is 308 and the ``range`` header is not of the form 

821 ``bytes 0-{end}``. 

822 

823 .. _sans-I/O: https://sans-io.readthedocs.io/ 

824 """ 

825 _helpers.require_status_code( 

826 response, (http.client.PERMANENT_REDIRECT,), self._get_status_code 

827 ) 

828 headers = self._get_headers(response) 

829 if _helpers.RANGE_HEADER in headers: 

830 bytes_range = headers[_helpers.RANGE_HEADER] 

831 match = _BYTES_RANGE_RE.match(bytes_range) 

832 if match is None: 

833 raise common.InvalidResponse( 

834 response, 

835 'Unexpected "range" header', 

836 bytes_range, 

837 'Expected to be of the form "bytes=0-{end}"', 

838 ) 

839 self._bytes_uploaded = int(match.group("end_byte")) + 1 

840 else: 

841 # In this case, the upload has not "begun". 

842 self._bytes_uploaded = 0 

843 

844 self._stream.seek(self._bytes_uploaded) 

845 self._invalid = False 

846 

847 def recover(self, transport): 

848 """Recover from a failure. 

849 

850 This method should be used when a :class:`ResumableUpload` is in an 

851 :attr:`~ResumableUpload.invalid` state due to a request failure. 

852 

853 This will verify the progress with the server and make sure the 

854 current upload is in a valid state before :meth:`transmit_next_chunk` 

855 can be used again. 

856 

857 Args: 

858 transport (object): An object which can make authenticated 

859 requests. 

860 

861 Raises: 

862 NotImplementedError: Always, since virtual. 

863 """ 

864 raise NotImplementedError("This implementation is virtual.") 

865 

866 

867class XMLMPUContainer(UploadBase): 

868 """Initiate and close an upload using the XML MPU API. 

869 

870 An XML MPU sends an initial request and then receives an upload ID. 

871 Using the upload ID, the upload is then done in numbered parts and the 

872 parts can be uploaded concurrently. 

873 

874 In order to avoid concurrency issues with this container object, the 

875 uploading of individual parts is handled separately, by XMLMPUPart objects 

876 spawned from this container class. The XMLMPUPart objects are not 

877 necessarily in the same process as the container, so they do not update the 

878 container automatically. 

879 

880 MPUs are sometimes referred to as "Multipart Uploads", which is ambiguous 

881 given the JSON multipart upload, so the abbreviation "MPU" will be used 

882 throughout. 

883 

884 See: https://cloud.google.com/storage/docs/multipart-uploads 

885 

886 Args: 

887 upload_url (str): The URL of the object (without query parameters). The 

888 initiate, PUT, and finalization requests will all use this URL, with 

889 varying query parameters. 

890 filename (str): The name (path) of the file to upload. 

891 headers (Optional[Mapping[str, str]]): Extra headers that should 

892 be sent with every request. 

893 

894 Attributes: 

895 upload_url (str): The URL where the content will be uploaded. 

896 upload_id (Optional(str)): The ID of the upload from the initialization 

897 response. 

898 """ 

899 

900 def __init__(self, upload_url, filename, headers=None, upload_id=None): 

901 super().__init__(upload_url, headers=headers) 

902 self._filename = filename 

903 self._upload_id = upload_id 

904 self._parts = {} 

905 

906 @property 

907 def upload_id(self): 

908 return self._upload_id 

909 

910 def register_part(self, part_number, etag): 

911 """Register an uploaded part by part number and corresponding etag. 

912 

913 XMLMPUPart objects represent individual parts, and their part number 

914 and etag can be registered to the container object with this method 

915 and therefore incorporated in the finalize() call to finish the upload. 

916 

917 This method accepts part_number and etag, but not XMLMPUPart objects 

918 themselves, to reduce the complexity involved in running XMLMPUPart 

919 uploads in separate processes. 

920 

921 Args: 

922 part_number (int): The part number. Parts are assembled into the 

923 final uploaded object with finalize() in order of their part 

924 numbers. 

925 etag (str): The etag included in the server response after upload. 

926 """ 

927 self._parts[part_number] = etag 

928 

929 def _prepare_initiate_request(self, content_type): 

930 """Prepare the contents of HTTP request to initiate upload. 

931 

932 This is everything that must be done before a request that doesn't 

933 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

934 philosophy. 

935 

936 Args: 

937 content_type (str): The content type of the resource, e.g. a JPEG 

938 image has content type ``image/jpeg``. 

939 

940 Returns: 

941 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

942 

943 * HTTP verb for the request (always POST) 

944 * the URL for the request 

945 * the body of the request 

946 * headers for the request 

947 

948 Raises: 

949 ValueError: If the current upload has already been initiated. 

950 

951 .. _sans-I/O: https://sans-io.readthedocs.io/ 

952 """ 

953 if self.upload_id is not None: 

954 raise ValueError("This upload has already been initiated.") 

955 

956 initiate_url = self.upload_url + _MPU_INITIATE_QUERY 

957 

958 headers = { 

959 **self._headers, 

960 _CONTENT_TYPE_HEADER: content_type, 

961 } 

962 return _POST, initiate_url, None, headers 

963 

964 def _process_initiate_response(self, response): 

965 """Process the response from an HTTP request that initiated the upload. 

966 

967 This is everything that must be done after a request that doesn't 

968 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

969 philosophy. 

970 

971 This method takes the URL from the ``Location`` header and stores it 

972 for future use. Within that URL, we assume the ``upload_id`` query 

973 parameter has been included, but we do not check. 

974 

975 Args: 

976 response (object): The HTTP response object. 

977 

978 Raises: 

979 ~google.resumable_media.common.InvalidResponse: If the status 

980 code is not 200. 

981 

982 .. _sans-I/O: https://sans-io.readthedocs.io/ 

983 """ 

984 _helpers.require_status_code(response, (http.client.OK,), self._get_status_code) 

985 root = ElementTree.fromstring(response.text) 

986 self._upload_id = root.find(_S3_COMPAT_XML_NAMESPACE + _UPLOAD_ID_NODE).text 

987 

988 def initiate( 

989 self, 

990 transport, 

991 content_type, 

992 timeout=None, 

993 ): 

994 """Initiate an MPU and record the upload ID. 

995 

996 Args: 

997 transport (object): An object which can make authenticated 

998 requests. 

999 content_type (str): The content type of the resource, e.g. a JPEG 

1000 image has content type ``image/jpeg``. 

1001 timeout (Optional[Union[float, Tuple[float, float]]]): 

1002 The number of seconds to wait for the server response. 

1003 Depending on the retry strategy, a request may be repeated 

1004 several times using the same timeout each time. 

1005 

1006 Can also be passed as a tuple (connect_timeout, read_timeout). 

1007 See :meth:`requests.Session.request` documentation for details. 

1008 

1009 Raises: 

1010 NotImplementedError: Always, since virtual. 

1011 """ 

1012 raise NotImplementedError("This implementation is virtual.") 

1013 

1014 def _prepare_finalize_request(self): 

1015 """Prepare the contents of an HTTP request to finalize the upload. 

1016 

1017 All of the parts must be registered before calling this method. 

1018 

1019 Returns: 

1020 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

1021 

1022 * HTTP verb for the request (always POST) 

1023 * the URL for the request 

1024 * the body of the request 

1025 * headers for the request 

1026 

1027 Raises: 

1028 ValueError: If the upload has not been initiated. 

1029 """ 

1030 if self.upload_id is None: 

1031 raise ValueError("This upload has not yet been initiated.") 

1032 

1033 final_query = _MPU_FINAL_QUERY_TEMPLATE.format(upload_id=self._upload_id) 

1034 finalize_url = self.upload_url + final_query 

1035 final_xml_root = ElementTree.Element("CompleteMultipartUpload") 

1036 for part_number, etag in self._parts.items(): 

1037 part = ElementTree.SubElement(final_xml_root, "Part") # put in a loop 

1038 ElementTree.SubElement(part, "PartNumber").text = str(part_number) 

1039 ElementTree.SubElement(part, "ETag").text = etag 

1040 payload = ElementTree.tostring(final_xml_root) 

1041 return _POST, finalize_url, payload, self._headers 

1042 

1043 def _process_finalize_response(self, response): 

1044 """Process the response from an HTTP request that finalized the upload. 

1045 

1046 This is everything that must be done after a request that doesn't 

1047 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

1048 philosophy. 

1049 

1050 Args: 

1051 response (object): The HTTP response object. 

1052 

1053 Raises: 

1054 ~google.resumable_media.common.InvalidResponse: If the status 

1055 code is not 200. 

1056 

1057 .. _sans-I/O: https://sans-io.readthedocs.io/ 

1058 """ 

1059 

1060 _helpers.require_status_code(response, (http.client.OK,), self._get_status_code) 

1061 self._finished = True 

1062 

1063 def finalize( 

1064 self, 

1065 transport, 

1066 timeout=None, 

1067 ): 

1068 """Finalize an MPU request with all the parts. 

1069 

1070 Args: 

1071 transport (object): An object which can make authenticated 

1072 requests. 

1073 timeout (Optional[Union[float, Tuple[float, float]]]): 

1074 The number of seconds to wait for the server response. 

1075 Depending on the retry strategy, a request may be repeated 

1076 several times using the same timeout each time. 

1077 

1078 Can also be passed as a tuple (connect_timeout, read_timeout). 

1079 See :meth:`requests.Session.request` documentation for details. 

1080 

1081 Raises: 

1082 NotImplementedError: Always, since virtual. 

1083 """ 

1084 raise NotImplementedError("This implementation is virtual.") 

1085 

1086 def _prepare_cancel_request(self): 

1087 """Prepare the contents of an HTTP request to cancel the upload. 

1088 

1089 Returns: 

1090 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

1091 

1092 * HTTP verb for the request (always DELETE) 

1093 * the URL for the request 

1094 * the body of the request 

1095 * headers for the request 

1096 

1097 Raises: 

1098 ValueError: If the upload has not been initiated. 

1099 """ 

1100 if self.upload_id is None: 

1101 raise ValueError("This upload has not yet been initiated.") 

1102 

1103 cancel_query = _MPU_FINAL_QUERY_TEMPLATE.format(upload_id=self._upload_id) 

1104 cancel_url = self.upload_url + cancel_query 

1105 return _DELETE, cancel_url, None, self._headers 

1106 

1107 def _process_cancel_response(self, response): 

1108 """Process the response from an HTTP request that canceled the upload. 

1109 

1110 This is everything that must be done after a request that doesn't 

1111 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

1112 philosophy. 

1113 

1114 Args: 

1115 response (object): The HTTP response object. 

1116 

1117 Raises: 

1118 ~google.resumable_media.common.InvalidResponse: If the status 

1119 code is not 204. 

1120 

1121 .. _sans-I/O: https://sans-io.readthedocs.io/ 

1122 """ 

1123 

1124 _helpers.require_status_code( 

1125 response, (http.client.NO_CONTENT,), self._get_status_code 

1126 ) 

1127 

1128 def cancel( 

1129 self, 

1130 transport, 

1131 timeout=None, 

1132 ): 

1133 """Cancel an MPU request and permanently delete any uploaded parts. 

1134 

1135 This cannot be undone. 

1136 

1137 Args: 

1138 transport (object): An object which can make authenticated 

1139 requests. 

1140 timeout (Optional[Union[float, Tuple[float, float]]]): 

1141 The number of seconds to wait for the server response. 

1142 Depending on the retry strategy, a request may be repeated 

1143 several times using the same timeout each time. 

1144 

1145 Can also be passed as a tuple (connect_timeout, read_timeout). 

1146 See :meth:`requests.Session.request` documentation for details. 

1147 

1148 Raises: 

1149 NotImplementedError: Always, since virtual. 

1150 """ 

1151 raise NotImplementedError("This implementation is virtual.") 

1152 

1153 

1154class XMLMPUPart(UploadBase): 

1155 """Upload a single part of an existing XML MPU container. 

1156 

1157 An XML MPU sends an initial request and then receives an upload ID. 

1158 Using the upload ID, the upload is then done in numbered parts and the 

1159 parts can be uploaded concurrently. 

1160 

1161 In order to avoid concurrency issues with the container object, the 

1162 uploading of individual parts is handled separately by multiple objects 

1163 of this class. Once a part is uploaded, it can be registered with the 

1164 container with `container.register_part(part.part_number, part.etag)`. 

1165 

1166 MPUs are sometimes referred to as "Multipart Uploads", which is ambiguous 

1167 given the JSON multipart upload, so the abbreviation "MPU" will be used 

1168 throughout. 

1169 

1170 See: https://cloud.google.com/storage/docs/multipart-uploads 

1171 

1172 Args: 

1173 upload_url (str): The URL of the object (without query parameters). 

1174 upload_id (str): The ID of the upload from the initialization response. 

1175 filename (str): The name (path) of the file to upload. 

1176 start (int): The byte index of the beginning of the part. 

1177 end (int): The byte index of the end of the part. 

1178 part_number (int): The part number. Part numbers will be assembled in 

1179 sequential order when the container is finalized. 

1180 headers (Optional[Mapping[str, str]]): Extra headers that should 

1181 be sent with every request. 

1182 checksum (Optional([str])): The type of checksum to compute to verify 

1183 the integrity of the object. The request headers will be amended 

1184 to include the computed value. Supported values are "md5", "crc32c" 

1185 and None. The default is None. 

1186 

1187 Attributes: 

1188 upload_url (str): The URL of the object (without query parameters). 

1189 upload_id (str): The ID of the upload from the initialization response. 

1190 filename (str): The name (path) of the file to upload. 

1191 start (int): The byte index of the beginning of the part. 

1192 end (int): The byte index of the end of the part. 

1193 part_number (int): The part number. Part numbers will be assembled in 

1194 sequential order when the container is finalized. 

1195 etag (Optional(str)): The etag returned by the service after upload. 

1196 """ 

1197 

1198 def __init__( 

1199 self, 

1200 upload_url, 

1201 upload_id, 

1202 filename, 

1203 start, 

1204 end, 

1205 part_number, 

1206 headers=None, 

1207 checksum=None, 

1208 ): 

1209 super().__init__(upload_url, headers=headers) 

1210 self._filename = filename 

1211 self._start = start 

1212 self._end = end 

1213 self._upload_id = upload_id 

1214 self._part_number = part_number 

1215 self._etag = None 

1216 self._checksum_type = checksum 

1217 self._checksum_object = None 

1218 

1219 @property 

1220 def part_number(self): 

1221 return self._part_number 

1222 

1223 @property 

1224 def upload_id(self): 

1225 return self._upload_id 

1226 

1227 @property 

1228 def filename(self): 

1229 return self._filename 

1230 

1231 @property 

1232 def etag(self): 

1233 return self._etag 

1234 

1235 @property 

1236 def start(self): 

1237 return self._start 

1238 

1239 @property 

1240 def end(self): 

1241 return self._end 

1242 

1243 def _prepare_upload_request(self): 

1244 """Prepare the contents of HTTP request to upload a part. 

1245 

1246 This is everything that must be done before a request that doesn't 

1247 require network I/O. This is based on the `sans-I/O`_ philosophy. 

1248 

1249 For the time being, this **does require** some form of I/O to read 

1250 a part from ``stream`` (via :func:`get_part_payload`). However, this 

1251 will (almost) certainly not be network I/O. 

1252 

1253 Returns: 

1254 Tuple[str, str, bytes, Mapping[str, str]]: The quadruple 

1255 

1256 * HTTP verb for the request (always PUT) 

1257 * the URL for the request 

1258 * the body of the request 

1259 * headers for the request 

1260 

1261 The headers incorporate the ``_headers`` on the current instance. 

1262 

1263 Raises: 

1264 ValueError: If the current upload has finished. 

1265 

1266 .. _sans-I/O: https://sans-io.readthedocs.io/ 

1267 """ 

1268 if self.finished: 

1269 raise ValueError("This part has already been uploaded.") 

1270 

1271 with open(self._filename, "br") as f: 

1272 f.seek(self._start) 

1273 payload = f.read(self._end - self._start) 

1274 

1275 self._checksum_object = _helpers._get_checksum_object(self._checksum_type) 

1276 if self._checksum_object is not None: 

1277 self._checksum_object.update(payload) 

1278 

1279 part_query = _MPU_PART_QUERY_TEMPLATE.format( 

1280 part=self._part_number, upload_id=self._upload_id 

1281 ) 

1282 upload_url = self.upload_url + part_query 

1283 return _PUT, upload_url, payload, self._headers 

1284 

1285 def _process_upload_response(self, response): 

1286 """Process the response from an HTTP request. 

1287 

1288 This is everything that must be done after a request that doesn't 

1289 require network I/O (or other I/O). This is based on the `sans-I/O`_ 

1290 philosophy. 

1291 

1292 Args: 

1293 response (object): The HTTP response object. 

1294 

1295 Raises: 

1296 ~google.resumable_media.common.InvalidResponse: If the status 

1297 code is not 200 or the response is missing data. 

1298 

1299 .. _sans-I/O: https://sans-io.readthedocs.io/ 

1300 """ 

1301 _helpers.require_status_code( 

1302 response, 

1303 (http.client.OK,), 

1304 self._get_status_code, 

1305 ) 

1306 

1307 self._validate_checksum(response) 

1308 

1309 etag = _helpers.header_required(response, "etag", self._get_headers) 

1310 self._etag = etag 

1311 self._finished = True 

1312 

1313 def upload( 

1314 self, 

1315 transport, 

1316 timeout=None, 

1317 ): 

1318 """Upload the part. 

1319 

1320 Args: 

1321 transport (object): An object which can make authenticated 

1322 requests. 

1323 timeout (Optional[Union[float, Tuple[float, float]]]): 

1324 The number of seconds to wait for the server response. 

1325 Depending on the retry strategy, a request may be repeated 

1326 several times using the same timeout each time. 

1327 

1328 Can also be passed as a tuple (connect_timeout, read_timeout). 

1329 See :meth:`requests.Session.request` documentation for details. 

1330 

1331 Raises: 

1332 NotImplementedError: Always, since virtual. 

1333 """ 

1334 raise NotImplementedError("This implementation is virtual.") 

1335 

1336 def _validate_checksum(self, response): 

1337 """Check the computed checksum, if any, against the response headers. 

1338 

1339 Args: 

1340 response (object): The HTTP response object. 

1341 

1342 Raises: 

1343 ~google.resumable_media.common.DataCorruption: If the checksum 

1344 computed locally and the checksum reported by the remote host do 

1345 not match. 

1346 """ 

1347 if self._checksum_type is None: 

1348 return 

1349 

1350 remote_checksum = _helpers._get_uploaded_checksum_from_headers( 

1351 response, self._get_headers, self._checksum_type 

1352 ) 

1353 

1354 if remote_checksum is None: 

1355 metadata_key = _helpers._get_metadata_key(self._checksum_type) 

1356 raise common.InvalidResponse( 

1357 response, 

1358 _UPLOAD_METADATA_NO_APPROPRIATE_CHECKSUM_MESSAGE.format(metadata_key), 

1359 self._get_headers(response), 

1360 ) 

1361 local_checksum = _helpers.prepare_checksum_digest( 

1362 self._checksum_object.digest() 

1363 ) 

1364 if local_checksum != remote_checksum: 

1365 raise common.DataCorruption( 

1366 response, 

1367 _UPLOAD_CHECKSUM_MISMATCH_MESSAGE.format( 

1368 self._checksum_type.upper(), local_checksum, remote_checksum 

1369 ), 

1370 ) 

1371 

1372 

1373def get_boundary(): 

1374 """Get a random boundary for a multipart request. 

1375 

1376 Returns: 

1377 bytes: The boundary used to separate parts of a multipart request. 

1378 """ 

1379 random_int = random.randrange(sys.maxsize) 

1380 boundary = _BOUNDARY_FORMAT.format(random_int) 

1381 # NOTE: Neither % formatting nor .format() are available for byte strings 

1382 # in Python 3.4, so we must use unicode strings as templates. 

1383 return boundary.encode("utf-8") 

1384 

1385 

1386def construct_multipart_request(data, metadata, content_type): 

1387 """Construct a multipart request body. 

1388 

1389 Args: 

1390 data (bytes): The resource content (UTF-8 encoded as bytes) 

1391 to be uploaded. 

1392 metadata (Mapping[str, str]): The resource metadata, such as an 

1393 ACL list. 

1394 content_type (str): The content type of the resource, e.g. a JPEG 

1395 image has content type ``image/jpeg``. 

1396 

1397 Returns: 

1398 Tuple[bytes, bytes]: The multipart request body and the boundary used 

1399 between each part. 

1400 """ 

1401 multipart_boundary = get_boundary() 

1402 json_bytes = json.dumps(metadata).encode("utf-8") 

1403 content_type = content_type.encode("utf-8") 

1404 # Combine the two parts into a multipart payload. 

1405 # NOTE: We'd prefer a bytes template but are restricted by Python 3.4. 

1406 boundary_sep = _MULTIPART_SEP + multipart_boundary 

1407 content = ( 

1408 boundary_sep 

1409 + _MULTIPART_BEGIN 

1410 + json_bytes 

1411 + _CRLF 

1412 + boundary_sep 

1413 + _CRLF 

1414 + b"content-type: " 

1415 + content_type 

1416 + _CRLF 

1417 + _CRLF 

1418 + data # Empty line between headers and body. 

1419 + _CRLF 

1420 + boundary_sep 

1421 + _MULTIPART_SEP 

1422 ) 

1423 

1424 return content, multipart_boundary 

1425 

1426 

1427def get_total_bytes(stream): 

1428 """Determine the total number of bytes in a stream. 

1429 

1430 Args: 

1431 stream (IO[bytes]): The stream (i.e. file-like object). 

1432 

1433 Returns: 

1434 int: The number of bytes. 

1435 """ 

1436 current_position = stream.tell() 

1437 # NOTE: ``.seek()`` **should** return the same value that ``.tell()`` 

1438 # returns, but in Python 2, ``file`` objects do not. 

1439 stream.seek(0, os.SEEK_END) 

1440 end_position = stream.tell() 

1441 # Go back to the initial position. 

1442 stream.seek(current_position) 

1443 

1444 return end_position 

1445 

1446 

1447def get_next_chunk(stream, chunk_size, total_bytes): 

1448 """Get a chunk from an I/O stream. 

1449 

1450 The ``stream`` may have fewer bytes remaining than ``chunk_size`` 

1451 so it may not always be the case that 

1452 ``end_byte == start_byte + chunk_size - 1``. 

1453 

1454 Args: 

1455 stream (IO[bytes]): The stream (i.e. file-like object). 

1456 chunk_size (int): The size of the chunk to be read from the ``stream``. 

1457 total_bytes (Optional[int]): The (expected) total number of bytes 

1458 in the ``stream``. 

1459 

1460 Returns: 

1461 Tuple[int, bytes, str]: Triple of: 

1462 

1463 * the start byte index 

1464 * the content in between the start and end bytes (inclusive) 

1465 * content range header for the chunk (slice) that has been read 

1466 

1467 Raises: 

1468 ValueError: If ``total_bytes == 0`` but ``stream.read()`` yields 

1469 non-empty content. 

1470 ValueError: If there is no data left to consume. This corresponds 

1471 exactly to the case ``end_byte < start_byte``, which can only 

1472 occur if ``end_byte == start_byte - 1``. 

1473 """ 

1474 start_byte = stream.tell() 

1475 if total_bytes is not None and start_byte + chunk_size >= total_bytes > 0: 

1476 payload = stream.read(total_bytes - start_byte) 

1477 else: 

1478 payload = stream.read(chunk_size) 

1479 end_byte = stream.tell() - 1 

1480 

1481 num_bytes_read = len(payload) 

1482 if total_bytes is None: 

1483 if num_bytes_read < chunk_size: 

1484 # We now **KNOW** the total number of bytes. 

1485 total_bytes = end_byte + 1 

1486 elif total_bytes == 0: 

1487 # NOTE: We also expect ``start_byte == 0`` here but don't check 

1488 # because ``_prepare_initiate_request()`` requires the 

1489 # stream to be at the beginning. 

1490 if num_bytes_read != 0: 

1491 raise ValueError( 

1492 "Stream specified as empty, but produced non-empty content." 

1493 ) 

1494 else: 

1495 if num_bytes_read == 0: 

1496 raise ValueError( 

1497 "Stream is already exhausted. There is no content remaining." 

1498 ) 

1499 

1500 content_range = get_content_range(start_byte, end_byte, total_bytes) 

1501 return start_byte, payload, content_range 

1502 

1503 

1504def get_content_range(start_byte, end_byte, total_bytes): 

1505 """Convert start, end and total into content range header. 

1506 

1507 If ``total_bytes`` is not known, uses "bytes {start}-{end}/*". 

1508 If we are dealing with an empty range (i.e. ``end_byte < start_byte``) 

1509 then "bytes */{total}" is used. 

1510 

1511 This function **ASSUMES** that if the size is not known, the caller will 

1512 not also pass an empty range. 

1513 

1514 Args: 

1515 start_byte (int): The start (inclusive) of the byte range. 

1516 end_byte (int): The end (inclusive) of the byte range. 

1517 total_bytes (Optional[int]): The number of bytes in the byte 

1518 range (if known). 

1519 

1520 Returns: 

1521 str: The content range header. 

1522 """ 

1523 if total_bytes is None: 

1524 return _RANGE_UNKNOWN_TEMPLATE.format(start_byte, end_byte) 

1525 elif end_byte < start_byte: 

1526 return _EMPTY_RANGE_TEMPLATE.format(total_bytes) 

1527 else: 

1528 return _CONTENT_RANGE_TEMPLATE.format(start_byte, end_byte, total_bytes)