3333import com .google .api .core .ApiFuture ;
3434import com .google .api .core .InternalApi ;
3535import com .google .api .core .SettableApiFuture ;
36+ import com .google .api .gax .resumable .ChunkUploadRequest ;
37+ import com .google .api .gax .resumable .ChunkUploadResponse ;
3638import com .google .api .gax .resumable .ResumableUploadClient ;
3739import com .google .api .gax .resumable .ResumableUploadSession ;
3840import com .google .api .gax .resumable .StartUploadRequest ;
@@ -67,14 +69,20 @@ public final class HttpJsonResumableUploadClient implements ResumableUploadClien
6769
6870 private static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol" ;
6971 private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command" ;
72+ private static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset" ;
7073 private static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL" ;
7174 private static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity" ;
75+ private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status" ;
76+ private static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received" ;
77+ private static final String STATUS_FINAL = "final" ;
7278
7379 private static final Map <String , List <String >> START_UPLOAD_HEADERS =
7480 ImmutableMap .of (
7581 UPLOAD_PROTOCOL_HEADER , ImmutableList .of ("resumable" ),
7682 UPLOAD_COMMAND_HEADER , ImmutableList .of ("start" ));
7783
84+ private static final PathTemplate PATH_TEMPLATE = PathTemplate .create ("{+path}" );
85+
7886 private static final ApiMethodDescriptor <StartUploadRequest , String > START_UPLOAD_DESCRIPTOR =
7987 ApiMethodDescriptor .<StartUploadRequest , String >newBuilder ()
8088 .setFullMethodName ("ResumableUpload/StartUpload" )
@@ -99,7 +107,37 @@ public String getPath(StartUploadRequest request) {
99107
100108 @ Override
101109 public PathTemplate getPathTemplate () {
102- return PathTemplate .create ("{+path}" );
110+ return PATH_TEMPLATE ;
111+ }
112+ })
113+ .setResponseParser (StringHttpResponseParser .create ())
114+ .build ();
115+
116+ private static final ApiMethodDescriptor <ChunkUploadRequest , String > UPLOAD_CHUNK_DESCRIPTOR =
117+ ApiMethodDescriptor .<ChunkUploadRequest , String >newBuilder ()
118+ .setFullMethodName ("ResumableUpload/UploadChunk" )
119+ .setHttpMethod (HttpMethods .POST )
120+ .setType (ApiMethodDescriptor .MethodType .UNARY )
121+ .setRequestFormatter (
122+ new ResumableUploadChunkRequestFormatter <ChunkUploadRequest >() {
123+ @ Override
124+ public Map <String , List <String >> getQueryParamNames (ChunkUploadRequest request ) {
125+ return Collections .emptyMap ();
126+ }
127+
128+ @ Override
129+ public byte [] getBinaryRequestBody (ChunkUploadRequest request ) {
130+ return request .getPayload ().toByteArray ();
131+ }
132+
133+ @ Override
134+ public String getPath (ChunkUploadRequest request ) {
135+ return request .getUploadUrl ();
136+ }
137+
138+ @ Override
139+ public PathTemplate getPathTemplate () {
140+ return PATH_TEMPLATE ;
103141 }
104142 })
105143 .setResponseParser (StringHttpResponseParser .create ())
@@ -141,6 +179,46 @@ public ApiFuture<ResumableUploadSession> futureCall(
141179 };
142180 }
143181
182+ @ Override
183+ public UnaryCallable <ChunkUploadRequest , ChunkUploadResponse > uploadChunkCallable () {
184+ return new UnaryCallable <ChunkUploadRequest , ChunkUploadResponse >() {
185+ @ Override
186+ public ApiFuture <ChunkUploadResponse > futureCall (
187+ ChunkUploadRequest request , @ Nullable ApiCallContext inputContext ) {
188+ Preconditions .checkNotNull (request );
189+ boolean isPayloadEmpty = request .getPayload ().isEmpty ();
190+ String command ;
191+ if (request .isFinal ()) {
192+ command = !isPayloadEmpty ? "upload, finalize" : "finalize" ;
193+ } else {
194+ command = "upload" ;
195+ }
196+ Map <String , List <String >> chunkHeaders =
197+ ImmutableMap .of (
198+ UPLOAD_COMMAND_HEADER ,
199+ ImmutableList .of (command ),
200+ UPLOAD_OFFSET_HEADER ,
201+ ImmutableList .of (String .valueOf (request .getOffset ())));
202+
203+ HttpJsonCallContext context =
204+ (HttpJsonCallContext )
205+ HttpJsonCallContext .createDefault ()
206+ .nullToSelf (clientContext .getDefaultCallContext ())
207+ .merge (inputContext )
208+ .withExtraHeaders (chunkHeaders );
209+
210+ HttpJsonClientCall <ChunkUploadRequest , String > clientCall =
211+ HttpJsonClientCalls .newCall (UPLOAD_CHUNK_DESCRIPTOR , context );
212+
213+ SettableApiFuture <ChunkUploadResponse > future = SettableApiFuture .create ();
214+ HttpJsonClientCalls .startUnaryCall (
215+ clientCall , request , context , new ChunkUploadResponseListener (request , future ));
216+
217+ return future ;
218+ }
219+ };
220+ }
221+
144222 private static class StartUploadResponseListener extends HttpJsonClientCall .Listener <String > {
145223
146224 private final SettableApiFuture <ResumableUploadSession > future ;
@@ -194,25 +272,124 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) {
194272 /* retryable= */ false ));
195273 }
196274 } else {
197- Throwable cause = trailers .getException ();
198- ApiException apiException =
199- cause != null
200- ? API_EXCEPTION_FACTORY .create (cause )
201- : ApiExceptionFactory .createException (
202- "Failed to start upload with status code: " + statusCode ,
203- /* cause= */ null ,
204- HttpJsonStatusCode .of (statusCode ),
205- /* retryable= */ false );
206- future .setException (apiException );
275+ future .setException (createApiException (statusCode , trailers , "Failed to start upload" ));
207276 }
208- } catch (Throwable t ) {
277+ } catch (Exception e ) {
209278 future .setException (
210279 ApiExceptionFactory .createException (
211280 "Internal error processing start upload response" ,
212- t ,
281+ e ,
213282 HttpJsonStatusCode .of (StatusCode .Code .INTERNAL ),
214283 /* retryable= */ false ));
215284 }
216285 }
217286 }
287+
288+ private static class ChunkUploadResponseListener extends HttpJsonClientCall .Listener <String > {
289+
290+ private final ChunkUploadRequest request ;
291+ private final SettableApiFuture <ChunkUploadResponse > future ;
292+ private boolean hasUploadStatusHeader = false ;
293+ private boolean isComplete = false ;
294+ private long committedOffset = -1L ;
295+ private String responseBody = "" ;
296+
297+ ChunkUploadResponseListener (
298+ ChunkUploadRequest request , SettableApiFuture <ChunkUploadResponse > future ) {
299+ this .request = request ;
300+ this .future = future ;
301+ }
302+
303+ @ Override
304+ public void onHeaders (HttpJsonMetadata responseHeaders ) {
305+ Map <String , Object > headers = responseHeaders .getHeaders ();
306+
307+ String statusStr = HttpHeadersUtils .getSingleHeader (headers , UPLOAD_STATUS_HEADER );
308+ if (statusStr != null ) {
309+ this .hasUploadStatusHeader = true ;
310+ if (STATUS_FINAL .equalsIgnoreCase (statusStr )) {
311+ this .isComplete = true ;
312+ }
313+ }
314+
315+ String sizeReceivedStr =
316+ HttpHeadersUtils .getSingleHeader (headers , UPLOAD_SIZE_RECEIVED_HEADER );
317+ if (!Strings .isNullOrEmpty (sizeReceivedStr )) {
318+ try {
319+ this .committedOffset = Long .parseLong (sizeReceivedStr );
320+ } catch (NumberFormatException ignored ) {
321+ // Ignore invalid/malformed size received header and fall back to local offset
322+ // calculation.
323+ }
324+ }
325+ }
326+
327+ @ Override
328+ public void onMessage (@ Nullable String message ) {
329+ if (message != null ) {
330+ this .responseBody = message ;
331+ }
332+ }
333+
334+ @ Override
335+ public void onClose (int statusCode , HttpJsonMetadata trailers ) {
336+ try {
337+ if (statusCode >= 200 && statusCode < 300 ) {
338+ if (!hasUploadStatusHeader ) {
339+ future .setException (
340+ ApiExceptionFactory .createException (
341+ "Upload chunk response did not contain valid X-Goog-Upload-Status header" ,
342+ /* cause= */ null ,
343+ HttpJsonStatusCode .of (StatusCode .Code .INTERNAL ),
344+ /* retryable= */ false ));
345+ return ;
346+ }
347+ long confirmedOffset =
348+ committedOffset >= 0
349+ ? committedOffset
350+ : request .getOffset () + request .getPayload ().size ();
351+ future .set (
352+ ChunkUploadResponse .create (
353+ confirmedOffset , isComplete , isComplete ? responseBody : "" ));
354+ } else {
355+ future .setException (createApiException (statusCode , trailers , "Failed to upload chunk" ));
356+ }
357+ } catch (Exception e ) {
358+ future .setException (
359+ ApiExceptionFactory .createException (
360+ "Internal error processing upload chunk response" ,
361+ e ,
362+ HttpJsonStatusCode .of (StatusCode .Code .INTERNAL ),
363+ /* retryable= */ false ));
364+ }
365+ }
366+ }
367+
368+ private static boolean isUploadFinal (HttpJsonMetadata responseHeaders ) {
369+ String statusStr =
370+ HttpHeadersUtils .getSingleHeader (responseHeaders .getHeaders (), UPLOAD_STATUS_HEADER );
371+ return STATUS_FINAL .equalsIgnoreCase (statusStr );
372+ }
373+
374+ private static ApiException createApiException (
375+ int statusCode , @ Nullable HttpJsonMetadata trailers , String actionDescription ) {
376+ Throwable cause = trailers != null ? trailers .getException () : null ;
377+ if (cause != null ) {
378+ ApiException apiException = API_EXCEPTION_FACTORY .create (cause );
379+ if (isUploadFinal (trailers )) {
380+ return ApiExceptionFactory .createException (
381+ apiException .getMessage (),
382+ apiException .getCause (),
383+ apiException .getStatusCode (),
384+ /* retryable= */ false ,
385+ apiException .getErrorDetails ());
386+ }
387+ return apiException ;
388+ }
389+ return ApiExceptionFactory .createException (
390+ actionDescription + " with status code: " + statusCode ,
391+ /* cause= */ null ,
392+ HttpJsonStatusCode .of (statusCode ),
393+ /* retryable= */ false );
394+ }
218395}
0 commit comments