|
8 | 8 | from urllib.parse import quote, urljoin |
9 | 9 |
|
10 | 10 | import pandas as pd |
| 11 | +from async_retrying import retry |
11 | 12 | from fsspec.callbacks import _DEFAULT_CALLBACK |
12 | 13 | from fsspec.implementations.http import HTTPFile, HTTPFileSystem, sync, sync_wrapper |
13 | 14 | from fsspec.utils import nullcontext |
@@ -418,6 +419,98 @@ def _construct_headers( |
418 | 419 |
|
419 | 420 | return headers, headers_chunk_lst |
420 | 421 |
|
| 422 | + def _cloud_copy( |
| 423 | + self, |
| 424 | + lpath, |
| 425 | + rpath, |
| 426 | + chunk_size=5 * 2**20, |
| 427 | + callback=_DEFAULT_CALLBACK, |
| 428 | + method="put", |
| 429 | + dt_from=None, |
| 430 | + dt_to=None, |
| 431 | + dt_created=None, |
| 432 | + ): |
| 433 | + async def _get_operation_id(session) -> dict: |
| 434 | + async with session.post( |
| 435 | + rpath + "/operationType/upload", **self.kwargs |
| 436 | + ) as r: |
| 437 | + await self._async_raise_not_found_for_status( |
| 438 | + r, rpath + "/operationType/upload" |
| 439 | + ) |
| 440 | + return await r.json() |
| 441 | + |
| 442 | + async def _finish_operation(session, operation_id, kw): |
| 443 | + async with session.post( |
| 444 | + url=rpath + f"/operations/upload?operationId={operation_id}", |
| 445 | + json={"parts": resps}, |
| 446 | + **kw, |
| 447 | + ) as r: |
| 448 | + await self._async_raise_not_found_for_status( |
| 449 | + r, rpath + f"/operations/upload?operationId={operation_id}" |
| 450 | + ) |
| 451 | + |
| 452 | + def put_data(session): |
| 453 | + @retry(attempts=3) |
| 454 | + async def _meth(session, url, kw): |
| 455 | + meth = getattr(session, method) |
| 456 | + async with meth(url=url, data=chunk, **kw) as resp: |
| 457 | + await self._async_raise_not_found_for_status(resp, url) |
| 458 | + return await resp.json() |
| 459 | + |
| 460 | + context = nullcontext(lpath) |
| 461 | + |
| 462 | + with context as f: |
| 463 | + callback.get_size(getattr(f, "size", None)) |
| 464 | + |
| 465 | + chunk = f.read(chunk_size) |
| 466 | + i = 0 |
| 467 | + headers_chunks = {"Content-Type": "application/octet-stream", "Digest": ""} |
| 468 | + while chunk: |
| 469 | + hash_sha256_chunk = hashlib.sha256() |
| 470 | + hash_sha256_chunk.update(chunk) |
| 471 | + hash_sha256_lst[0].update(hash_sha256_chunk.digest()) |
| 472 | + headers_chunks = deepcopy(headers_chunks) |
| 473 | + headers_chunks["Digest"] = ( |
| 474 | + "SHA-256=" + base64.b64encode(hash_sha256_chunk.digest()).decode() |
| 475 | + ) |
| 476 | + kw = self.kwargs.copy() |
| 477 | + kw.update({"headers": headers_chunks}) |
| 478 | + url = ( |
| 479 | + rpath |
| 480 | + + f"/operations/upload?operationId={operation_id}&partNumber={i+1}" |
| 481 | + ) |
| 482 | + yield sync(self.loop, _meth, session, url, kw) |
| 483 | + i += 1 |
| 484 | + callback.relative_update(len(chunk)) |
| 485 | + chunk = f.read(chunk_size) |
| 486 | + |
| 487 | + session = sync(self.loop, self.set_session) |
| 488 | + method = method.lower() |
| 489 | + if method not in ("put", "post"): |
| 490 | + raise ValueError(f"method has to be either 'post' or 'put', not {method!r}") |
| 491 | + hash_sha256 = hashlib.sha256() |
| 492 | + hash_sha256_lst = [hash_sha256] |
| 493 | + headers = { |
| 494 | + "Content-Type": "application/json", |
| 495 | + "x-jpmc-distribution-created-date": dt_created, |
| 496 | + "x-jpmc-distribution-from-date": dt_from, |
| 497 | + "x-jpmc-distribution-to-date": dt_to, |
| 498 | + "Digest": "", # to be changed to x-jpmc-digest |
| 499 | + } |
| 500 | + lpath.seek(0) |
| 501 | + kw = self.kwargs.copy() |
| 502 | + kw.update({"headers": headers}) |
| 503 | + operation_id = sync(self.loop, _get_operation_id, session)["operationId"] |
| 504 | + resps = [] |
| 505 | + for resp in put_data(session): |
| 506 | + resps.append(resp) |
| 507 | + |
| 508 | + hash_sha256 = hash_sha256_lst[0] |
| 509 | + headers["Digest"] = "SHA-256=" + base64.b64encode(hash_sha256.digest()).decode() |
| 510 | + kw = self.kwargs.copy() |
| 511 | + kw.update({"headers": headers}) |
| 512 | + sync(self.loop, _finish_operation, session, operation_id, kw) |
| 513 | + |
421 | 514 | def put( |
422 | 515 | self, |
423 | 516 | lpath, |
@@ -455,11 +548,14 @@ def put( |
455 | 548 | dt_to = pd.Timestamp(to_date).strftime("%Y-%m-%d") |
456 | 549 |
|
457 | 550 | dt_created = pd.Timestamp.now().strftime("%Y-%m-%d") |
458 | | - |
| 551 | + rpath = self._decorate_url(rpath) |
| 552 | + if type(lpath).__name__ in ["S3File"]: |
| 553 | + return self._cloud_copy( |
| 554 | + lpath, rpath, chunk_size, callback, method, dt_from, dt_to, dt_created |
| 555 | + ) |
459 | 556 | headers, chunk_headers_lst = self._construct_headers( |
460 | 557 | lpath, dt_from, dt_to, dt_created, chunk_size, multipart |
461 | 558 | ) |
462 | | - rpath = self._decorate_url(rpath) |
463 | 559 | kwargs.update({"headers": headers}) |
464 | 560 | if multipart: |
465 | 561 | kwargs.update({"chunk_headers_lst": chunk_headers_lst}) |
|
0 commit comments