Skip to content

Commit d1a4963

Browse files
committed
feat(gax): implement uploadChunk in HttpJsonResumableUploadClient
1 parent 798876d commit d1a4963

5 files changed

Lines changed: 535 additions & 10 deletions

File tree

sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClient.java

Lines changed: 168 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@
2929
*/
3030
package com.google.api.gax.httpjson;
3131

32+
import com.google.api.client.http.ByteArrayContent;
33+
import com.google.api.client.http.EmptyContent;
34+
import com.google.api.client.http.HttpContent;
3235
import com.google.api.client.http.HttpMethods;
3336
import com.google.api.core.ApiFuture;
3437
import com.google.api.core.InternalApi;
3538
import com.google.api.core.SettableApiFuture;
39+
import com.google.api.gax.resumable.ChunkUploadRequest;
40+
import com.google.api.gax.resumable.ChunkUploadResponse;
3641
import com.google.api.gax.resumable.ResumableUploadClient;
3742
import com.google.api.gax.resumable.ResumableUploadSession;
3843
import com.google.api.gax.resumable.StartUploadRequest;
@@ -67,8 +72,12 @@ public final class HttpJsonResumableUploadClient implements ResumableUploadClien
6772

6873
private static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol";
6974
private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
75+
private static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset";
7076
private static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL";
7177
private static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity";
78+
private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";
79+
private static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received";
80+
private static final String STATUS_FINAL = "final";
7281

