Skip to content

Commit c89f097

Browse files
feat: Support upload big file version convenience method (box/box-codegen#988)
1 parent b49ce2c commit c89f097

4 files changed

Lines changed: 194 additions & 1 deletion

File tree

‎.codegen.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "engineHash": "3d7ee46", "specHash": "88cd5aa", "version": "10.15.0" }
1+
{ "engineHash": "0072e8b", "specHash": "88cd5aa", "version": "10.15.0" }

‎box_sdk_gen/managers/chunked_uploads.py‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from box_sdk_gen.internal.utils import Iterator
2222

23+
from box_sdk_gen.schemas.upload_part_plan_hit import UploadPartPlanHit
24+
2325
from box_sdk_gen.schemas.upload_session import UploadSession
2426

2527
from box_sdk_gen.schemas.client_error import ClientError
@@ -81,12 +83,15 @@ def __init__(
8183
file_size: int,
8284
upload_part_url: str,
8385
file_hash: Hash,
86+
*,
87+
plan_url: str = ''
8488
):
8589
self.last_index = last_index
8690
self.parts = parts
8791
self.file_size = file_size
8892
self.upload_part_url = upload_part_url
8993
self.file_hash = file_hash
94+
self.plan_url = plan_url
9095

9196

9297
class ChunkedUploadsManager:
@@ -932,6 +937,7 @@ def _reducer(self, acc: _PartAccumulator, chunk: ByteStream) -> _PartAccumulator
932937
file_size=acc.file_size,
933938
upload_part_url=acc.upload_part_url,
934939
file_hash=acc.file_hash,
940+
plan_url=acc.plan_url,
935941
)
936942

