Skip to content

Commit 2c18ffc

Browse files
committed
feat(gax): implement uploadChunk in HttpJsonResumableUploadClient
1 parent a139b72 commit 2c18ffc

6 files changed

Lines changed: 727 additions & 13 deletions

File tree

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

Lines changed: 197 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import com.google.api.core.ApiFuture;
3434
import com.google.api.core.InternalApi;
3535
import com.google.api.core.SettableApiFuture;
36+
import com.google.api.gax.resumable.ChunkUploadRequest;
37+
import com.google.api.gax.resumable.ChunkUploadResponse;
3638
import com.google.api.gax.resumable.ResumableUploadClient;
3739
import com.google.api.gax.resumable.ResumableUploadSession;
3840
import com.google.api.gax.resumable.StartUploadRequest;
@@ -47,6 +49,8 @@
4749
import com.google.common.base.Strings;
4850
import com.google.common.collect.ImmutableList;
4951
import com.google.common.collect.ImmutableMap;
52+
import com.google.common.io.ByteSource;
53+
import java.io.IOException;
5054
import java.util.Collections;
5155
import java.util.List;
5256
import java.util.Map;
@@ -67,14 +71,20 @@ public final class HttpJsonResumableUploadClient implements ResumableUploadClien
6771

6872
private static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol";
6973
private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
74+
private static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset";
7075
private static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL";
7176
private static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity";
77+
private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";
78+
private static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received";
79+
private static final String STATUS_FINAL = "final";
7280

7381
private static final Map<String, List<String>> START_UPLOAD_HEADERS =
7482
ImmutableMap.of(
7583
UPLOAD_PROTOCOL_HEADER, ImmutableList.of("resumable"),
7684
UPLOAD_COMMAND_HEADER, ImmutableList.of("start"));
7785

86+
private static final PathTemplate PATH_TEMPLATE = PathTemplate.create("{+path}");
87+
7888
private static final ApiMethodDescriptor<StartUploadRequest, String> START_UPLOAD_DESCRIPTOR =
7989
ApiMethodDescriptor.<StartUploadRequest, String>newBuilder()
8090
.setFullMethodName("ResumableUpload/StartUpload")
@@ -99,7 +109,37 @@ public String getPath(StartUploadRequest request) {
99109

100110
@Override
101111
public PathTemplate getPathTemplate() {
102-
return PathTemplate.create("{+path}");
112+
return PATH_TEMPLATE;
113+
}
114+
})
115+
.setResponseParser(StringHttpResponseParser.create())
116+
.build();
117+
118+
private static final ApiMethodDescriptor<ChunkUploadRequest, String> UPLOAD_CHUNK_DESCRIPTOR =
119+
ApiMethodDescriptor.<ChunkUploadRequest, String>newBuilder()
120+
.setFullMethodName("ResumableUpload/UploadChunk")
121+
.setHttpMethod(HttpMethods.POST)
122+
.setType(ApiMethodDescriptor.MethodType.UNARY)
123+
.setRequestFormatter(
124+
new ResumableUploadChunkRequestFormatter<ChunkUploadRequest>() {
125+
@Override
126+
public Map<String, List<String>> getQueryParamNames(ChunkUploadRequest request) {
127+
return Collections.emptyMap();
128+
}
129+
130+
@Override
131+
public ByteSource getBinaryRequestBody(ChunkUploadRequest request) {
132+
return request.getPayload();
133+
}
134+
135+
@Override
136+
public String getPath(ChunkUploadRequest request) {
137+
return request.getUploadUrl();
138+
}
139+
140+
@Override
141+
public PathTemplate getPathTemplate() {
142+
return PATH_TEMPLATE;
103143
}
104144
})
105145
.setResponseParser(StringHttpResponseParser.create())
@@ -141,6 +181,51 @@ public ApiFuture<ResumableUploadSession> futureCall(
141181
};
142182
}
143183

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

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

0 commit comments

Comments
 (0)