2929 */
3030package 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 ;
3235import com .google .api .client .http .HttpMethods ;
3336import com .google .api .core .ApiFuture ;
3437import com .google .api .core .InternalApi ;
3538import com .google .api .core .SettableApiFuture ;
39+ import com .google .api .gax .resumable .ChunkUploadRequest ;
40+ import com .google .api .gax .resumable .ChunkUploadResponse ;
3641import com .google .api .gax .resumable .ResumableUploadClient ;
3742import com .google .api .gax .resumable .ResumableUploadSession ;
3843import 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