237 lines
8.0 KiB
Ruby
237 lines
8.0 KiB
Ruby
# 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.
|
|
# 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/upload'
|
|
require 'google/apis/core/download'
|
|
require 'google/apis/core/composite_io'
|
|
require 'addressable/uri'
|
|
require 'securerandom'
|
|
|
|
module Google
|
|
module Apis
|
|
module Core
|
|
# Wrapper request for batching multiple calls in a single server request
|
|
class BatchCommand < HttpCommand
|
|
MULTIPART_MIXED = 'multipart/mixed'
|
|
|
|
# @param [symbol] method
|
|
# HTTP method
|
|
# @param [String,Addressable::URI, Addressable::Template] url
|
|
# HTTP URL or template
|
|
def initialize(method, url)
|
|
super(method, url)
|
|
@calls = []
|
|
@base_id = SecureRandom.uuid
|
|
end
|
|
|
|
##
|
|
# Add a new call to the batch request.
|
|
#
|
|
# @param [Google::Apis::Core::HttpCommand] call API Request to add
|
|
# @yield [result, err] Result & error when response available
|
|
# @return [Google::Apis::Core::BatchCommand] self
|
|
def add(call, &block)
|
|
ensure_valid_command(call)
|
|
@calls << [call, block]
|
|
self
|
|
end
|
|
|
|
protected
|
|
|
|
##
|
|
# Deconstruct the batch response and process the individual results
|
|
#
|
|
# @param [String] content_type
|
|
# Content type of body
|
|
# @param [String, #read] body
|
|
# Response body
|
|
# @return [Object]
|
|
# Response object
|
|
def decode_response_body(content_type, body)
|
|
m = /.*boundary=(.+)/.match(content_type)
|
|
if m
|
|
parts = split_parts(body, m[1])
|
|
deserializer = CallDeserializer.new
|
|
parts.each_index do |index|
|
|
response = deserializer.to_http_response(parts[index])
|
|
outer_header = response.shift
|
|
call_id = header_to_id(outer_header['Content-ID'].first) || index
|
|
call, callback = @calls[call_id]
|
|
begin
|
|
result = call.process_response(*response) unless call.nil?
|
|
success(result, &callback)
|
|
rescue => e
|
|
error(e, &callback)
|
|
end
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
def split_parts(body, boundary)
|
|
parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/)
|
|
parts[1...-1]
|
|
end
|
|
|
|
# Encode the batch request
|
|
# @return [void]
|
|
# @raise [Google::Apis::BatchError] if batch is empty
|
|
def prepare!
|
|
fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
|
|
|
|
serializer = CallSerializer.new
|
|
multipart = Multipart.new(content_type: MULTIPART_MIXED)
|
|
@calls.each_index do |index|
|
|
call, _ = @calls[index]
|
|
content_id = id_to_header(index)
|
|
io = serializer.to_part(call)
|
|
multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
|
|
end
|
|
self.body = multipart.assemble
|
|
|
|
header['Content-Type'] = multipart.content_type
|
|
super
|
|
end
|
|
|
|
def ensure_valid_command(command)
|
|
if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand)
|
|
fail Google::Apis::ClientError, 'Can not include media requests in batch'
|
|
end
|
|
fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand)
|
|
end
|
|
|
|
def id_to_header(call_id)
|
|
return sprintf('<%s+%i>', @base_id, call_id)
|
|
end
|
|
|
|
def header_to_id(content_id)
|
|
match = /<response-.*\+(\d+)>/.match(content_id)
|
|
return match[1].to_i if match
|
|
return nil
|
|
end
|
|
|
|
end
|
|
|
|
# Wrapper request for batching multiple uploads in a single server request
|
|
class BatchUploadCommand < BatchCommand
|
|
def ensure_valid_command(command)
|
|
fail Google::Apis::ClientError, 'Can only include upload commands in batch' \
|
|
unless command.is_a?(Google::Apis::Core::BaseUploadCommand)
|
|
end
|
|
|
|
def prepare!
|
|
header['X-Goog-Upload-Protocol'] = 'batch'
|
|
super
|
|
end
|
|
end
|
|
|
|
# Serializes a command for embedding in a multipart batch request
|
|
# @private
|
|
class CallSerializer
|
|
##
|
|
# Serialize a single batched call for assembling the multipart message
|
|
#
|
|
# @param [Google::Apis::Core::HttpCommand] call
|
|
# the call to serialize.
|
|
# @return [IO]
|
|
# the serialized request
|
|
def to_part(call)
|
|
call.prepare!
|
|
# This will add the Authorization header if needed.
|
|
call.apply_request_options(call.header)
|
|
parts = []
|
|
parts << build_head(call)
|
|
parts << build_body(call) unless call.body.nil?
|
|
length = parts.inject(0) { |a, e| a + e.length }
|
|
Google::Apis::Core::CompositeIO.new(*parts)
|
|
end
|
|
|
|
protected
|
|
|
|
def build_head(call)
|
|
request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1"
|
|
call.header.each do |key, value|
|
|
request_head << sprintf("\r\n%s: %s", key, value)
|
|
end
|
|
request_head << sprintf("\r\nHost: %s", call.url.host)
|
|
request_head << "\r\n\r\n"
|
|
StringIO.new(request_head)
|
|
end
|
|
|
|
def build_body(call)
|
|
return nil if call.body.nil?
|
|
return call.body if call.body.respond_to?(:read)
|
|
StringIO.new(call.body)
|
|
end
|
|
end
|
|
|
|
# Deconstructs a raw HTTP response part
|
|
# @private
|
|
class CallDeserializer
|
|
# Parse a batched response.
|
|
#
|
|
# @param [String] call_response
|
|
# the response to parse.
|
|
# @return [Array<(Fixnum, Hash, String)>]
|
|
# Status, header, and response body.
|
|
def to_http_response(call_response)
|
|
outer_header, outer_body = split_header_and_body(call_response)
|
|
status_line, payload = outer_body.split(/\n/, 2)
|
|
_, status = status_line.split(' ', 3)
|
|
|
|
header, body = split_header_and_body(payload)
|
|
[outer_header, status.to_i, header, body]
|
|
end
|
|
|
|
protected
|
|
|
|
# Auxiliary method to split the header from the body in an HTTP response.
|
|
#
|
|
# @param [String] response
|
|
# the response to parse.
|
|
# @return [Array<(HTTP::Message::Headers, String)>]
|
|
# the header and the body, separately.
|
|
def split_header_and_body(response)
|
|
header = HTTP::Message::Headers.new
|
|
payload = response.lstrip
|
|
while payload
|
|
line, payload = payload.split(/\n/, 2)
|
|
line.sub!(/\s+\z/, '')
|
|
break if line.empty?
|
|
match = /\A([^:]+):\s*/.match(line)
|
|
fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
|
|
header[match[1]] = match.post_match
|
|
end
|
|
[header, payload]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|