937943
def upload_big_file(
@@ -982,3 +988,129 @@ def upload_big_file(
982988
self.create_file_upload_session_commit_by_url(commit_url, parts, digest)
983989
)
984990
return committed_session.entries[0]
991+
992+
def _get_cached_upload_part(
993+
self, plan_url: str, offset: int, size: int, sha_512: str
994+
) -> Optional[UploadPart]:
995+
plan: UploadSessionPlanResponse = self.create_file_upload_session_plan_by_url(
996+
plan_url, [UploadPartPlan(offset=offset, size=size, sha_512=sha_512)]
997+
)
998+
if len(plan.hits) > 0:
999+
hit: UploadPartPlanHit = plan.hits[0]
1000+
return UploadPart(part_id=hit.part_id, offset=hit.offset, size=hit.size)
1001+
return None
1002+
1003+
def _reducer_for_file_version(
1004+
self, acc: _PartAccumulator, chunk: ByteStream
1005+
) -> _PartAccumulator:
1006+
last_index: int = acc.last_index
1007+
parts: List[UploadPart] = acc.parts
1008+
chunk_buffer: Buffer = read_byte_stream(chunk)
1009+
hash: Hash = Hash(algorithm=HashName.SHA1)
1010+
hash.update_hash(chunk_buffer)
1011+
sha_1: str = hash.digest_hash('base64')
1012+
digest: str = ''.join(['sha=', sha_1])
1013+
chunk_size: int = buffer_length(chunk_buffer)
1014+
bytes_start: int = last_index + 1
1015+
bytes_end: int = last_index + chunk_size
1016+
content_range: str = ''.join(
1017+
[
1018+
'bytes ',
1019+
to_string(bytes_start),
1020+
'-',
1021+
to_string(bytes_end),
1022+
'/',
1023+
to_string(acc.file_size),
1024+
]
1025+
)
1026+
sha_512_hash: Hash = Hash(algorithm=HashName.SHA512)
1027+
sha_512_hash.update_hash(chunk_buffer)
1028+
sha_512: str = sha_512_hash.digest_hash('hex')
1029+
cached_part: Optional[UploadPart] = self._get_cached_upload_part(
1030+
acc.plan_url, bytes_start, chunk_size, sha_512
1031+
)
1032+
if not cached_part == None:
1033+
acc.file_hash.update_hash(chunk_buffer)
1034+
return _PartAccumulator(
1035+
last_index=bytes_end,
1036+
parts=parts + [cached_part],
1037+
file_size=acc.file_size,
1038+
upload_part_url=acc.upload_part_url,
1039+
file_hash=acc.file_hash,
1040+
plan_url=acc.plan_url,
1041+
)
1042+
uploaded_part: UploadedPart = self.upload_file_part_by_url(
1043+
acc.upload_part_url,
1044+
generate_byte_stream_from_buffer(chunk_buffer),
1045+
digest,
1046+
content_range,
1047+
)
1048+
part: UploadPart = uploaded_part.part
1049+
part_sha_1: str = hex_to_base_64(part.sha_1)
1050+
assert part_sha_1 == sha_1
1051+
assert part.size == chunk_size
1052+
assert part.offset == bytes_start
1053+
acc.file_hash.update_hash(chunk_buffer)
1054+
return _PartAccumulator(
1055+
last_index=bytes_end,
1056+
parts=parts + [part],
1057+
file_size=acc.file_size,
1058+
upload_part_url=acc.upload_part_url,
1059+
file_hash=acc.file_hash,
1060+
plan_url=acc.plan_url,
1061+
)
1062+
1063+
def upload_big_file_version(
1064+
self,
1065+
file_id: str,
1066+
file: ByteStream,
1067+
file_size: int,
1068+
*,
1069+
file_name: Optional[str] = None
1070+
) -> Optional[FileFull]:
1071+
"""
1072+
Starts the process of chunk uploading a new version of a big file. Should return a File object representing the uploaded file version. Returns nothing when commit responds with 202 because the file did not change.
1073+
:param file_id: The ID of the file to upload a new version of.
1074+
:type file_id: str
1075+
:param file: The stream of the file to upload.
1076+
:type file: ByteStream
1077+
:param file_size: The total size of the file for the chunked upload in bytes.
1078+
:type file_size: int
1079+
:param file_name: The optional new name of the file., defaults to None
1080+
:type file_name: Optional[str], optional
1081+
"""
1082+
upload_session: UploadSession = (
1083+
self.create_file_upload_session_for_existing_file(
1084+
file_id, file_size, file_name=file_name
1085+
)
1086+
)
1087+
upload_part_url: str = upload_session.session_endpoints.upload_part
1088+
commit_url: str = upload_session.session_endpoints.commit
1089+
plan_url: str = upload_session.session_endpoints.plan
1090+
part_size: int = upload_session.part_size
1091+
total_parts: int = upload_session.total_parts
1092+
assert part_size * total_parts >= file_size
1093+
assert upload_session.num_parts_processed == 0
1094+
file_hash: Hash = Hash(algorithm=HashName.SHA1)
1095+
chunks_iterator: Iterator = iterate_chunks(file, part_size, file_size)
1096+
results: _PartAccumulator = reduce_iterator(
1097+
chunks_iterator,
1098+
self._reducer_for_file_version,
1099+
_PartAccumulator(
1100+
last_index=-1,
1101+
parts=[],
1102+
file_size=file_size,
1103+
upload_part_url=upload_part_url,
1104+
file_hash=file_hash,
1105+
plan_url=plan_url,
1106+
),
1107+
)
1108+
parts: List[UploadPart] = results.parts
1109+
sha_1: str = file_hash.digest_hash('base64')
1110+
digest: str = ''.join(['sha=', sha_1])
1111+
committed_session: Optional[Files] = (
1112+
self.create_file_upload_session_commit_by_url(commit_url, parts, digest)
1113+
)
1114+
if committed_session == None:
1115+
return None
1116+
return committed_session.entries[0]

‎docs/chunked_uploads.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This is a manager for chunked uploads (allowed for files at least 20MB).
1717
- [Commit upload session by URL](#commit-upload-session-by-url)
1818
- [Commit upload session](#commit-upload-session)
1919
- [Upload big file](#upload-big-file)
20+
- [Upload big file version](#upload-big-file-version)
2021

2122
## Create upload session
2223

@@ -555,3 +556,33 @@ client.chunked_uploads.upload_big_file(
555556
### Returns
556557

557558
This function returns a value of type `FileFull`.
559+
560+
## Upload big file version
561+
562+
Starts the process of chunk uploading a new version of a big file. Should return a File object representing the uploaded file version. Returns nothing when commit responds with 202 because the file did not change.
563+
564+
This operation is performed by calling function `upload_big_file_version`.
565+
566+
```python
567+
client.chunked_uploads.upload_big_file_version(
568+
uploaded_file.id,
569+
generate_byte_stream(version_file_size),
570+
version_file_size,
571+
file_name=version_name,
572+
)
573+
```
574+
575+
### Arguments
576+
577+
- file_id `str`
578+
- The ID of the file to upload a new version of.
579+
- file `ByteStream`
580+
- The stream of the file to upload.
581+
- file_size `int`
582+
- The total size of the file for the chunked upload in bytes.
583+
- file_name `Optional[str]`
584+
- The optional new name of the file.
585+
586+
### Returns
587+
588+
This function returns a value of type `Optional[FileFull]`.

‎test/chunked_uploads.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from box_sdk_gen.schemas.upload_part_plan_hit import UploadPartPlanHit
1414

15+
from box_sdk_gen.schemas.file_full import FileFull
16+
1517
from box_sdk_gen.internal.utils import generate_byte_stream_from_buffer
1618

1719
from box_sdk_gen.internal.utils import hex_to_base_64
@@ -347,3 +349,31 @@ def testChunkedUploadConvenienceMethod():
347349
assert uploaded_file.size == file_size
348350
assert uploaded_file.parent.id == parent_folder_id
349351
client.files.delete_file_by_id(uploaded_file.id)
352+
353+
354+
def testChunkedUploadFileVersionConvenienceMethod():
355+
file_name: str = get_uuid()
356+
file_size: int = (20 * 1024) * 1024
357+
parent_folder_id: str = '0'
358+
uploaded_file: File = client.chunked_uploads.upload_big_file(
359+
generate_byte_stream(file_size), file_name, file_size, parent_folder_id
360+
)
361+
assert uploaded_file.name == file_name
362+
assert uploaded_file.size == file_size
363+
version_file_size: int = (21 * 1024) * 1024
364+
version_name: str = get_uuid()
365+
uploaded_file_version: Optional[FileFull] = (
366+
client.chunked_uploads.upload_big_file_version(
367+
uploaded_file.id,
368+
generate_byte_stream(version_file_size),
369+
version_file_size,
370+
file_name=version_name,
371+
)
372+
)
373+
assert not uploaded_file_version == None
374+
assert uploaded_file_version.id == uploaded_file.id
375+
assert uploaded_file_version.name == version_name
376+
assert uploaded_file_version.size == version_file_size
377+
assert not uploaded_file_version.name == file_name
378+
assert not uploaded_file_version.size == uploaded_file.size
379+
client.files.delete_file_by_id(uploaded_file.id)

0 commit comments

Comments
 (0)