Skip to content

Commit 010c0aa

Browse files
committed
feat(gax): implement uploadChunk in HttpJsonResumableUploadClient
1 parent 8d52acf commit 010c0aa

7 files changed

Lines changed: 723 additions & 0 deletions

File tree

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

Lines changed: 167 additions & 0 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,15 @@ 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";
81+
82+
/** HTTP status code 308 (Resume Incomplete in Google Scotty resumable upload protocol). */
83+
private static final int HTTP_STATUS_RESUME_INCOMPLETE = 308;
7284

7385
private static final Map<String, List<String>> START_UPLOAD_HEADERS =
7486
ImmutableMap.of(
@@ -105,6 +117,45 @@ public PathTemplate getPathTemplate() {
105117
.setResponseParser(StringHttpResponseParser.create())
106118
.build();
107119

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

110161
public static HttpJsonResumableUploadClient create(ClientContext clientContext) {
@@ -141,6 +192,45 @@ public ApiFuture<ResumableUploadSession> futureCall(
141192
};
142193
}
143194

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

146236
private final SettableApiFuture<ResumableUploadSession> future;
@@ -205,4 +295,81 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
205295
}
206296
}
207297
}
298+
299+
private static class ChunkUploadResponseListener extends HttpJsonClientCall.Listener<String> {
300+
301+
private final ChunkUploadRequest request;
302+
private final SettableApiFuture<ChunkUploadResponse> future;
303+
private boolean isComplete = false;
304+
private long committedOffset = -1L;
305+
private String responseBody = "";
306+
307+
ChunkUploadResponseListener(
308+
ChunkUploadRequest request, SettableApiFuture<ChunkUploadResponse> future) {
309+
this.request = request;
310+
this.future = future;
311+
}
312+
313+
@Override
314+
public void onHeaders(HttpJsonMetadata responseHeaders) {
315+
Map<String, Object> headers = responseHeaders.getHeaders();
316+
317+
String statusStr = HttpHeadersUtils.getFirstHeader(headers, UPLOAD_STATUS_HEADER);
318+
if (STATUS_FINAL.equalsIgnoreCase(statusStr)) {
319+
this.isComplete = true;
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+
}
329+
}
330+
}
331+
332+
@Override
333+
public void onMessage(@Nullable String message) {
334+
if (message != null) {
335+
this.responseBody = message;
336+
}
337+
}
338+
339+
@Override
340+
public void onClose(int statusCode, HttpJsonMetadata trailers) {
341+
if ((statusCode >= 200 && statusCode < 300)
342+
|| statusCode == HTTP_STATUS_RESUME_INCOMPLETE) {
343+
if (statusCode == HTTP_STATUS_RESUME_INCOMPLETE && committedOffset < 0) {
344+
future.setException(
345+
ApiExceptionFactory.createException(
346+
"Server returned 308 Resume Incomplete but the "
347+
+ UPLOAD_SIZE_RECEIVED_HEADER
348+
+ " header was missing or invalid",
349+
/* cause= */ null,
350+
HttpJsonStatusCode.of(statusCode),
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+
Throwable cause = trailers.getException();
363+
ApiException apiException =
364+
cause != null
365+
? API_EXCEPTION_FACTORY.create(cause)
366+
: ApiExceptionFactory.createException(
367+
"Failed to upload chunk with status code: " + statusCode,
368+
/* cause= */ null,
369+
HttpJsonStatusCode.of(statusCode),
370+
/* retryable= */ false);
371+
future.setException(apiException);
372+
}
373+
}
374+
}
208375
}

0 commit comments

Comments
 (0)