From ccb314f107152d7aa67ddb788cd596c6ab200e84 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Thu, 3 Sep 2026 08:03:48 -0700 Subject: [PATCH 1/5] feat: support chunk upload avoidance API (box/box-openapi#616) --- .codegen.json | 2 +- docs/sdkgen/chunkeduploads.md | 70 +++++++++++ .../chunkeduploads/ChunkedUploadsManager.java | 117 ++++++++++++++++++ ...eateFileUploadSessionPlanByUrlHeaders.java | 42 +++++++ .../CreateFileUploadSessionPlanHeaders.java | 42 +++++++ .../uploadpartplan/UploadPartPlan.java | 81 ++++++++++++ .../uploadpartplanhit/UploadPartPlanHit.java | 96 ++++++++++++++ .../UploadSessionSessionEndpointsField.java | 26 +++- .../UploadSessionPlanRequest.java | 50 ++++++++ .../UploadSessionPlanResponse.java | 85 +++++++++++++ 10 files changed, 608 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanByUrlHeaders.java create mode 100644 src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanHeaders.java create mode 100644 src/main/java/com/box/sdkgen/schemas/uploadpartplan/UploadPartPlan.java create mode 100644 src/main/java/com/box/sdkgen/schemas/uploadpartplanhit/UploadPartPlanHit.java create mode 100644 src/main/java/com/box/sdkgen/schemas/uploadsessionplanrequest/UploadSessionPlanRequest.java create mode 100644 src/main/java/com/box/sdkgen/schemas/uploadsessionplanresponse/UploadSessionPlanResponse.java diff --git a/.codegen.json b/.codegen.json index f5f7e2679..33b8585ec 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "04310d4", "specHash": "be75fa1", "version": "5.15.2" } +{ "engineHash": "04310d4", "specHash": "88cd5aa", "version": "5.15.2" } diff --git a/docs/sdkgen/chunkeduploads.md b/docs/sdkgen/chunkeduploads.md index 715868fb4..07203bfd6 100644 --- a/docs/sdkgen/chunkeduploads.md +++ b/docs/sdkgen/chunkeduploads.md @@ -12,6 +12,8 @@ This is a manager for chunked uploads (allowed for files at least 20MB). - [Remove upload session](#remove-upload-session) - [List parts by URL](#list-parts-by-url) - [List parts](#list-parts) +- [Plan upload session by URL](#plan-upload-session-by-url) +- [Plan upload session](#plan-upload-session) - [Commit upload session by URL](#commit-upload-session-by-url) - [Commit upload session](#commit-upload-session) - [Upload big file](#upload-big-file) @@ -341,6 +343,74 @@ This function returns a value of type `UploadParts`. Returns a list of parts that have been uploaded. +## Plan upload session by URL + +Plan an upload session by checking which parts already exist on the server. +This endpoint allows clients to optimize uploads by skipping parts that +have already been uploaded (cache hits) and only uploading missing parts. + +The actual endpoint URL is returned by the [`Create upload session`](e://post-files-upload-sessions) +and [`Get upload session`](e://get-files-upload-sessions-id) endpoints. + +This operation is performed by calling function `createFileUploadSessionPlanByUrl`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/post-files-upload-sessions-id-plan/). + +*Currently we don't have an example for calling `createFileUploadSessionPlanByUrl` in integration tests* + +### Arguments + +- url `String` + - URL of createFileUploadSessionPlan method +- requestBody `UploadSessionPlanRequest` + - Request body of createFileUploadSessionPlan method +- headers `CreateFileUploadSessionPlanByUrlHeaders` + - Headers of createFileUploadSessionPlan method + + +### Returns + +This function returns a value of type `UploadSessionPlanResponse`. + +Returns information about which parts already exist (hits) +and which parts need to be uploaded (misses). + + +## Plan upload session + +Plan an upload session by checking which parts already exist on the server. +This endpoint allows clients to optimize uploads by skipping parts that +have already been uploaded (cache hits) and only uploading missing parts. + +The actual endpoint URL is returned by the [`Create upload session`](e://post-files-upload-sessions) +and [`Get upload session`](e://get-files-upload-sessions-id) endpoints. + +This operation is performed by calling function `createFileUploadSessionPlan`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/post-files-upload-sessions-id-plan/). + +*Currently we don't have an example for calling `createFileUploadSessionPlan` in integration tests* + +### Arguments + +- uploadSessionId `String` + - The ID of the upload session. Example: "D5E3F7A" +- requestBody `UploadSessionPlanRequest` + - Request body of createFileUploadSessionPlan method +- headers `CreateFileUploadSessionPlanHeaders` + - Headers of createFileUploadSessionPlan method + + +### Returns + +This function returns a value of type `UploadSessionPlanResponse`. + +Returns information about which parts already exist (hits) +and which parts need to be uploaded (misses). + + ## Commit upload session by URL Close an upload session and create a file from the uploaded chunks. diff --git a/src/main/java/com/box/sdkgen/managers/chunkeduploads/ChunkedUploadsManager.java b/src/main/java/com/box/sdkgen/managers/chunkeduploads/ChunkedUploadsManager.java index 999b5fc69..efcc13a92 100644 --- a/src/main/java/com/box/sdkgen/managers/chunkeduploads/ChunkedUploadsManager.java +++ b/src/main/java/com/box/sdkgen/managers/chunkeduploads/ChunkedUploadsManager.java @@ -25,6 +25,8 @@ import com.box.sdkgen.schemas.uploadpart.UploadPart; import com.box.sdkgen.schemas.uploadparts.UploadParts; import com.box.sdkgen.schemas.uploadsession.UploadSession; +import com.box.sdkgen.schemas.uploadsessionplanrequest.UploadSessionPlanRequest; +import com.box.sdkgen.schemas.uploadsessionplanresponse.UploadSessionPlanResponse; import com.box.sdkgen.serialization.json.JsonManager; import java.io.InputStream; import java.util.Arrays; @@ -586,6 +588,121 @@ public UploadParts getFileUploadSessionParts( return JsonManager.deserialize(response.getData(), UploadParts.class); } + /** + * Using this method with urls provided in response when creating a new upload session is + * preferred to use over CreateFileUploadSessionPlan method. This allows to always upload your + * content to the closest Box data center and can significantly improve upload speed. Plan an + * upload session by checking which parts already exist on the server. This endpoint allows + * clients to optimize uploads by skipping parts that have already been uploaded (cache hits) and + * only uploading missing parts. + * + *

The actual endpoint URL is returned by the [`Create upload + * session`](e://post-files-upload-sessions) and [`Get upload + * session`](e://get-files-upload-sessions-id) endpoints. + * + * @param url URL of createFileUploadSessionPlan method + * @param requestBody Request body of createFileUploadSessionPlan method + */ + public UploadSessionPlanResponse createFileUploadSessionPlanByUrl( + String url, UploadSessionPlanRequest requestBody) { + return createFileUploadSessionPlanByUrl( + url, requestBody, new CreateFileUploadSessionPlanByUrlHeaders()); + } + + /** + * Using this method with urls provided in response when creating a new upload session is + * preferred to use over CreateFileUploadSessionPlan method. This allows to always upload your + * content to the closest Box data center and can significantly improve upload speed. Plan an + * upload session by checking which parts already exist on the server. This endpoint allows + * clients to optimize uploads by skipping parts that have already been uploaded (cache hits) and + * only uploading missing parts. + * + *

The actual endpoint URL is returned by the [`Create upload + * session`](e://post-files-upload-sessions) and [`Get upload + * session`](e://get-files-upload-sessions-id) endpoints. + * + * @param url URL of createFileUploadSessionPlan method + * @param requestBody Request body of createFileUploadSessionPlan method + * @param headers Headers of createFileUploadSessionPlan method + */ + public UploadSessionPlanResponse createFileUploadSessionPlanByUrl( + String url, + UploadSessionPlanRequest requestBody, + CreateFileUploadSessionPlanByUrlHeaders headers) { + Map headersMap = prepareParams(mergeMaps(mapOf(), headers.getExtraHeaders())); + FetchResponse response = + this.networkSession + .getNetworkClient() + .fetch( + new FetchOptions.Builder(url, "POST") + .headers(headersMap) + .data(JsonManager.serialize(requestBody)) + .contentType("application/json") + .responseFormat(ResponseFormat.JSON) + .auth(this.auth) + .networkSession(this.networkSession) + .build()); + return JsonManager.deserialize(response.getData(), UploadSessionPlanResponse.class); + } + + /** + * Plan an upload session by checking which parts already exist on the server. This endpoint + * allows clients to optimize uploads by skipping parts that have already been uploaded (cache + * hits) and only uploading missing parts. + * + *

The actual endpoint URL is returned by the [`Create upload + * session`](e://post-files-upload-sessions) and [`Get upload + * session`](e://get-files-upload-sessions-id) endpoints. + * + * @param uploadSessionId The ID of the upload session. Example: "D5E3F7A" + * @param requestBody Request body of createFileUploadSessionPlan method + */ + public UploadSessionPlanResponse createFileUploadSessionPlan( + String uploadSessionId, UploadSessionPlanRequest requestBody) { + return createFileUploadSessionPlan( + uploadSessionId, requestBody, new CreateFileUploadSessionPlanHeaders()); + } + + /** + * Plan an upload session by checking which parts already exist on the server. This endpoint + * allows clients to optimize uploads by skipping parts that have already been uploaded (cache + * hits) and only uploading missing parts. + * + *

The actual endpoint URL is returned by the [`Create upload + * session`](e://post-files-upload-sessions) and [`Get upload + * session`](e://get-files-upload-sessions-id) endpoints. + * + * @param uploadSessionId The ID of the upload session. Example: "D5E3F7A" + * @param requestBody Request body of createFileUploadSessionPlan method + * @param headers Headers of createFileUploadSessionPlan method + */ + public UploadSessionPlanResponse createFileUploadSessionPlan( + String uploadSessionId, + UploadSessionPlanRequest requestBody, + CreateFileUploadSessionPlanHeaders headers) { + Map headersMap = prepareParams(mergeMaps(mapOf(), headers.getExtraHeaders())); + FetchResponse response = + this.networkSession + .getNetworkClient() + .fetch( + new FetchOptions.Builder( + String.join( + "", + this.networkSession.getBaseUrls().getUploadUrl(), + "/2.0/files/upload_sessions/", + convertToString(uploadSessionId), + "/plan"), + "POST") + .headers(headersMap) + .data(JsonManager.serialize(requestBody)) + .contentType("application/json") + .responseFormat(ResponseFormat.JSON) + .auth(this.auth) + .networkSession(this.networkSession) + .build()); + return JsonManager.deserialize(response.getData(), UploadSessionPlanResponse.class); + } + /** * Using this method with urls provided in response when creating a new upload session is * preferred to use over CreateFileUploadSessionCommit method. This allows to always upload your diff --git a/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanByUrlHeaders.java b/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanByUrlHeaders.java new file mode 100644 index 000000000..460036659 --- /dev/null +++ b/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanByUrlHeaders.java @@ -0,0 +1,42 @@ +package com.box.sdkgen.managers.chunkeduploads; + +import static com.box.sdkgen.internal.utils.UtilsManager.mapOf; + +import java.util.Map; + +public class CreateFileUploadSessionPlanByUrlHeaders { + + /** Extra headers that will be included in the HTTP request. */ + public Map extraHeaders; + + public CreateFileUploadSessionPlanByUrlHeaders() { + this.extraHeaders = mapOf(); + } + + protected CreateFileUploadSessionPlanByUrlHeaders(Builder builder) { + this.extraHeaders = builder.extraHeaders; + } + + public Map getExtraHeaders() { + return extraHeaders; + } + + public static class Builder { + + protected Map extraHeaders; + + public Builder() {} + + public Builder extraHeaders(Map extraHeaders) { + this.extraHeaders = extraHeaders; + return this; + } + + public CreateFileUploadSessionPlanByUrlHeaders build() { + if (this.extraHeaders == null) { + this.extraHeaders = mapOf(); + } + return new CreateFileUploadSessionPlanByUrlHeaders(this); + } + } +} diff --git a/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanHeaders.java b/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanHeaders.java new file mode 100644 index 000000000..bfe820e92 --- /dev/null +++ b/src/main/java/com/box/sdkgen/managers/chunkeduploads/CreateFileUploadSessionPlanHeaders.java @@ -0,0 +1,42 @@ +package com.box.sdkgen.managers.chunkeduploads; + +import static com.box.sdkgen.internal.utils.UtilsManager.mapOf; + +import java.util.Map; + +public class CreateFileUploadSessionPlanHeaders { + + /** Extra headers that will be included in the HTTP request. */ + public Map extraHeaders; + + public CreateFileUploadSessionPlanHeaders() { + this.extraHeaders = mapOf(); + } + + protected CreateFileUploadSessionPlanHeaders(Builder builder) { + this.extraHeaders = builder.extraHeaders; + } + + public Map getExtraHeaders() { + return extraHeaders; + } + + public static class Builder { + + protected Map extraHeaders; + + public Builder() {} + + public Builder extraHeaders(Map extraHeaders) { + this.extraHeaders = extraHeaders; + return this; + } + + public CreateFileUploadSessionPlanHeaders build() { + if (this.extraHeaders == null) { + this.extraHeaders = mapOf(); + } + return new CreateFileUploadSessionPlanHeaders(this); + } + } +} diff --git a/src/main/java/com/box/sdkgen/schemas/uploadpartplan/UploadPartPlan.java b/src/main/java/com/box/sdkgen/schemas/uploadpartplan/UploadPartPlan.java new file mode 100644 index 000000000..40ac5fe0b --- /dev/null +++ b/src/main/java/com/box/sdkgen/schemas/uploadpartplan/UploadPartPlan.java @@ -0,0 +1,81 @@ +package com.box.sdkgen.schemas.uploadpartplan; + +import com.box.sdkgen.internal.SerializableObject; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + +/** Represents a planned upload part with `SHA-512` hash for upload session planning. */ +@JsonFilter("nullablePropertyFilter") +public class UploadPartPlan extends SerializableObject { + + /** + * The offset of the chunk within the file in bytes. The lower bound of the position of the chunk + * within the file. + */ + protected final long offset; + + /** The size of the chunk in bytes. */ + protected final long size; + + /** The `SHA-512` hash of the chunk. */ + protected final String sha512; + + public UploadPartPlan( + @JsonProperty("offset") long offset, + @JsonProperty("size") long size, + @JsonProperty("sha512") String sha512) { + super(); + this.offset = offset; + this.size = size; + this.sha512 = sha512; + } + + public long getOffset() { + return offset; + } + + public long getSize() { + return size; + } + + public String getSha512() { + return sha512; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UploadPartPlan casted = (UploadPartPlan) o; + return Objects.equals(offset, casted.offset) + && Objects.equals(size, casted.size) + && Objects.equals(sha512, casted.sha512); + } + + @Override + public int hashCode() { + return Objects.hash(offset, size, sha512); + } + + @Override + public String toString() { + return "UploadPartPlan{" + + "offset='" + + offset + + '\'' + + ", " + + "size='" + + size + + '\'' + + ", " + + "sha512='" + + sha512 + + '\'' + + "}"; + } +} diff --git a/src/main/java/com/box/sdkgen/schemas/uploadpartplanhit/UploadPartPlanHit.java b/src/main/java/com/box/sdkgen/schemas/uploadpartplanhit/UploadPartPlanHit.java new file mode 100644 index 000000000..ba04991a5 --- /dev/null +++ b/src/main/java/com/box/sdkgen/schemas/uploadpartplanhit/UploadPartPlanHit.java @@ -0,0 +1,96 @@ +package com.box.sdkgen.schemas.uploadpartplanhit; + +import com.box.sdkgen.internal.SerializableObject; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + +/** Represents a planned upload part that already exists on the server (cache hit). */ +@JsonFilter("nullablePropertyFilter") +public class UploadPartPlanHit extends SerializableObject { + + /** + * The offset of the chunk within the file in bytes. The lower bound of the position of the chunk + * within the file. + */ + protected final long offset; + + /** The size of the chunk in bytes. */ + protected final long size; + + /** The `SHA-512` hash of the chunk. */ + protected final String sha512; + + /** The unique ID of the chunk. */ + @JsonProperty("part_id") + protected final String partId; + + public UploadPartPlanHit( + @JsonProperty("offset") long offset, + @JsonProperty("size") long size, + @JsonProperty("sha512") String sha512, + @JsonProperty("part_id") String partId) { + super(); + this.offset = offset; + this.size = size; + this.sha512 = sha512; + this.partId = partId; + } + + public long getOffset() { + return offset; + } + + public long getSize() { + return size; + } + + public String getSha512() { + return sha512; + } + + public String getPartId() { + return partId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UploadPartPlanHit casted = (UploadPartPlanHit) o; + return Objects.equals(offset, casted.offset) + && Objects.equals(size, casted.size) + && Objects.equals(sha512, casted.sha512) + && Objects.equals(partId, casted.partId); + } + + @Override + public int hashCode() { + return Objects.hash(offset, size, sha512, partId); + } + + @Override + public String toString() { + return "UploadPartPlanHit{" + + "offset='" + + offset + + '\'' + + ", " + + "size='" + + size + + '\'' + + ", " + + "sha512='" + + sha512 + + '\'' + + ", " + + "partId='" + + partId + + '\'' + + "}"; + } +} diff --git a/src/main/java/com/box/sdkgen/schemas/uploadsession/UploadSessionSessionEndpointsField.java b/src/main/java/com/box/sdkgen/schemas/uploadsession/UploadSessionSessionEndpointsField.java index 11c205c5f..e202f1e21 100644 --- a/src/main/java/com/box/sdkgen/schemas/uploadsession/UploadSessionSessionEndpointsField.java +++ b/src/main/java/com/box/sdkgen/schemas/uploadsession/UploadSessionSessionEndpointsField.java @@ -9,6 +9,11 @@ @JsonFilter("nullablePropertyFilter") public class UploadSessionSessionEndpointsField extends SerializableObject { + /** + * The URL used to plan the upload session by checking which parts already exist on the server. + */ + protected String plan; + /** The URL to upload parts to. */ @JsonProperty("upload_part") protected String uploadPart; @@ -36,6 +41,7 @@ public UploadSessionSessionEndpointsField() { protected UploadSessionSessionEndpointsField(Builder builder) { super(); + this.plan = builder.plan; this.uploadPart = builder.uploadPart; this.commit = builder.commit; this.abort = builder.abort; @@ -45,6 +51,10 @@ protected UploadSessionSessionEndpointsField(Builder builder) { markNullableFieldsAsSet(builder.getExplicitlySetNullableFields()); } + public String getPlan() { + return plan; + } + public String getUploadPart() { return uploadPart; } @@ -78,7 +88,8 @@ public boolean equals(Object o) { return false; } UploadSessionSessionEndpointsField casted = (UploadSessionSessionEndpointsField) o; - return Objects.equals(uploadPart, casted.uploadPart) + return Objects.equals(plan, casted.plan) + && Objects.equals(uploadPart, casted.uploadPart) && Objects.equals(commit, casted.commit) && Objects.equals(abort, casted.abort) && Objects.equals(listParts, casted.listParts) @@ -88,12 +99,16 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(uploadPart, commit, abort, listParts, status, logEvent); + return Objects.hash(plan, uploadPart, commit, abort, listParts, status, logEvent); } @Override public String toString() { return "UploadSessionSessionEndpointsField{" + + "plan='" + + plan + + '\'' + + ", " + "uploadPart='" + uploadPart + '\'' @@ -122,6 +137,8 @@ public String toString() { public static class Builder extends NullableFieldTracker { + protected String plan; + protected String uploadPart; protected String commit; @@ -134,6 +151,11 @@ public static class Builder extends NullableFieldTracker { protected String logEvent; + public Builder plan(String plan) { + this.plan = plan; + return this; + } + public Builder uploadPart(String uploadPart) { this.uploadPart = uploadPart; return this; diff --git a/src/main/java/com/box/sdkgen/schemas/uploadsessionplanrequest/UploadSessionPlanRequest.java b/src/main/java/com/box/sdkgen/schemas/uploadsessionplanrequest/UploadSessionPlanRequest.java new file mode 100644 index 000000000..86b65f512 --- /dev/null +++ b/src/main/java/com/box/sdkgen/schemas/uploadsessionplanrequest/UploadSessionPlanRequest.java @@ -0,0 +1,50 @@ +package com.box.sdkgen.schemas.uploadsessionplanrequest; + +import com.box.sdkgen.internal.SerializableObject; +import com.box.sdkgen.schemas.uploadpartplan.UploadPartPlan; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; + +/** + * Request body for planning an upload session. This allows checking which parts already exist on + * the server before uploading. + */ +@JsonFilter("nullablePropertyFilter") +public class UploadSessionPlanRequest extends SerializableObject { + + /** The list of parts to check for existence. */ + protected final List parts; + + public UploadSessionPlanRequest(@JsonProperty("parts") List parts) { + super(); + this.parts = parts; + } + + public List getParts() { + return parts; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UploadSessionPlanRequest casted = (UploadSessionPlanRequest) o; + return Objects.equals(parts, casted.parts); + } + + @Override + public int hashCode() { + return Objects.hash(parts); + } + + @Override + public String toString() { + return "UploadSessionPlanRequest{" + "parts='" + parts + '\'' + "}"; + } +} diff --git a/src/main/java/com/box/sdkgen/schemas/uploadsessionplanresponse/UploadSessionPlanResponse.java b/src/main/java/com/box/sdkgen/schemas/uploadsessionplanresponse/UploadSessionPlanResponse.java new file mode 100644 index 000000000..488e8d56e --- /dev/null +++ b/src/main/java/com/box/sdkgen/schemas/uploadsessionplanresponse/UploadSessionPlanResponse.java @@ -0,0 +1,85 @@ +package com.box.sdkgen.schemas.uploadsessionplanresponse; + +import com.box.sdkgen.internal.SerializableObject; +import com.box.sdkgen.schemas.uploadpartplan.UploadPartPlan; +import com.box.sdkgen.schemas.uploadpartplanhit.UploadPartPlanHit; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; + +/** + * Response from planning an upload session. Contains information about which parts already exist + * (hits) and which need to be uploaded (misses). + */ +@JsonFilter("nullablePropertyFilter") +public class UploadSessionPlanResponse extends SerializableObject { + + /** The unique identifier for this upload session. */ + @JsonProperty("upload_session_id") + protected final String uploadSessionId; + + /** Parts that already exist on the server and do not need to be uploaded again. */ + protected final List hits; + + /** Parts that do not exist on the server and need to be uploaded. */ + protected final List misses; + + public UploadSessionPlanResponse( + @JsonProperty("upload_session_id") String uploadSessionId, + @JsonProperty("hits") List hits, + @JsonProperty("misses") List misses) { + super(); + this.uploadSessionId = uploadSessionId; + this.hits = hits; + this.misses = misses; + } + + public String getUploadSessionId() { + return uploadSessionId; + } + + public List getHits() { + return hits; + } + + public List getMisses() { + return misses; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UploadSessionPlanResponse casted = (UploadSessionPlanResponse) o; + return Objects.equals(uploadSessionId, casted.uploadSessionId) + && Objects.equals(hits, casted.hits) + && Objects.equals(misses, casted.misses); + } + + @Override + public int hashCode() { + return Objects.hash(uploadSessionId, hits, misses); + } + + @Override + public String toString() { + return "UploadSessionPlanResponse{" + + "uploadSessionId='" + + uploadSessionId + + '\'' + + ", " + + "hits='" + + hits + + '\'' + + ", " + + "misses='" + + misses + + '\'' + + "}"; + } +} From ce197a2890e987220787a31739d78cea1d414823 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Thu, 3 Sep 2026 08:05:53 -0700 Subject: [PATCH 2/5] chore: Update `.codegen.json` with commit hash of `codegen` and `openapi` spec [skip ci] --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 33b8585ec..27a0e6777 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "04310d4", "specHash": "88cd5aa", "version": "5.15.2" } +{ "engineHash": "db84f4c", "specHash": "88cd5aa", "version": "5.15.2" } From e856f68dd0f30383d582be1201d012e6a33a6d8c Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Fri, 4 Sep 2026 08:45:36 -0700 Subject: [PATCH 3/5] chore: Update `.codegen.json` with commit hash of `codegen` and `openapi` spec [skip ci] --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 27a0e6777..59e51b1bc 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "db84f4c", "specHash": "88cd5aa", "version": "5.15.2" } +{ "engineHash": "ad1ed4e", "specHash": "88cd5aa", "version": "5.15.2" } From ecf2440a1880171a21163f682427b7bac1d74443 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Mon, 7 Sep 2026 04:15:57 -0700 Subject: [PATCH 4/5] test: Add tests for chunked upload plan endpoint (box/box-codegen#975) --- .codegen.json | 2 +- docs/sdkgen/chunkeduploads.md | 12 ++- .../chunkeduploads/ChunkedUploadsITest.java | 73 +++++++++++++++++++ .../TestPartPlanAccumulator.java | 31 ++++++++ .../com/box/sdkgen/internal/utils/Hash.java | 17 ++++- .../box/sdkgen/internal/utils/HashName.java | 3 +- .../sdkgen/internal/utils/UtilsManager.java | 12 ++- 7 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 src/intTest/java/com/box/sdkgen/chunkeduploads/TestPartPlanAccumulator.java diff --git a/.codegen.json b/.codegen.json index 59e51b1bc..8f692a286 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "ad1ed4e", "specHash": "88cd5aa", "version": "5.15.2" } +{ "engineHash": "bcfc85f", "specHash": "88cd5aa", "version": "5.15.2" } diff --git a/docs/sdkgen/chunkeduploads.md b/docs/sdkgen/chunkeduploads.md index 07203bfd6..fc4d1a6ca 100644 --- a/docs/sdkgen/chunkeduploads.md +++ b/docs/sdkgen/chunkeduploads.md @@ -56,7 +56,10 @@ This operation is performed by calling function `createFileUploadSessionForExist See the endpoint docs at [API Reference](https://developer.box.com/reference/post-files-id-upload-sessions/). -*Currently we don't have an example for calling `createFileUploadSessionForExistingFile` in integration tests* + +``` +client.getChunkedUploads().createFileUploadSessionForExistingFile(uploadedFile.getId(), new CreateFileUploadSessionForExistingFileRequestBody(fileSize)) +``` ### Arguments @@ -357,7 +360,10 @@ This operation is performed by calling function `createFileUploadSessionPlanByUr See the endpoint docs at [API Reference](https://developer.box.com/reference/post-files-upload-sessions-id-plan/). -*Currently we don't have an example for calling `createFileUploadSessionPlanByUrl` in integration tests* + +``` +client.getChunkedUploads().createFileUploadSessionPlanByUrl(planUrl, new UploadSessionPlanRequest(parts)) +``` ### Arguments @@ -496,7 +502,7 @@ This operation is performed by calling function `uploadBigFile`. ``` -client.getChunkedUploads().uploadBigFile(fileByteStream, fileName, fileSize, parentFolderId) +client.getChunkedUploads().uploadBigFile(generateByteStreamFromBuffer(fileBuffer), fileName, fileSize, parentFolderId) ``` ### Arguments diff --git a/src/intTest/java/com/box/sdkgen/chunkeduploads/ChunkedUploadsITest.java b/src/intTest/java/com/box/sdkgen/chunkeduploads/ChunkedUploadsITest.java index 1536763ee..c6c16080f 100644 --- a/src/intTest/java/com/box/sdkgen/chunkeduploads/ChunkedUploadsITest.java +++ b/src/intTest/java/com/box/sdkgen/chunkeduploads/ChunkedUploadsITest.java @@ -3,6 +3,7 @@ import static com.box.sdkgen.commons.CommonsManager.getDefaultClient; import static com.box.sdkgen.internal.utils.UtilsManager.bufferLength; import static com.box.sdkgen.internal.utils.UtilsManager.convertToString; +import static com.box.sdkgen.internal.utils.UtilsManager.delayInSeconds; import static com.box.sdkgen.internal.utils.UtilsManager.generateByteStream; import static com.box.sdkgen.internal.utils.UtilsManager.generateByteStreamFromBuffer; import static com.box.sdkgen.internal.utils.UtilsManager.getUuid; @@ -18,6 +19,7 @@ import com.box.sdkgen.managers.chunkeduploads.CreateFileUploadSessionCommitByUrlRequestBody; import com.box.sdkgen.managers.chunkeduploads.CreateFileUploadSessionCommitHeaders; import com.box.sdkgen.managers.chunkeduploads.CreateFileUploadSessionCommitRequestBody; +import com.box.sdkgen.managers.chunkeduploads.CreateFileUploadSessionForExistingFileRequestBody; import com.box.sdkgen.managers.chunkeduploads.CreateFileUploadSessionRequestBody; import com.box.sdkgen.managers.chunkeduploads.UploadFilePartByUrlHeaders; import com.box.sdkgen.managers.chunkeduploads.UploadFilePartHeaders; @@ -25,8 +27,12 @@ import com.box.sdkgen.schemas.files.Files; import com.box.sdkgen.schemas.uploadedpart.UploadedPart; import com.box.sdkgen.schemas.uploadpart.UploadPart; +import com.box.sdkgen.schemas.uploadpartplan.UploadPartPlan; +import com.box.sdkgen.schemas.uploadpartplanhit.UploadPartPlanHit; import com.box.sdkgen.schemas.uploadparts.UploadParts; import com.box.sdkgen.schemas.uploadsession.UploadSession; +import com.box.sdkgen.schemas.uploadsessionplanrequest.UploadSessionPlanRequest; +import com.box.sdkgen.schemas.uploadsessionplanresponse.UploadSessionPlanResponse; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; @@ -126,6 +132,24 @@ public static TestPartAccumulator reducerByUrl(TestPartAccumulator acc, InputStr .build(); } + public static TestPartPlanAccumulator reducerForUploadSessionPlan( + TestPartPlanAccumulator acc, InputStream chunk) { + int lastIndex = acc.getLastIndex(); + List parts = acc.getParts(); + byte[] chunkBuffer = readByteStream(chunk); + Hash hash = new Hash(HashName.SHA512); + hash.updateHash(chunkBuffer); + String sha512 = hash.digestHash("hex"); + int chunkSize = bufferLength(chunkBuffer); + int bytesStart = lastIndex + 1; + int bytesEnd = lastIndex + chunkSize; + UploadPartPlan part = new UploadPartPlan(bytesStart, chunkSize, sha512); + return new TestPartPlanAccumulator( + bytesEnd, + Stream.concat(parts.stream(), Arrays.asList(part).stream()).collect(Collectors.toList()), + acc.getFileSize()); + } + @Test public void testChunkedManualProcessById() { int fileSize = 20 * 1024 * 1024; @@ -221,6 +245,55 @@ public void testChunkedManualProcessByUrl() { client.getChunkedUploads().deleteFileUploadSessionByUrl(abortUrl); } + @Test + public void testUploadSessionPlan() { + int fileSize = 20 * 1024 * 1024; + String fileName = getUuid(); + String parentFolderId = "0"; + InputStream fileContentStream = generateByteStream(fileSize); + byte[] fileBuffer = readByteStream(fileContentStream); + File uploadedFile = + client + .getChunkedUploads() + .uploadBigFile( + generateByteStreamFromBuffer(fileBuffer), fileName, fileSize, parentFolderId); + delayInSeconds(5); + UploadSession uploadSession = + client + .getChunkedUploads() + .createFileUploadSessionForExistingFile( + uploadedFile.getId(), + new CreateFileUploadSessionForExistingFileRequestBody(fileSize)); + String uploadSessionId = uploadSession.getId(); + String planUrl = uploadSession.getSessionEndpoints().getPlan(); + long partSize = uploadSession.getPartSize(); + int totalParts = uploadSession.getTotalParts(); + Iterator chunksIterator = + iterateChunks(generateByteStreamFromBuffer(fileBuffer), partSize, fileSize); + TestPartPlanAccumulator results = + reduceIterator( + chunksIterator, + (TestPartPlanAccumulator acc, InputStream chunk) -> + reducerForUploadSessionPlan(acc, chunk), + new TestPartPlanAccumulator(-1, Collections.emptyList(), fileSize)); + List parts = results.getParts(); + UploadSessionPlanResponse plan = + client + .getChunkedUploads() + .createFileUploadSessionPlanByUrl(planUrl, new UploadSessionPlanRequest(parts)); + assert plan.getUploadSessionId().equals(uploadSessionId); + assert plan.getHits().size() == totalParts; + assert plan.getMisses().size() == 0; + UploadPartPlan firstPart = parts.get(0); + UploadPartPlanHit firstHit = plan.getHits().get(0); + assert firstHit.getOffset() == firstPart.getOffset(); + assert firstHit.getSize() == firstPart.getSize(); + assert firstHit.getSha512().equals(firstPart.getSha512()); + assert !(firstHit.getPartId().equals("")); + client.getChunkedUploads().deleteFileUploadSessionById(uploadSessionId); + client.getFiles().deleteFileById(uploadedFile.getId()); + } + @Test public void testChunkedUploadConvenienceMethod() { int fileSize = 20 * 1024 * 1024; diff --git a/src/intTest/java/com/box/sdkgen/chunkeduploads/TestPartPlanAccumulator.java b/src/intTest/java/com/box/sdkgen/chunkeduploads/TestPartPlanAccumulator.java new file mode 100644 index 000000000..2a90e8ed7 --- /dev/null +++ b/src/intTest/java/com/box/sdkgen/chunkeduploads/TestPartPlanAccumulator.java @@ -0,0 +1,31 @@ +package com.box.sdkgen.chunkeduploads; + +import com.box.sdkgen.schemas.uploadpartplan.UploadPartPlan; +import java.util.List; + +public class TestPartPlanAccumulator { + + public final int lastIndex; + + public final List parts; + + public final long fileSize; + + public TestPartPlanAccumulator(int lastIndex, List parts, long fileSize) { + this.lastIndex = lastIndex; + this.parts = parts; + this.fileSize = fileSize; + } + + public int getLastIndex() { + return lastIndex; + } + + public List getParts() { + return parts; + } + + public long getFileSize() { + return fileSize; + } +} diff --git a/src/main/java/com/box/sdkgen/internal/utils/Hash.java b/src/main/java/com/box/sdkgen/internal/utils/Hash.java index e8a9664c8..42a933a98 100644 --- a/src/main/java/com/box/sdkgen/internal/utils/Hash.java +++ b/src/main/java/com/box/sdkgen/internal/utils/Hash.java @@ -4,6 +4,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; +import org.apache.commons.codec.binary.Hex; public class Hash { @@ -13,7 +14,7 @@ public class Hash { public Hash(HashName algorithm) { this.algorithm = algorithm; try { - this.digest = MessageDigest.getInstance(algorithm.getValue()); + this.digest = MessageDigest.getInstance(getAlgorithmName(algorithm)); } catch (NoSuchAlgorithmException ae) { throw new BoxSDKError("Digest algorithm not found", ae); } @@ -25,10 +26,24 @@ public void updateHash(byte[] data) { public String digestHash(String encoding) { byte[] digestBytes = digest.digest(); + if ("hex".equals(encoding)) { + return Hex.encodeHexString(digestBytes); + } return Base64.getEncoder().encodeToString(digestBytes); } public HashName getAlgorithm() { return algorithm; } + + private static String getAlgorithmName(HashName algorithm) { + switch (algorithm) { + case SHA1: + return "SHA-1"; + case SHA512: + return "SHA-512"; + default: + throw new BoxSDKError("Digest algorithm not supported"); + } + } } diff --git a/src/main/java/com/box/sdkgen/internal/utils/HashName.java b/src/main/java/com/box/sdkgen/internal/utils/HashName.java index ea96f0003..53bad9873 100644 --- a/src/main/java/com/box/sdkgen/internal/utils/HashName.java +++ b/src/main/java/com/box/sdkgen/internal/utils/HashName.java @@ -3,7 +3,8 @@ import com.box.sdkgen.serialization.json.Valuable; public enum HashName implements Valuable { - SHA1("sha1"); + SHA1("sha1"), + SHA512("sha512"); private final String value; diff --git a/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java b/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java index 7832c8dcd..a19d4f41a 100644 --- a/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java +++ b/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java @@ -15,11 +15,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.MessageDigest; +import java.security.SecureRandom; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -42,6 +42,8 @@ import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; @@ -150,7 +152,7 @@ public static String getUuid() { public static byte[] generateByteBuffer(int size) { byte[] bytes = new byte[size]; - Arrays.fill(bytes, (byte) 0); + new SecureRandom().nextBytes(bytes); return bytes; } @@ -306,7 +308,11 @@ public static double random(double min, double max) { } public static String hexToBase64(String hex) { - return Base64.getEncoder().encodeToString(new BigInteger(hex, 16).toByteArray()); + try { + return Base64.getEncoder().encodeToString(Hex.decodeHex(hex)); + } catch (DecoderException e) { + throw new BoxSDKError("Error decoding hex string.", e); + } } public static Iterator iterateChunks( From 6a4dc2d1b9c7f86058cd0f11c0f8f246051a12f5 Mon Sep 17 00:00:00 2001 From: lsocha Date: Mon, 7 Sep 2026 13:48:44 +0200 Subject: [PATCH 5/5] add missing dependency --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index b87039b72..35601d314 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,7 @@ dependencies { implementation "org.bouncycastle:bcpkix-jdk18on:1.85" implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.github.luben:zstd-jni:1.5.7-2" + implementation "commons-codec:commons-codec:1.22.0" testsCommonImplementation "junit:junit:4.13.2" testsCommonImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0" testsCommonImplementation "org.hamcrest:hamcrest-library:2.2"