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 ;
4749import com .google .common .base .Strings ;
4850import com .google .common .collect .ImmutableList ;
4951import com .google .common .collect .ImmutableMap ;
52+ import com .google .common .io .ByteSource ;
53+ import java .io .IOException ;
5054import java .util .Collections ;
5155import java .util .List ;
5256import 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