Skip to content

Commit e6fd648

Browse files
committed
feat(gax): implement uploadChunk in HttpJsonResumableUploadClient
1 parent 3039c34 commit e6fd648

5 files changed

Lines changed: 608 additions & 11 deletions

File tree

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

Lines changed: 199 additions & 11 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,14 +72,20 @@ 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(
7584
UPLOAD_PROTOCOL_HEADER, ImmutableList.of("resumable"),
7685
UPLOAD_COMMAND_HEADER, ImmutableList.of("start"));
7786

87+
private static final PathTemplate PATH_TEMPLATE = PathTemplate.create("{+path}");
88+
7889
private static final ApiMethodDescriptor<StartUploadRequest, String> START_UPLOAD_DESCRIPTOR =
7990
ApiMethodDescriptor.<StartUploadRequest, String>newBuilder()
8091
.setFullMethodName("ResumableUpload/StartUpload")
@@ -99,7 +110,46 @@ public String getPath(StartUploadRequest request) {
99110

100111
@Override
101112
public PathTemplate getPathTemplate() {
102-
return PathTemplate.create("{+path}");
113+
return PATH_TEMPLATE;
114+
}
115+
})
116+
.setResponseParser(StringHttpResponseParser.create())
117+
.build();
118+
119+
private static final ApiMethodDescriptor<ChunkUploadRequest, String> UPLOAD_CHUNK_DESCRIPTOR =
120+
ApiMethodDescriptor.<ChunkUploadRequest, String>newBuilder()
121+
.setFullMethodName("ResumableUpload/UploadChunk")
122+
.setHttpMethod(HttpMethods.POST)
123+
.setType(ApiMethodDescriptor.MethodType.UNARY)
124+
.setRequestFormatter(
125+
new HttpRequestFormatter<ChunkUploadRequest>() {
126+
@Override
127+
public Map<String, List<String>> getQueryParamNames(ChunkUploadRequest request) {
128+
return Collections.emptyMap();
129+
}
130+
131+
@Override
132+
public String getRequestBody(ChunkUploadRequest request) {
133+
return "";
134+
}
135+
136+
@Override
137+
public HttpContent getHttpContent(ChunkUploadRequest request) {
138+
if (!request.getPayload().isEmpty()) {
139+
return new ByteArrayContent(
140+
"application/octet-stream", request.getPayload().toByteArray());
141+
}
142+
return new EmptyContent();
143+
}
144+
145+
@Override
146+
public String getPath(ChunkUploadRequest request) {
147+
return request.getUploadUrl();
148+
}
149+
150+
@Override
151+
public PathTemplate getPathTemplate() {
152+
return PATH_TEMPLATE;
103153
}
104154
})
105155
.setResponseParser(StringHttpResponseParser.create())
@@ -141,6 +191,45 @@ public ApiFuture<ResumableUploadSession> futureCall(
141191
};
142192
}
143193

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

146235
private final SettableApiFuture<ResumableUploadSession> future;
@@ -190,16 +279,7 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
190279
/* retryable= */ false));
191280
}
192281
} 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);
282+
future.setException(createApiException(statusCode, trailers, "Failed to start upload"));
203283
}
204284
} catch (Throwable t) {
205285
future.setException(
@@ -211,4 +291,112 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
211291
}
212292
}
213293
}
294+
295+
private static class ChunkUploadResponseListener extends HttpJsonClientCall.Listener<String> {
296+
297+
private final ChunkUploadRequest request;
298+
private final SettableApiFuture<ChunkUploadResponse> future;
299+
private boolean hasUploadStatusHeader = false;
300+
private boolean isComplete = false;
301+
private long committedOffset = -1L;
302+
private String responseBody = "";
303+
304+
ChunkUploadResponseListener(
305+
ChunkUploadRequest request, SettableApiFuture<ChunkUploadResponse> future) {
306+
this.request = request;
307+
this.future = future;
308+
}
309+
310+
@Override
311+
public void onHeaders(HttpJsonMetadata responseHeaders) {
312+
Map<String, Object> headers = responseHeaders.getHeaders();
313+
314+
String statusStr = HttpHeadersUtils.getFirstHeader(headers, UPLOAD_STATUS_HEADER);
315+
if (statusStr != null) {
316+
this.hasUploadStatusHeader = true;
317+
if (STATUS_FINAL.equalsIgnoreCase(statusStr)) {
318+
this.isComplete = true;
319+
}
320+
}
321+
322+
String sizeReceivedStr =
323+
HttpHeadersUtils.getFirstHeader(headers, UPLOAD_SIZE_RECEIVED_HEADER);
324+
if (!Strings.isNullOrEmpty(sizeReceivedStr)) {
325+
try {
326+
this.committedOffset = Long.parseLong(sizeReceivedStr);
327+
} catch (NumberFormatException ignored) {
328+
// Ignore invalid/malformed size received header and fall back to local offset
329+
// calculation.
330+
}
331+
}
332+
}
333+
334+
@Override
335+
public void onMessage(@Nullable String message) {
336+
if (message != null) {
337+
this.responseBody = message;
338+
}
339+
}
340+
341+
@Override
342+
public void onClose(int statusCode, HttpJsonMetadata trailers) {
343+
try {
344+
if (statusCode >= 200 && statusCode < 300) {
345+
if (!hasUploadStatusHeader) {
346+
future.setException(
347+
ApiExceptionFactory.createException(
348+
"Upload chunk response did not contain valid X-Goog-Upload-Status header",
349+
/* cause= */ null,
350+
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
351+
/* retryable= */ false));
352+
return;
353+
}
354+
long confirmedOffset =
355+
committedOffset >= 0
356+
? committedOffset
357+
: request.getOffset() + request.getPayload().size();
358+
future.set(
359+
ChunkUploadResponse.create(
360+
confirmedOffset, isComplete, isComplete ? responseBody : ""));
361+
} else {
362+
future.setException(createApiException(statusCode, trailers, "Failed to upload chunk"));
363+
}
364+
} catch (Throwable t) {
365+
future.setException(
366+
ApiExceptionFactory.createException(
367+
"Internal error processing upload chunk response",
368+
t,
369+
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
370+
/* retryable= */ false));
371+
}
372+
}
373+
}
374+
375+
private static boolean isUploadFinal(HttpJsonMetadata responseHeaders) {
376+
String statusStr =
377+
HttpHeadersUtils.getFirstHeader(responseHeaders.getHeaders(), UPLOAD_STATUS_HEADER);
378+
return STATUS_FINAL.equalsIgnoreCase(statusStr);
379+
}
380+
381+
private static ApiException createApiException(
382+
int statusCode, @Nullable HttpJsonMetadata trailers, String actionDescription) {
383+
Throwable cause = trailers != null ? trailers.getException() : null;
384+
if (cause != null) {
385+
ApiException apiException = API_EXCEPTION_FACTORY.create(cause);
386+
if (trailers != null && isUploadFinal(trailers)) {
387+
return ApiExceptionFactory.createException(
388+
apiException.getMessage(),
389+
apiException.getCause(),
390+
apiException.getStatusCode(),
391+
/* retryable= */ false,
392+
apiException.getErrorDetails());
393+
}
394+
return apiException;
395+
}
396+
return ApiExceptionFactory.createException(
397+
actionDescription + " with status code: " + statusCode,
398+
/* cause= */ null,
399+
HttpJsonStatusCode.of(statusCode),
400+
/* retryable= */ false);
401+
}
214402
}

0 commit comments

Comments
 (0)