| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | # Copyright 2015 Google Inc. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | # you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | # You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require 'google/apis/core/multipart' | 
					
						
							|  |  |  | require 'google/apis/core/http_command' | 
					
						
							|  |  |  | require 'google/apis/core/api_command' | 
					
						
							|  |  |  | require 'google/apis/errors' | 
					
						
							|  |  |  | require 'addressable/uri' | 
					
						
							| 
									
										
										
										
											2016-01-20 22:55:38 +00:00
										 |  |  | require 'tempfile' | 
					
						
							| 
									
										
										
										
											2019-05-13 19:21:53 +00:00
										 |  |  | require 'mini_mime' | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | module Google | 
					
						
							|  |  |  |   module Apis | 
					
						
							|  |  |  |     module Core | 
					
						
							|  |  |  |       # Base upload command. Not intended to be used directly | 
					
						
							|  |  |  |       # @private | 
					
						
							|  |  |  |       class BaseUploadCommand < ApiCommand | 
					
						
							|  |  |  |         UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol' | 
					
						
							|  |  |  |         UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type' | 
					
						
							|  |  |  |         UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length' | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |         CONTENT_TYPE_HEADER = 'Content-Type' | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # File name or IO containing the content to upload | 
					
						
							|  |  |  |         # @return [String, File, #read] | 
					
						
							|  |  |  |         attr_accessor :upload_source | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Content type of the upload material | 
					
						
							|  |  |  |         # @return [String] | 
					
						
							|  |  |  |         attr_accessor :upload_content_type | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Content, as UploadIO | 
					
						
							|  |  |  |         # @return [Google::Apis::Core::UploadIO] | 
					
						
							|  |  |  |         attr_accessor :upload_io | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # Ensure the content is readable and wrapped in an IO instance. | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         # | 
					
						
							|  |  |  |         # @return [void] | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] if upload source is invalid | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           super | 
					
						
							| 
									
										
										
										
											2016-01-06 05:35:23 +00:00
										 |  |  |           if streamable?(upload_source) | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |             self.upload_io = upload_source | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |             @close_io_on_finish = false | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |           elsif self.upload_source.is_a?(String) | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |             self.upload_io = File.new(upload_source, 'r') | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |             if self.upload_content_type.nil? | 
					
						
							| 
									
										
										
										
											2019-05-13 19:21:53 +00:00
										 |  |  |               type = MiniMime.lookup_by_filename(upload_source) | 
					
						
							|  |  |  |               self.upload_content_type = type && type.content_type | 
					
						
							| 
									
										
										
										
											2016-08-17 21:38:25 +00:00
										 |  |  |             end | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |             @close_io_on_finish = true | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             fail Google::Apis::ClientError, 'Invalid upload source' | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |           if self.upload_content_type.nil? || self.upload_content_type.empty? | 
					
						
							|  |  |  |             self.upload_content_type = 'application/octet-stream' | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Close IO stream when command done. Only closes the stream if it was opened by the command. | 
					
						
							|  |  |  |         def release! | 
					
						
							|  |  |  |           upload_io.close if @close_io_on_finish | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-01-06 05:35:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         private | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def streamable?(upload_source) | 
					
						
							|  |  |  |           upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Implementation of the raw upload protocol | 
					
						
							|  |  |  |       class RawUploadCommand < BaseUploadCommand | 
					
						
							|  |  |  |         RAW_PROTOCOL = 'raw' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @return [void] | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] if upload source is invalid | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           super | 
					
						
							|  |  |  |           self.body = upload_io | 
					
						
							|  |  |  |           header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Implementation of the multipart upload protocol | 
					
						
							|  |  |  |       class MultipartUploadCommand < BaseUploadCommand | 
					
						
							|  |  |  |         MULTIPART_PROTOCOL = 'multipart' | 
					
						
							|  |  |  |         MULTIPART_RELATED = 'multipart/related' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Encode the multipart request | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @return [void] | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] if upload source is invalid | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           super | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           multipart = Multipart.new | 
					
						
							|  |  |  |           multipart.add_json(body) | 
					
						
							|  |  |  |           multipart.add_upload(upload_io, content_type: upload_content_type) | 
					
						
							|  |  |  |           self.body = multipart.assemble | 
					
						
							|  |  |  |           header['Content-Type'] = multipart.content_type | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Implementation of the resumable upload protocol | 
					
						
							|  |  |  |       class ResumableUploadCommand < BaseUploadCommand | 
					
						
							|  |  |  |         UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command' | 
					
						
							|  |  |  |         UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset' | 
					
						
							|  |  |  |         BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received' | 
					
						
							|  |  |  |         UPLOAD_URL_HEADER = 'X-Goog-Upload-URL' | 
					
						
							|  |  |  |         UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status' | 
					
						
							|  |  |  |         STATUS_ACTIVE = 'active' | 
					
						
							|  |  |  |         STATUS_FINAL = 'final' | 
					
						
							|  |  |  |         STATUS_CANCELLED = 'cancelled' | 
					
						
							|  |  |  |         RESUMABLE = 'resumable' | 
					
						
							|  |  |  |         START_COMMAND = 'start' | 
					
						
							|  |  |  |         QUERY_COMMAND = 'query' | 
					
						
							|  |  |  |         UPLOAD_COMMAND = 'upload, finalize' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Reset upload to initial state. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @return [void] | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] if upload source is invalid | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           @state = :start | 
					
						
							|  |  |  |           @upload_url = nil | 
					
						
							|  |  |  |           @offset = 0
 | 
					
						
							| 
									
										
										
										
											2019-11-05 16:28:54 +00:00
										 |  |  |           # Prevent the command from populating the body with form encoding, by | 
					
						
							|  |  |  |           # asserting that it already has a body. Form encoding is never used | 
					
						
							|  |  |  |           # by upload requests. | 
					
						
							|  |  |  |           self.body = '' unless self.body | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           super | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Check the to see if the upload is complete or needs to be resumed. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @param [Fixnum] status | 
					
						
							|  |  |  |         #   HTTP status code of response | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @param [HTTP::Message::Headers] header | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   Response headers | 
					
						
							|  |  |  |         # @param [String, #read] body | 
					
						
							|  |  |  |         #  Response body | 
					
						
							|  |  |  |         # @return [Object] | 
					
						
							|  |  |  |         #   Response object | 
					
						
							|  |  |  |         # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification | 
					
						
							|  |  |  |         # @raise [Google::Apis::AuthorizationError] Authorization is required | 
					
						
							|  |  |  |         def process_response(status, header, body) | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           @offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty? | 
					
						
							|  |  |  |           @upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty? | 
					
						
							|  |  |  |           upload_status = header[UPLOAD_STATUS_HEADER].first | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           logger.debug { sprintf('Upload status %s', upload_status) } | 
					
						
							|  |  |  |           if upload_status == STATUS_ACTIVE | 
					
						
							|  |  |  |             @state = :active | 
					
						
							|  |  |  |           elsif upload_status == STATUS_FINAL | 
					
						
							|  |  |  |             @state = :final | 
					
						
							|  |  |  |           elsif upload_status == STATUS_CANCELLED | 
					
						
							|  |  |  |             @state = :cancelled | 
					
						
							|  |  |  |             fail Google::Apis::ClientError, body | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           super(status, header, body) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def send_start_command(client) | 
					
						
							|  |  |  |           logger.debug { sprintf('Sending upload start command to %s', url) } | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |           request_header = header.dup | 
					
						
							|  |  |  |           apply_request_options(request_header) | 
					
						
							|  |  |  |           request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE | 
					
						
							|  |  |  |           request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND | 
					
						
							|  |  |  |           request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s | 
					
						
							|  |  |  |           request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           client.request(method.to_s.upcase, | 
					
						
							|  |  |  |                          url.to_s, query: nil, | 
					
						
							|  |  |  |                          body: body, | 
					
						
							|  |  |  |                          header: request_header, | 
					
						
							|  |  |  |                          follow_redirect: true) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         rescue => e | 
					
						
							|  |  |  |           raise Google::Apis::ServerError, e.message | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Query for the status of an incomplete upload | 
					
						
							|  |  |  |         # | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @param [HTTPClient] client | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   HTTP client | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @return [HTTP::Message] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         # @raise [Google::Apis::ServerError] Unable to send the request | 
					
						
							|  |  |  |         def send_query_command(client) | 
					
						
							|  |  |  |           logger.debug { sprintf('Sending upload query command to %s', @upload_url) } | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |           request_header = header.dup | 
					
						
							|  |  |  |           apply_request_options(request_header) | 
					
						
							|  |  |  |           request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |           client.post(@upload_url, body: '', header: request_header, follow_redirect: true) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         # Send the actual content | 
					
						
							|  |  |  |         # | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @param [HTTPClient] client | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   HTTP client | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @return [HTTP::Message] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         # @raise [Google::Apis::ServerError] Unable to send the request | 
					
						
							|  |  |  |         def send_upload_command(client) | 
					
						
							|  |  |  |           logger.debug { sprintf('Sending upload command to %s', @upload_url) } | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           content = upload_io | 
					
						
							|  |  |  |           content.pos = @offset | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |           request_header = header.dup | 
					
						
							|  |  |  |           apply_request_options(request_header) | 
					
						
							|  |  |  |           request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND | 
					
						
							|  |  |  |           request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND | 
					
						
							|  |  |  |           request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s | 
					
						
							| 
									
										
										
										
											2017-04-03 18:10:54 +00:00
										 |  |  |           request_header[CONTENT_TYPE_HEADER] = upload_content_type | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |           client.post(@upload_url, body: content, header: request_header, follow_redirect: true) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query | 
					
						
							|  |  |  |         # for the status of the upload, the second to send the (remaining) content. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @private | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @param [HTTPClient] client | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   HTTP client | 
					
						
							|  |  |  |         # @yield [result, err] Result or error if block supplied | 
					
						
							|  |  |  |         # @return [Object] | 
					
						
							|  |  |  |         # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried | 
					
						
							|  |  |  |         # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification | 
					
						
							|  |  |  |         # @raise [Google::Apis::AuthorizationError] Authorization is required | 
					
						
							|  |  |  |         def execute_once(client, &block) | 
					
						
							| 
									
										
										
										
											2015-11-30 23:35:18 +00:00
										 |  |  |           case @state | 
					
						
							|  |  |  |           when :start | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |             response = send_start_command(client) | 
					
						
							| 
									
										
										
										
											2015-11-30 23:35:18 +00:00
										 |  |  |             result = process_response(response.status_code, response.header, response.body) | 
					
						
							|  |  |  |           when :active | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |             response = send_query_command(client) | 
					
						
							| 
									
										
										
										
											2015-11-30 23:35:18 +00:00
										 |  |  |             result = process_response(response.status_code, response.header, response.body) | 
					
						
							|  |  |  |           when :cancelled, :final | 
					
						
							|  |  |  |             error(@last_error, rethrow: true, &block) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           end | 
					
						
							|  |  |  |           if @state == :active | 
					
						
							|  |  |  |             response = send_upload_command(client) | 
					
						
							|  |  |  |             result = process_response(response.status_code, response.header, response.body) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           success(result, &block) if @state == :final | 
					
						
							|  |  |  |         rescue => e | 
					
						
							| 
									
										
										
										
											2015-11-30 23:35:18 +00:00
										 |  |  |           # Some APIs like Youtube generate non-retriable 401 errors and mark | 
					
						
							|  |  |  |           # the upload as finalized. Save the error just in case we get | 
					
						
							|  |  |  |           # retried. | 
					
						
							|  |  |  |           @last_error = e | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           error(e, rethrow: true, &block) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |