@@ -49,6 +49,14 @@ class StorageUploadCommand < ApiCommand
4949 # @return [Integer]
5050 attr_accessor :upload_chunk_size
5151
52+ # Unique upload_id of a resumable upload
53+ # @return [String]
54+ attr_accessor :upload_id
55+
56+ # Boolean Value to specify is a resumable upload is to be deleted or not
57+ # @return [Boolean]
58+ attr_accessor :delete_upload
59+
5260 # Ensure the content is readable and wrapped in an IO instance.
5361 #
5462 # @return [void]
@@ -61,7 +69,6 @@ def prepare!
6169 # asserting that it already has a body. Form encoding is never used
6270 # by upload requests.
6371 self . body = '' unless self . body
64-
6572 super
6673 if streamable? ( upload_source )
6774 self . upload_io = upload_source
@@ -73,14 +80,16 @@ def prepare!
7380 self . upload_content_type = type &.content_type
7481 end
7582 @close_io_on_finish = true
83+ elsif !upload_id . nil? && delete_upload
84+ @close_io_on_finish = false
7685 else
7786 fail Google ::Apis ::ClientError , 'Invalid upload source'
7887 end
7988 end
8089
8190 # Close IO stream when command done. Only closes the stream if it was opened by the command.
8291 def release!
83- upload_io . close if @close_io_on_finish
92+ upload_io . close if @close_io_on_finish && ! upload_io . nil?
8493 end
8594
8695 # Execute the command, retrying as necessary
@@ -96,8 +105,16 @@ def execute(client)
96105 prepare!
97106 opencensus_begin_span
98107 @upload_chunk_size = options . upload_chunk_size
108+ if upload_id . nil?
109+ res = do_retry :initiate_resumable_upload , client
110+ elsif delete_upload && !upload_id . nil?
111+ construct_resumable_upload_url upload_id
112+ res = do_retry :cancel_resumable_upload , client
113+ else
114+ construct_resumable_upload_url upload_id
115+ res = do_retry :reinitiate_resumable_upload , client
116+ end
99117
100- do_retry :initiate_resumable_upload , client
101118 while @upload_incomplete
102119 res = do_retry :send_upload_command , client
103120 end
@@ -131,6 +148,22 @@ def initiate_resumable_upload(client)
131148 error ( e , rethrow : true )
132149 end
133150
151+ # Reinitiating resumable upload
152+ def reinitiate_resumable_upload ( client )
153+ logger . debug { sprintf ( 'Restarting resumable upload command to %s' , url ) }
154+ check_resumable_upload client
155+ upload_io . pos = @offset
156+ end
157+
158+ # Making resumable upload url from upload_id
159+ def construct_resumable_upload_url ( upload_id )
160+ query_params = query . dup
161+ query_params [ 'uploadType' ] = RESUMABLE
162+ query_params [ 'upload_id' ] = upload_id
163+ resumable_upload_params = query_params . map { |key , value | "#{ key } =#{ value } " } . join ( '&' )
164+ @upload_url = "#{ url } &#{ resumable_upload_params } "
165+ end
166+
134167 # Send the actual content
135168 #
136169 # @param [HTTPClient] client
@@ -160,6 +193,9 @@ def send_upload_command(client)
160193 @offset += current_chunk_size if @upload_incomplete
161194 success ( result )
162195 rescue => e
196+ logger . warn {
197+ "error occured please use uploadId-#{ response . headers [ 'X-GUploader-UploadID' ] } to resume your upload"
198+ } unless response . nil?
163199 upload_io . pos = @offset
164200 error ( e , rethrow : true )
165201 end
@@ -182,6 +218,59 @@ def process_response(status, header, body)
182218 super ( status , header , body )
183219 end
184220
221+ def check_resumable_upload ( client )
222+ # Setting up request header
223+ request_header = header . dup
224+ request_header [ CONTENT_RANGE_HEADER ] = "bytes */#{ upload_io . size } "
225+ request_header [ CONTENT_LENGTH_HEADER ] = '0'
226+ # Initiating call
227+ response = client . put ( @upload_url , header : request_header , follow_redirect : true )
228+ handle_resumable_upload_http_response_codes ( response )
229+ end
230+
231+ # Cancel resumable upload
232+ def cancel_resumable_upload ( client )
233+ # Setting up request header
234+ request_header = header . dup
235+ request_header [ CONTENT_LENGTH_HEADER ] = '0'
236+ # Initiating call
237+ response = client . delete ( @upload_url , header : request_header , follow_redirect : true )
238+ handle_resumable_upload_http_response_codes ( response )
239+
240+ if !@upload_incomplete && ( 400 ..499 ) . include? ( response . code . to_i )
241+ @close_io_on_finish = true
242+ true # method returns true if upload is successfully cancelled
243+ else
244+ logger . debug { sprintf ( "Failed to cancel upload session. Response: #{ response . code } - #{ response . body } " ) }
245+ end
246+
247+ end
248+
249+ def handle_resumable_upload_http_response_codes ( response )
250+ code = response . code . to_i
251+
252+ case code
253+ when 308
254+ if response . headers [ 'Range' ]
255+ range = response . headers [ 'Range' ]
256+ @offset = range . split ( '-' ) . last . to_i + 1
257+ logger . debug { sprintf ( "Upload is incomplete. Bytes uploaded so far: #{ range } " ) }
258+ else
259+ logger . debug { sprintf ( 'No bytes uploaded yet.' ) }
260+ end
261+ @upload_incomplete = true
262+ when 400 ..499
263+ # Upload is canceled
264+ @upload_incomplete = false
265+ when 200 , 201
266+ # Upload is complete.
267+ @upload_incomplete = false
268+ else
269+ logger . debug { sprintf ( "Unexpected response: #{ response . code } - #{ response . body } " ) }
270+ @upload_incomplete = true
271+ end
272+ end
273+
185274 def streamable? ( upload_source )
186275 upload_source . is_a? ( IO ) || upload_source . is_a? ( StringIO ) || upload_source . is_a? ( Tempfile )
187276 end
0 commit comments