7382
private static final Map<String, List<String>> START_UPLOAD_HEADERS =
7483
ImmutableMap.of(
@@ -105,6 +114,45 @@ public PathTemplate getPathTemplate() {
105114
.setResponseParser(StringHttpResponseParser.create())
106115
.build();
107116

117+
private static final ApiMethodDescriptor<ChunkUploadRequest, String> UPLOAD_CHUNK_DESCRIPTOR =
118+
ApiMethodDescriptor.<ChunkUploadRequest, String>newBuilder()
119+
.setFullMethodName("ResumableUpload/UploadChunk")
120+
.setHttpMethod(HttpMethods.POST)
121+
.setType(ApiMethodDescriptor.MethodType.UNARY)
122+
.setRequestFormatter(
123+
new HttpRequestFormatter<ChunkUploadRequest>() {
124+
@Override
125+
public Map<String, List<String>> getQueryParamNames(ChunkUploadRequest request) {
126+
return Collections.emptyMap();
127+
}
128+
129+
@Override
130+
public String getRequestBody(ChunkUploadRequest request) {
131+
return "";
132+
}
133+
134+
@Override
135+
public HttpContent getHttpContent(ChunkUploadRequest request) {
136+
if (!request.getPayload().isEmpty()) {
137+
return new ByteArrayContent(
138+
"application/octet-stream", request.getPayload().toByteArray());
139+
}
140+
return new EmptyContent();
141+
}
142+
143+
@Override
144+
public String getPath(ChunkUploadRequest request) {
145+
return request.getUploadUrl();
146+
}
147+
148+
@Override
149+
public PathTemplate getPathTemplate() {
150+
return PathTemplate.create("{+path}");
151+
}
152+
})
153+
.setResponseParser(StringHttpResponseParser.create())
154+
.build();
155+
108156
private final ClientContext clientContext;
109157

110158
public static HttpJsonResumableUploadClient create(ClientContext clientContext) {
@@ -141,6 +189,45 @@ public ApiFuture<ResumableUploadSession> futureCall(
141189
};
142190
}
143191

192+
@Override
193+
public UnaryCallable<ChunkUploadRequest, ChunkUploadResponse> uploadChunkCallable() {
194+
return new UnaryCallable<ChunkUploadRequest, ChunkUploadResponse>() {
195+
@Override
196+
public ApiFuture<ChunkUploadResponse> futureCall(
197+
ChunkUploadRequest request, @Nullable ApiCallContext inputContext) {
198+
Preconditions.checkNotNull(request);
199+
String command;
200+
if (request.isFinal()) {
201+
command = !request.getPayload().isEmpty() ? "upload, finalize" : "finalize";
202+
} else {
203+
command = "upload";
204+
}
205+
Map<String, List<String>> chunkHeaders =
206+
ImmutableMap.of(
207+
UPLOAD_COMMAND_HEADER,
208+
ImmutableList.of(command),
209+
UPLOAD_OFFSET_HEADER,
210+
ImmutableList.of(String.valueOf(request.getOffset())));
211+
212+
HttpJsonCallContext context =
213+
(HttpJsonCallContext)
214+
HttpJsonCallContext.createDefault()
215+
.nullToSelf(clientContext.getDefaultCallContext())
216+
.merge(inputContext)
217+
.withExtraHeaders(chunkHeaders);
218+
219+
HttpJsonClientCall<ChunkUploadRequest, String> clientCall =
220+
HttpJsonClientCalls.newCall(UPLOAD_CHUNK_DESCRIPTOR, context);
221+
222+
SettableApiFuture<ChunkUploadResponse> future = SettableApiFuture.create();
223+
HttpJsonClientCalls.startUnaryCall(
224+
clientCall, request, context, new ChunkUploadResponseListener(request, future));
225+
226+
return future;
227+
}
228+
};
229+
}
230+
144231
private static class StartUploadResponseListener extends HttpJsonClientCall.Listener<String> {
145232

146233
private final SettableApiFuture<ResumableUploadSession> future;
@@ -190,16 +277,7 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
190277
/* retryable= */ false));
191278
}
192279
} else {
193-
Throwable cause = trailers != null ? trailers.getException() : null;
194-
ApiException apiException =
195-
cause != null
196-
? API_EXCEPTION_FACTORY.create(cause)
197-
: ApiExceptionFactory.createException(
198-
"Failed to start upload with status code: " + statusCode,
199-
/* cause= */ null,
200-
HttpJsonStatusCode.of(statusCode),
201-
/* retryable= */ false);
202-
future.setException(apiException);
280+
future.setException(createApiException(statusCode, trailers, "Failed to start upload"));
203281
}
204282
} catch (Throwable t) {
205283
future.setException(
@@ -211,4 +289,84 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
211289
}
212290
}
213291
}
292+
293+
private static class ChunkUploadResponseListener extends HttpJsonClientCall.Listener<String> {
294+
295+
private final ChunkUploadRequest request;
296+
private final SettableApiFuture<ChunkUploadResponse> future;
297+
private boolean isComplete = false;
298+
private long committedOffset = -1L;
299+
private String responseBody = "";
300+
301+
ChunkUploadResponseListener(
302+
ChunkUploadRequest request, SettableApiFuture<ChunkUploadResponse> future) {
303+
this.request = request;
304+
this.future = future;
305+
}
306+
307+
@Override
308+
public void onHeaders(HttpJsonMetadata responseHeaders) {
309+
Map<String, Object> headers = responseHeaders.getHeaders();
310+
311+
String statusStr = HttpHeadersUtils.getFirstHeader(headers, UPLOAD_STATUS_HEADER);
312+
if (STATUS_FINAL.equalsIgnoreCase(statusStr)) {
313+
this.isComplete = true;
314+
}
315+
316+
String sizeReceivedStr =
317+
HttpHeadersUtils.getFirstHeader(headers, UPLOAD_SIZE_RECEIVED_HEADER);
318+
if (!Strings.isNullOrEmpty(sizeReceivedStr)) {
319+
try {
320+
this.committedOffset = Long.parseLong(sizeReceivedStr);
321+
} catch (NumberFormatException ignored) {
322+
// Ignore invalid/malformed size received header and fall back to local offset
323+
// calculation.
324+
}
325+
}
326+
}
327+
328+
@Override
329+
public void onMessage(@Nullable String message) {
330+
if (message != null) {
331+
this.responseBody = message;
332+
}
333+
}
334+
335+
@Override
336+
public void onClose(int statusCode, HttpJsonMetadata trailers) {
337+
try {
338+
if (statusCode >= 200 && statusCode < 300) {
339+
long confirmedOffset =
340+
committedOffset >= 0
341+
? committedOffset
342+
: request.getOffset() + request.getPayload().size();
343+
future.set(
344+
ChunkUploadResponse.create(
345+
confirmedOffset, isComplete, isComplete ? responseBody : ""));
346+
} else {
347+
future.setException(createApiException(statusCode, trailers, "Failed to upload chunk"));
348+
}
349+
} catch (Throwable t) {
350+
future.setException(
351+
ApiExceptionFactory.createException(
352+
"Internal error processing upload chunk response",
353+
t,
354+
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
355+
/* retryable= */ false));
356+
}
357+
}
358+
}
359+
360+
private static ApiException createApiException(
361+
int statusCode, @Nullable HttpJsonMetadata trailers, String actionDescription) {
362+
Throwable cause = trailers != null ? trailers.getException() : null;
363+
if (cause != null) {
364+
return API_EXCEPTION_FACTORY.create(cause);
365+
}
366+
return ApiExceptionFactory.createException(
367+
actionDescription + " with status code: " + statusCode,
368+
/* cause= */ null,
369+
HttpJsonStatusCode.of(statusCode),
370+
/* retryable= */ false);
371+
}
214372
}

0 commit comments

Comments
 (0)