Remove Hurley as a dependency

This commit is contained in:
Steve Bazyl 2016-08-17 13:51:09 -07:00
parent c5327670d0
commit 2046e00f14
18 changed files with 417 additions and 529 deletions

View File

@ -24,9 +24,8 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'retriable', '~> 2.0' spec.add_runtime_dependency 'retriable', '~> 2.0'
spec.add_runtime_dependency 'addressable', '~> 2.3' spec.add_runtime_dependency 'addressable', '~> 2.3'
spec.add_runtime_dependency 'mime-types', '>= 1.6' spec.add_runtime_dependency 'mime-types', '>= 1.6'
spec.add_runtime_dependency 'hurley', '~> 0.1'
spec.add_runtime_dependency 'googleauth', '~> 0.5' spec.add_runtime_dependency 'googleauth', '~> 0.5'
spec.add_runtime_dependency 'thor', '~> 0.19' spec.add_runtime_dependency 'thor', '~> 0.19'
spec.add_runtime_dependency 'httpclient', '~> 2.7' spec.add_runtime_dependency 'httpclient', '>= 2.8.1', '< 3.0'
spec.add_runtime_dependency 'memoist', '~> 0.11' spec.add_runtime_dependency 'memoist', '~> 0.11'
end end

View File

@ -50,7 +50,7 @@ module Google
def prepare! def prepare!
query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM) query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
if request_representation && request_object if request_representation && request_object
header[:content_type] ||= JSON_CONTENT_TYPE header['Content-Type'] ||= JSON_CONTENT_TYPE
self.body = request_representation.new(request_object).to_json(skip_undefined: true) self.body = request_representation.new(request_object).to_json(skip_undefined: true)
end end
super super
@ -78,7 +78,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [Hash] header
# HTTP response headers # HTTP response headers
# @param [String] body # @param [String] body
# HTTP response body # HTTP response body

View File

@ -19,11 +19,8 @@ require 'google/apis/core/api_command'
require 'google/apis/core/batch' require 'google/apis/core/batch'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/http_client_adapter'
require 'google/apis/options' require 'google/apis/options'
require 'googleauth' require 'googleauth'
require 'hurley'
require 'hurley/addressable'
module Google module Google
module Apis module Apis
@ -96,7 +93,7 @@ module Google
attr_accessor :batch_path attr_accessor :batch_path
# HTTP client # HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
attr_accessor :client attr_accessor :client
# General settings # General settings
@ -198,7 +195,7 @@ module Google
end end
# Get the current HTTP client # Get the current HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
def client def client
@client ||= new_client @client ||= new_client
end end
@ -368,19 +365,30 @@ module Google
end end
# Create a new HTTP client # Create a new HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
def new_client def new_client
client = Hurley::Client.new client = ::HTTPClient.new
client.connection = Google::Apis::Core::HttpClientAdapter.new unless client_options.use_net_http
client.request_options.timeout = request_options.timeout_sec client.transparent_gzip_decompression = true
client.request_options.open_timeout = request_options.open_timeout_sec
client.request_options.proxy = client_options.proxy_url client.proxy = client_options.proxy_url if client_options.proxy_url
client.request_options.query_class = Hurley::Query::Flat
client.ssl_options.ca_file = File.join(Google::Apis::ROOT, 'lib', 'cacerts.pem') if request_options.timeout_sec
client.header[:user_agent] = user_agent client.connect_timeout = request_options.timeout_sec
client.receive_timeout = request_options.timeout_sec
client.send_timeout = request_options.timeout_sec
end
if request_options.open_timeout_sec
client.connect_timeout = request_options.open_timeout_sec
client.send_timeout = request_options.open_timeout_sec
end
client.follow_redirect_count = 5
client.default_header = { 'User-Agent' => user_agent }
client client
end end
# Build the user agent header # Build the user agent header
# @return [String] # @return [String]
def user_agent def user_agent

View File

@ -25,19 +25,19 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'hurley'
require 'google/apis/core/multipart' require 'google/apis/core/multipart'
require 'google/apis/core/http_command' require 'google/apis/core/http_command'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/composite_io'
require 'addressable/uri' require 'addressable/uri'
require 'securerandom' require 'securerandom'
module Google module Google
module Apis module Apis
module Core module Core
# Wrapper request for batching multiple calls in a single server request # Wrapper request for batching multiple calls in a single server request
class BatchCommand < HttpCommand class BatchCommand < HttpCommand
BATCH_BOUNDARY = 'RubyApiBatchRequest'.freeze
MULTIPART_MIXED = 'multipart/mixed' MULTIPART_MIXED = 'multipart/mixed'
# @param [symbol] method # @param [symbol] method
@ -81,7 +81,7 @@ module Google
parts.each_index do |index| parts.each_index do |index|
response = deserializer.to_http_response(parts[index]) response = deserializer.to_http_response(parts[index])
outer_header = response.shift outer_header = response.shift
call_id = header_to_id(outer_header[:content_id]) || index call_id = header_to_id(outer_header['Content-ID'].first) || index
call, callback = @calls[call_id] call, callback = @calls[call_id]
begin begin
result = call.process_response(*response) unless call.nil? result = call.process_response(*response) unless call.nil?
@ -106,17 +106,16 @@ module Google
fail BatchError, 'Cannot make an empty batch request' if @calls.empty? fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
serializer = CallSerializer.new serializer = CallSerializer.new
multipart = Multipart.new(boundary: BATCH_BOUNDARY, content_type: MULTIPART_MIXED) multipart = Multipart.new(content_type: MULTIPART_MIXED)
@calls.each_index do |index| @calls.each_index do |index|
call, _ = @calls[index] call, _ = @calls[index]
content_id = id_to_header(index) content_id = id_to_header(index)
io = serializer.to_upload_io(call) io = serializer.to_part(call)
multipart.add_upload(io, content_id: content_id) multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
end end
self.body = multipart.assemble self.body = multipart.assemble
header[:content_type] = multipart.content_type header['Content-Type'] = multipart.content_type
header[:content_length] = "#{body.length}"
super super
end end
@ -155,24 +154,20 @@ module Google
# Serializes a command for embedding in a multipart batch request # Serializes a command for embedding in a multipart batch request
# @private # @private
class CallSerializer class CallSerializer
HTTP_CONTENT_TYPE = 'application/http'
## ##
# Serialize a single batched call for assembling the multipart message # Serialize a single batched call for assembling the multipart message
# #
# @param [Google::Apis::Core::HttpCommand] call # @param [Google::Apis::Core::HttpCommand] call
# the call to serialize. # the call to serialize.
# @return [Hurley::UploadIO] # @return [IO]
# the serialized request # the serialized request
def to_upload_io(call) def to_part(call)
call.prepare! call.prepare!
parts = [] parts = []
parts << build_head(call) parts << build_head(call)
parts << build_body(call) unless call.body.nil? parts << build_body(call) unless call.body.nil?
length = parts.inject(0) { |a, e| a + e.length } length = parts.inject(0) { |a, e| a + e.length }
Hurley::UploadIO.new(Hurley::CompositeReadIO.new(length, *parts), Google::Apis::Core::CompositeIO.new(*parts)
HTTP_CONTENT_TYPE,
'ruby-api-request')
end end
protected protected
@ -201,7 +196,7 @@ module Google
# #
# @param [String] call_response # @param [String] call_response
# the response to parse. # the response to parse.
# @return [Array<(Fixnum, Hurley::Header, String)>] # @return [Array<(Fixnum, Hash, String)>]
# Status, header, and response body. # Status, header, and response body.
def to_http_response(call_response) def to_http_response(call_response)
outer_header, outer_body = split_header_and_body(call_response) outer_header, outer_body = split_header_and_body(call_response)
@ -218,10 +213,10 @@ module Google
# #
# @param [String] response # @param [String] response
# the response to parse. # the response to parse.
# @return [Array<(Hurley::Header, String)>] # @return [Array<(HTTP::Message::Headers, String)>]
# the header and the body, separately. # the header and the body, separately.
def split_header_and_body(response) def split_header_and_body(response)
header = Hurley::Header.new header = HTTP::Message::Headers.new
payload = response.lstrip payload = response.lstrip
while payload while payload
line, payload = payload.split(/\n/, 2) line, payload = payload.split(/\n/, 2)

View File

@ -0,0 +1,97 @@
# 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/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'addressable/uri'
require 'securerandom'
module Google
module Apis
module Core
class CompositeIO
def initialize(*ios)
@ios = ios.flatten
@pos = 0
@index = 0
@sizes = @ios.map(&:size)
end
def read(length = nil, buf = nil)
buf = buf ? buf.replace('') : ''
begin
io = @ios[@index]
break if io.nil?
result = io.read(length)
if result
buf << result
if length
length -= result.length
break if length == 0
end
end
@index += 1
end while @index < @ios.length
buf.length > 0 ? buf : nil
end
def size
@sizes.reduce(:+)
end
alias_method :length, :size
def pos
@pos
end
def pos=(pos)
fail ArgumentError, "Position can not be negative" if pos < 0
@pos = pos
new_index = nil
@ios.each_with_index do |io,idx|
size = io.size
if pos <= size
new_index ||= idx
io.pos = pos
pos = 0
else
io.pos = size
pos -= size
end
end
@index = new_index unless new_index.nil?
end
def rewind
self.pos = 0
end
end
end
end
end

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'google/apis/core/multipart'
require 'google/apis/core/api_command' require 'google/apis/core/api_command'
require 'google/apis/errors' require 'google/apis/errors'
require 'addressable/uri' require 'addressable/uri'
@ -22,7 +21,7 @@ module Google
module Core module Core
# Streaming/resumable media download support # Streaming/resumable media download support
class DownloadCommand < ApiCommand class DownloadCommand < ApiCommand
RANGE_HEADER = 'range' RANGE_HEADER = 'Range'
# File or IO to write content to # File or IO to write content to
# @return [String, File, #write] # @return [String, File, #write]
@ -57,7 +56,7 @@ module Google
# of file content. # of file content.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]
@ -65,34 +64,41 @@ module Google
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client, &block) def execute_once(client, &block)
client.get(@download_url || url) do |req| request_header = header.dup
apply_request_options(req) apply_request_options(request_header)
check_if_rewind_needed = false check_if_rewind_needed = false
if @offset > 0 if @offset > 0
logger.debug { sprintf('Resuming download from offset %d', @offset) } logger.debug { sprintf('Resuming download from offset %d', @offset) }
req.header[RANGE_HEADER] = sprintf('bytes=%d-', @offset) request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
check_if_rewind_needed = true check_if_rewind_needed = true
end end
req.on_body(200, 201, 206) do |res, chunk|
check_status(res.status_code, chunk) unless res.status_code.nil? http_res = client.get(url.to_s,
if check_if_rewind_needed && res.status_code != 206 query: query,
header: request_header,
follow_redirect: true) do |res, chunk|
status = res.http_header.status_code.to_i
if [200, 201, 206].include?(status)
if check_if_rewind_needed && status != 206
# Oh no! Requested a chunk, but received the entire content # Oh no! Requested a chunk, but received the entire content
# Attempt to rewind the stream # Attempt to rewind the stream
@download_io.rewind @download_io.rewind
check_if_rewind_needed = false check_if_rewind_needed = false
end end
logger.debug { sprintf('Writing chunk (%d bytes)', chunk.length) } logger.debug { sprintf('Writing chunk (%d bytes)', chunk.length) }
@offset += chunk.length @offset += chunk.length
@download_io.write(chunk) @download_io.write(chunk)
@download_io.flush @download_io.flush
end end
end end
if @close_io_on_finish if @close_io_on_finish
result = nil result = nil
else else
result = @download_io result = @download_io
end end
check_status(http_res.status.to_i, http_res.header, http_res.body)
success(result, &block) success(result, &block)
rescue => e rescue => e
error(e, rethrow: true, &block) error(e, rethrow: true, &block)

View File

@ -1,82 +0,0 @@
require 'httpclient'
require 'hurley'
require 'hurley/client'
module Google
module Apis
module Core
# HTTPClient adapter for Hurley.
class HttpClientAdapter
def call(request)
client = ::HTTPClient.new
configure_client(client, request)
begin
::Hurley::Response.new(request) do |res|
http_res = client.request(request.verb.to_s.upcase, request.url.to_s, nil, request.body_io, request.header.to_hash, false) do |http_res, chunk|
copy_response(http_res, res)
res.receive_body(chunk)
end
copy_response(http_res, res)
end
rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT
raise ::Hurley::Timeout, $!
rescue ::HTTPClient::BadResponseError => err
if err.message.include?('status 407')
raise ::Hurley::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Hurley::ClientError, $!
end
rescue Errno::ECONNREFUSED, EOFError
raise ::Hurley::ConnectionFailed, $!
rescue => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Hurley::SSLError, err
else
raise
end
end
end
def copy_response(http_res, res)
unless res.status_code
res.status_code = http_res.status.to_i
http_res.header.all.each do |(k,v)|
res.header[k] = v
end
end
end
def configure_client(client, request)
client.transparent_gzip_decompression = true
if request.options.proxy
proxy = request.options.proxy
client.proxy = sprintf('%s:%d', proxy.host, proxy.port)
if proxy.user && proxy.password
client.set_proxy_auth proxy.user, proxy.password
end
end
if request.options.timeout
client.connect_timeout = request.options.timeout
client.receive_timeout = request.options.timeout
client.send_timeout = request.options.timeout
end
if request.options.open_timeout
client.connect_timeout = request.options.open_timeout
client.send_timeout = request.options.open_timeout
end
ssl_config = client.ssl_config
ssl_opts = request.ssl_options
ssl_config.verify_mode = ssl_opts.openssl_verify_mode
ssl_config.cert_store = ssl_opts.openssl_cert_store
ssl_config.add_trust_ca ssl_opts.ca_file if ssl_opts.ca_file
ssl_config.add_trust_ca ssl_opts.ca_path if ssl_opts.ca_path
ssl_config.client_cert = ssl_opts.openssl_client_cert if ssl_opts.openssl_client_cert
ssl_config.client_key = ssl_opts.openssl_client_key if ssl_opts.openssl_client_key
ssl_config.verify_depth = ssl_opts.verify_depth if ssl_opts.verify_depth
end
end
end
end
end

View File

@ -17,9 +17,6 @@ require 'addressable/template'
require 'google/apis/options' require 'google/apis/options'
require 'google/apis/errors' require 'google/apis/errors'
require 'retriable' require 'retriable'
require 'hurley'
require 'hurley/addressable'
require 'hurley_patches'
require 'google/apis/core/logging' require 'google/apis/core/logging'
require 'pp' require 'pp'
@ -41,7 +38,7 @@ module Google
attr_accessor :url attr_accessor :url
# HTTP headers # HTTP headers
# @return [Hurley::Header] # @return [Hash]
attr_accessor :header attr_accessor :header
# Request body # Request body
@ -53,7 +50,7 @@ module Google
attr_accessor :method attr_accessor :method
# HTTP Client # HTTP Client
# @return [Hurley::Client] # @return [HTTPClient]
attr_accessor :connection attr_accessor :connection
# Query params # Query params
@ -75,7 +72,7 @@ module Google
self.url = url self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String) self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method self.method = method
self.header = Hurley::Header.new self.header = Hash.new
self.body = body self.body = body
self.query = {} self.query = {}
self.params = {} self.params = {}
@ -83,7 +80,7 @@ module Google
# Execute the command, retrying as necessary # Execute the command, retrying as necessary
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]
@ -166,7 +163,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [Hash] header
# Response headers # Response headers
# @param [String, #read] body # @param [String, #read] body
# Response body # Response body
@ -177,7 +174,7 @@ module Google
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body) def process_response(status, header, body)
check_status(status, header, body) check_status(status, header, body)
decode_response_body(header[:content_type], body) decode_response_body(header['Content-Type'].first, body)
end end
# Check the response and raise error if needed # Check the response and raise error if needed
@ -185,7 +182,7 @@ module Google
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param # @param
# @param [Hurley::Header] header # @param [Hash] header
# HTTP response headers # HTTP response headers
# @param [String] body # @param [String] body
# HTTP response body # HTTP response body
@ -201,7 +198,7 @@ module Google
when 200...300 when 200...300
nil nil
when 301, 302, 303, 307 when 301, 302, 303, 307
message ||= sprintf('Redirect to %s', header[:location]) message ||= sprintf('Redirect to %s', header['Location'])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body) raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401 when 401
message ||= 'Unauthorized' message ||= 'Unauthorized'
@ -251,7 +248,16 @@ module Google
# @raise [StandardError] if no block # @raise [StandardError] if no block
def error(err, rethrow: false, &block) def error(err, rethrow: false, &block)
logger.debug { sprintf('Error - %s', PP.pp(err, '')) } logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
err = Google::Apis::TransmissionError.new(err) if err.is_a?(Hurley::ClientError) || err.is_a?(SocketError) if err.is_a?(HTTPClient::BadResponseError)
begin
res = err.res
check_status(res.status.to_i, res.header, res.body)
rescue Google::Apis::Error => e
err = e
end
elsif err.is_a?(HTTPClient::TimeoutError) || err.is_a?(SocketError)
err = Google::Apis::TransmissionError.new(err)
end
block.call(nil, err) if block_given? block.call(nil, err) if block_given?
fail err if rethrow || block.nil? fail err if rethrow || block.nil?
end end
@ -259,7 +265,7 @@ module Google
# Execute the command once. # Execute the command once.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Object] # @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@ -269,21 +275,18 @@ module Google
body.rewind if body.respond_to?(:rewind) body.rewind if body.respond_to?(:rewind)
begin begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) } logger.debug { sprintf('Sending HTTP %s %s', method, url) }
response = client.send(method, url, body) do |req| request_header = header.dup
# Temporary workaround for Hurley bug where the connection preference apply_request_options(request_header)
# is ignored and it uses nested anyway
unless form_encoded? http_res = client.request(method.to_s.upcase,
req.url.query_class = Hurley::Query::Flat url.to_s,
query.each do | k, v| query: nil,
req.url.query[k] = normalize_query_value(v) body: body,
end header: request_header,
end follow_redirect: true)
# End workaround logger.debug { http_res.status }
apply_request_options(req) logger.debug { http_res.inspect }
end response = process_response(http_res.status.to_i, http_res.header, http_res.body)
logger.debug { response.status_code }
logger.debug { response.inspect }
response = process_response(response.status_code, response.header, response.body)
success(response) success(response)
rescue => e rescue => e
logger.debug { sprintf('Caught error %s', e) } logger.debug { sprintf('Caught error %s', e) }
@ -292,18 +295,16 @@ module Google
end end
# Update the request with any specified options. # Update the request with any specified options.
# @param [Hurley::Request] req # @param [Hash] header
# HTTP request # HTTP headers
# @return [void] # @return [void]
def apply_request_options(req) def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!) if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req.header) options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String) elsif options.authorization.is_a?(String)
req.header[:authorization] = sprintf('Bearer %s', options.authorization) req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end end
req.header.update(header) req_header.update(header)
req.options.timeout = options.timeout_sec
req.options.open_timeout = options.open_timeout_sec
end end
private private

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'hurley'
module Google module Google
module Apis module Apis
@ -21,108 +20,60 @@ module Google
# #
# @private # @private
class JsonPart class JsonPart
include Hurley::Multipart::Part
# @return [Fixnum]
# Length of part
attr_reader :length
# @param [String] boundary
# Multipart boundary
# @param [String] value # @param [String] value
# JSON content # JSON content
def initialize(boundary, value, header = {}) # @param [Hash] header
@part = build_part(boundary, value) # Additional headers
@length = @part.bytesize def initialize(value, header = {})
@io = StringIO.new(@part) @value = value
@header = header
end end
private def to_io(boundary)
# Format the part
#
# @param [String] boundary
# Multipart boundary
# @param [String] value
# JSON content
# @return [String]
def build_part(boundary, value)
part = '' part = ''
part << "--#{boundary}\r\n" part << "--#{boundary}\r\n"
part << "Content-Type: application/json\r\n" part << "Content-Type: application/json\r\n"
part << "\r\n" @header.each do |(k, v)|
part << "#{value}\r\n" part << "#{k}: #{v}\r\n"
end end
part << "\r\n"
part << "#{@value}\r\n"
StringIO.new(part)
end end
# Part of a multipart request for holding arbitrary content. Modified end
# from Hurley::Multipart::FilePart to remove Content-Disposition
# Part of a multipart request for holding arbitrary content.
# #
# @private # @private
class FilePart class FilePart
include Hurley::Multipart::Part # @param [IO] io
# @return [Fixnum]
# Length of part
attr_reader :length
# @param [String] boundary
# Multipart boundary
# @param [Google::Apis::Core::UploadIO] io
# IO stream # IO stream
# @param [Hash] header # @param [Hash] header
# Additional headers # Additional headers
def initialize(boundary, io, header = {}) def initialize(io, header = {})
file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path) @io = io
@header = header
@head = build_head(boundary, io.content_type, file_length, @length = io.respond_to?(:size) ? io.size : nil
io.respond_to?(:opts) ? io.opts.merge(header) : header)
@length = @head.bytesize + file_length + FOOT.length
@io = Hurley::CompositeReadIO.new(@length, StringIO.new(@head), io, StringIO.new(FOOT))
end end
private def to_io(boundary)
head = ''
# Construct the header for the part head << "--#{boundary}\r\n"
# @header.each do |(k, v)|
# @param [String] boundary head << "#{k}: #{v}\r\n"
# Multipart boundary
# @param [String] type
# Content type for the part
# @param [Fixnum] content_len
# Length of the part
# @param [Hash] header
# Headers for the part
def build_head(boundary, type, content_len, header)
content_id = ''
if header[:content_id]
content_id = sprintf(CID_FORMAT, header[:content_id])
end end
sprintf(HEAD_FORMAT, head << "Content-Length: #{@length}\r\n" unless @length.nil?
boundary, head << "Content-Transfer-Encoding: binary\r\n"
content_len.to_i, head << "\r\n"
content_id, Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
header[:content_type] || type,
header[:content_transfer_encoding] || DEFAULT_TR_ENCODING)
end end
DEFAULT_TR_ENCODING = 'binary'.freeze
FOOT = "\r\n".freeze
CID_FORMAT = "Content-ID: %s\r\n"
HEAD_FORMAT = <<-END
--%s\r
Content-Length: %d\r
%sContent-Type: %s\r
Content-Transfer-Encoding: %s\r
\r
END
end end
# Helper for building multipart requests # Helper for building multipart requests
class Multipart class Multipart
MULTIPART_RELATED = 'multipart/related' MULTIPART_RELATED = 'multipart/related'
DEFAULT_BOUNDARY = 'RubyApiClientMultiPart'
# @return [String] # @return [String]
# Content type header # Content type header
@ -135,8 +86,8 @@ Content-Transfer-Encoding: %s\r
def initialize(content_type: MULTIPART_RELATED, boundary: nil) def initialize(content_type: MULTIPART_RELATED, boundary: nil)
@parts = [] @parts = []
@boundary = boundary || DEFAULT_BOUNDARY @boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
@content_type = "#{content_type}; boundary=#{boundary}" @content_type = "#{content_type}; boundary=#{@boundary}"
end end
# Append JSON data part # Append JSON data part
@ -147,23 +98,26 @@ Content-Transfer-Encoding: %s\r
# Optional unique ID of this part # Optional unique ID of this part
# @return [self] # @return [self]
def add_json(body, content_id: nil) def add_json(body, content_id: nil)
header = { :content_id => content_id } header = {}
@parts << Google::Apis::Core::JsonPart.new(@boundary, body, header) header['Content-ID'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
self self
end end
# Append arbitrary data as a part # Append arbitrary data as a part
# #
# @param [Google::Apis::Core::UploadIO] upload_io # @param [IO] upload_io
# IO stream # IO stream
# @param [String] content_id # @param [String] content_id
# Optional unique ID of this part # Optional unique ID of this part
# @return [self] # @return [self]
def add_upload(upload_io, content_id: nil) def add_upload(upload_io, content_type: nil, content_id: nil)
header = { :content_id => content_id } header = {
@parts << Google::Apis::Core::FilePart.new(@boundary, 'Content-Type' => content_type || 'application/octet-stream'
upload_io, }
header) header['Content-Id'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::FilePart.new(upload_io,
header).to_io(@boundary)
self self
end end
@ -172,14 +126,8 @@ Content-Transfer-Encoding: %s\r
# @return [IO] # @return [IO]
# IO stream # IO stream
def assemble def assemble
@parts << Hurley::Multipart::EpiloguePart.new(@boundary) @parts << StringIO.new("--#{@boundary}--\r\n\r\n")
ios = [] Google::Apis::Core::CompositeIO.new(*@parts)
len = 0
@parts.each do |part|
len += part.length
ios << part.to_io
end
Hurley::CompositeReadIO.new(len, *ios)
end end
end end
end end

View File

@ -29,41 +29,6 @@ end
module Google module Google
module Apis module Apis
module Core module Core
# Extension of Hurley's UploadIO to add length accessor
class UploadIO < Hurley::UploadIO
OCTET_STREAM_CONTENT_TYPE = 'application/octet-stream'
# Get the length of the stream
# @return [Fixnum]
def length
io.respond_to?(:length) ? io.length : File.size(local_path)
end
# Create a new instance given a file path
# @param [String, File] file_name
# Path to file
# @param [String] content_type
# Optional content type. If nil, will attempt to auto-detect
# @return [Google::Apis::Core::UploadIO]
def self.from_file(file_name, content_type: nil)
if content_type.nil?
type = MIME::Types.of(file_name)
content_type = type.first.content_type unless type.nil? || type.empty?
end
new(file_name, content_type || OCTET_STREAM_CONTENT_TYPE)
end
# Wraps an IO stream in UploadIO
# @param [#read] io
# IO to wrap
# @param [String] content_type
# Optional content type.
# @return [Google::Apis::Core::UploadIO]
def self.from_io(io, content_type: OCTET_STREAM_CONTENT_TYPE)
new(io, content_type)
end
end
# Base upload command. Not intended to be used directly # Base upload command. Not intended to be used directly
# @private # @private
class BaseUploadCommand < ApiCommand class BaseUploadCommand < ApiCommand
@ -83,17 +48,17 @@ module Google
# @return [Google::Apis::Core::UploadIO] # @return [Google::Apis::Core::UploadIO]
attr_accessor :upload_io attr_accessor :upload_io
# Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance. # Ensure the content is readable and wrapped in an IO instance.
# #
# @return [void] # @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid # @raise [Google::Apis::ClientError] if upload source is invalid
def prepare! def prepare!
super super
if streamable?(upload_source) if streamable?(upload_source)
self.upload_io = UploadIO.from_io(upload_source, content_type: upload_content_type) self.upload_io = upload_source
@close_io_on_finish = false @close_io_on_finish = false
elsif upload_source.is_a?(String) elsif upload_source.is_a?(String)
self.upload_io = UploadIO.from_file(upload_source, content_type: upload_content_type) self.upload_io = File.new(upload_source, 'r')
@close_io_on_finish = true @close_io_on_finish = true
else else
fail Google::Apis::ClientError, 'Invalid upload source' fail Google::Apis::ClientError, 'Invalid upload source'
@ -124,13 +89,12 @@ module Google
super super
self.body = upload_io self.body = upload_io
header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
end end
end end
# Implementation of the multipart upload protocol # Implementation of the multipart upload protocol
class MultipartUploadCommand < BaseUploadCommand class MultipartUploadCommand < BaseUploadCommand
UPLOAD_BOUNDARY = 'RubyApiClientUpload'
MULTIPART_PROTOCOL = 'multipart' MULTIPART_PROTOCOL = 'multipart'
MULTIPART_RELATED = 'multipart/related' MULTIPART_RELATED = 'multipart/related'
@ -140,11 +104,11 @@ module Google
# @raise [Google::Apis::ClientError] if upload source is invalid # @raise [Google::Apis::ClientError] if upload source is invalid
def prepare! def prepare!
super super
@multipart = Multipart.new(boundary: UPLOAD_BOUNDARY, content_type: MULTIPART_RELATED) multipart = Multipart.new
@multipart.add_json(body) multipart.add_json(body)
@multipart.add_upload(upload_io) multipart.add_upload(upload_io, content_type: upload_content_type)
self.body = @multipart.assemble self.body = multipart.assemble
header[:content_type] = @multipart.content_type header['Content-Type'] = multipart.content_type
header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
end end
end end
@ -179,7 +143,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [HTTP::Message::Headers] header
# Response headers # Response headers
# @param [String, #read] body # @param [String, #read] body
# Response body # Response body
@ -189,9 +153,9 @@ module Google
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body) def process_response(status, header, body)
@offset = Integer(header[BYTES_RECEIVED_HEADER]) if header.key?(BYTES_RECEIVED_HEADER) @offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
@upload_url = header[UPLOAD_URL_HEADER] if header.key?(UPLOAD_URL_HEADER) @upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
upload_status = header[UPLOAD_STATUS_HEADER] upload_status = header[UPLOAD_STATUS_HEADER].first
logger.debug { sprintf('Upload status %s', upload_status) } logger.debug { sprintf('Upload status %s', upload_status) }
if upload_status == STATUS_ACTIVE if upload_status == STATUS_ACTIVE
@state = :active @state = :active
@ -204,61 +168,68 @@ module Google
super(status, header, body) super(status, header, body)
end end
# Send the start command to initiate the upload
#
# @param [Hurley::Client] client
# HTTP client
# @return [Hurley::Response]
# @raise [Google::Apis::ServerError] Unable to send the request
def send_start_command(client) def send_start_command(client)
logger.debug { sprintf('Sending upload start command to %s', url) } logger.debug { sprintf('Sending upload start command to %s', url) }
client.send(method, url, body) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE apply_request_options(request_header)
req.header[UPLOAD_COMMAND_HEADER] = START_COMMAND request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
req.header[UPLOAD_CONTENT_LENGTH] = upload_io.length.to_s request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
req.header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
end 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)
rescue => e rescue => e
raise Google::Apis::ServerError, e.message raise Google::Apis::ServerError, e.message
end end
# Query for the status of an incomplete upload # Query for the status of an incomplete upload
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Hurley::Response] # @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request # @raise [Google::Apis::ServerError] Unable to send the request
def send_query_command(client) def send_query_command(client)
logger.debug { sprintf('Sending upload query command to %s', @upload_url) } logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
client.post(@upload_url, nil) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND apply_request_options(request_header)
end request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
client.post(@upload_url, header: request_header, follow_redirect: true)
end end
# Send the actual content # Send the actual content
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Hurley::Response] # @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request # @raise [Google::Apis::ServerError] Unable to send the request
def send_upload_command(client) def send_upload_command(client)
logger.debug { sprintf('Sending upload command to %s', @upload_url) } logger.debug { sprintf('Sending upload command to %s', @upload_url) }
content = upload_io content = upload_io
content.pos = @offset content.pos = @offset
client.post(@upload_url, content) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND apply_request_options(request_header)
req.header[UPLOAD_OFFSET_HEADER] = @offset.to_s request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
end request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
end end
# Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query # 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. # for the status of the upload, the second to send the (remaining) content.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/api_command' require 'google/apis/core/api_command'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
RSpec.describe Google::Apis::Core::HttpCommand do RSpec.describe Google::Apis::Core::HttpCommand do
include TestHelpers include TestHelpers

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/batch' require 'google/apis/core/batch'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
RSpec.describe Google::Apis::Core::BatchCommand do RSpec.describe Google::Apis::Core::BatchCommand do
include TestHelpers include TestHelpers
@ -30,19 +29,20 @@ RSpec.describe Google::Apis::Core::BatchCommand do
let(:post_with_string_command) do let(:post_with_string_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2') command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2')
command.body = 'Hello world' command.body = 'Hello world'
command.header[:content_type] = 'text/plain' command.header['Content-Type'] = 'text/plain'
command command
end end
let(:post_with_io_command) do let(:post_with_io_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3') command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3')
command.body = StringIO.new('Goodbye!') command.body = StringIO.new('Goodbye!')
command.header[:content_type] = 'text/plain' command.header['Content-Type'] = 'text/plain'
command command
end end
before(:example) do before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f') allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f')
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
response = <<EOF response = <<EOF
--batch123 --batch123
@ -83,20 +83,20 @@ EOF
command.execute(client) command.execute(client)
expected_body = <<EOF.gsub(/\n/, "\r\n") expected_body = <<EOF.gsub(/\n/, "\r\n")
--RubyApiBatchRequest --123abc
Content-Length: 58
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Length: 58
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
GET /zoo/animals/1? HTTP/1.1 GET /zoo/animals/1? HTTP/1.1
Host: www.googleapis.com Host: www.googleapis.com
--RubyApiBatchRequest --123abc
Content-Length: 96
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Length: 96
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
POST /zoo/animals/2? HTTP/1.1 POST /zoo/animals/2? HTTP/1.1
@ -104,10 +104,10 @@ Content-Type: text/plain
Host: www.googleapis.com Host: www.googleapis.com
Hello world Hello world
--RubyApiBatchRequest --123abc
Content-Length: 93
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Length: 93
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
POST /zoo/animals/3? HTTP/1.1 POST /zoo/animals/3? HTTP/1.1
@ -115,7 +115,7 @@ Content-Type: text/plain
Host: www.googleapis.com Host: www.googleapis.com
Goodbye! Goodbye!
--RubyApiBatchRequest-- --123abc--
EOF EOF
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made

View File

@ -0,0 +1,76 @@
# 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 'spec_helper'
require 'google/apis/core/composite_io'
RSpec.describe Google::Apis::Core::CompositeIO do
shared_examples 'should act like IO' do
it 'should read from all IOs' do
expect(io.read).to eq 'Hello Cruel World'
end
it 'should respond to size' do
expect(io.size).to eq 17
end
it 'should respond to pos=' do
io.pos = 6
expect(io.read).to eq('Cruel World')
end
it 'should reject negative positions' do
expect { io.pos = -1 }.to raise_error(ArgumentError)
end
it 'should return nil if position beyond size' do
io.pos = 20
expect(io.read).to be_nil
end
it 'should be readable after rewinding' do
expect(io.read).to eq 'Hello Cruel World'
expect(io.read).to be_nil
io.rewind
expect(io.read).to eq 'Hello Cruel World'
end
end
context 'with StringIOs' do
let(:io) do
Google::Apis::Core::CompositeIO.new(
StringIO.new("Hello "),
StringIO.new("Cruel "),
StringIO.new("World"))
end
include_examples 'should act like IO'
end
context 'with Files' do
let(:io) do
files = []
dir = Dir.mktmpdir
['Hello ', 'Cruel ', 'World'].each_with_index do |text, index|
name = File.join(dir, "f#{index}")
File.open(name, 'w') { |f| f.write(text) }
files << name
end
Google::Apis::Core::CompositeIO.new(files.map { |name| File.open(name, 'r') })
end
include_examples 'should act like IO'
end
end

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
require 'tempfile' require 'tempfile'
require 'tmpdir' require 'tmpdir'

View File

@ -14,7 +14,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/http_command' require 'google/apis/core/http_command'
require 'hurley/test'
RSpec.describe Google::Apis::Core::HttpCommand do RSpec.describe Google::Apis::Core::HttpCommand do
include TestHelpers include TestHelpers

View File

@ -16,8 +16,6 @@ require 'spec_helper'
require 'google/apis/options' require 'google/apis/options'
require 'google/apis/core/base_service' require 'google/apis/core/base_service'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
require 'ostruct'
RSpec.describe Google::Apis::Core::BaseService do RSpec.describe Google::Apis::Core::BaseService do
include TestHelpers include TestHelpers
@ -190,6 +188,8 @@ EOF
context 'with batch uploads' do context 'with batch uploads' do
before(:example) do before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d')
allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner')
response = <<EOF.gsub(/\n/, "\r\n") response = <<EOF.gsub(/\n/, "\r\n")
--batch123 --batch123
Content-Type: application/http Content-Type: application/http
@ -227,6 +227,44 @@ EOF
end.to yield_with_args('Hello', nil) end.to yield_with_args('Hello', nil)
end end
it 'should send nested multipart' do
service.batch_upload do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
command.upload_source = StringIO.new('test')
command.upload_content_type = 'text/plain'
service.send(:execute_or_queue_command, command)
end
expected_body = <<EOF.gsub(/\n/, "\r\n")
--outer
Content-Type: application/http
Content-Id: <b1981e17-f622-49af-b2eb-203308b1b17d+0>
Content-Length: 303
Content-Transfer-Encoding: binary
POST /upload/zoo/animals? HTTP/1.1
Content-Type: multipart/related; boundary=inner
X-Goog-Upload-Protocol: multipart
Host: www.googleapis.com
--inner
Content-Type: application/json
--inner
Content-Type: text/plain
Content-Length: 4
Content-Transfer-Encoding: binary
test
--inner--
--outer--
EOF
expect(a_request(:put, 'https://www.googleapis.com/upload/').with(body: expected_body)).to have_been_made
end
it 'should disallow downloads in batch' do it 'should disallow downloads in batch' do
expect do |b| expect do |b|
service.batch_upload do |service| service.batch_upload do |service|

View File

@ -15,70 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
# TODO: JSON Response decoding
# TODO: Upload from IO
# TODO: Upload from file
RSpec.describe Google::Apis::Core::UploadIO do
context 'from_file' do
let(:upload_io) { Google::Apis::Core::UploadIO.from_file(file) }
context 'with text file' do
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.txt') }
it 'should infer content type from file' do
expect(upload_io.content_type).to eql('text/plain')
end
it 'should allow overriding the mime type' do
io = Google::Apis::Core::UploadIO.from_file(file, content_type: 'application/json')
expect(io.content_type).to eql('application/json')
end
end
context 'with unknown type' do
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.blah') }
it 'should use the default mime type' do
expect(upload_io.content_type).to eql('application/octet-stream')
end
it 'should allow overriding the mime type' do
io = Google::Apis::Core::UploadIO.from_file(file, content_type: 'application/json')
expect(io.content_type).to eql('application/json')
end
it 'should setup length of the stream' do
upload_io = Google::Apis::Core::UploadIO.from_file(file)
expect(upload_io.length).to eq File.size(file)
end
end
end
context 'from_io' do
context 'with i/o stream' do
let(:io) { StringIO.new 'Hello google' }
it 'should setup default content-type' do
upload_io = Google::Apis::Core::UploadIO.from_io(io)
expect(upload_io.content_type).to eql Google::Apis::Core::UploadIO::OCTET_STREAM_CONTENT_TYPE
end
it 'should allow overring the mime type' do
upload_io = Google::Apis::Core::UploadIO.from_io(io, content_type: 'application/x-gzip')
expect(upload_io.content_type).to eq('application/x-gzip')
end
it 'should setup length of the stream' do
upload_io = Google::Apis::Core::UploadIO.from_io(io)
expect(upload_io.length).to eq 'Hello google'.length
end
end
end
end
RSpec.describe Google::Apis::Core::RawUploadCommand do RSpec.describe Google::Apis::Core::RawUploadCommand do
include TestHelpers include TestHelpers
@ -170,21 +106,22 @@ RSpec.describe Google::Apis::Core::MultipartUploadCommand do
before(:example) do before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world)) stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
end end
it 'should send content' do it 'should send content' do
expected_body = <<EOF.gsub(/\n/, "\r\n") expected_body = <<EOF.gsub(/\n/, "\r\n")
--RubyApiClientUpload --123abc
Content-Type: application/json Content-Type: application/json
metadata metadata
--RubyApiClientUpload --123abc
Content-Length: 11
Content-Type: text/plain Content-Type: text/plain
Content-Length: 11
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
Hello world Hello world
--RubyApiClientUpload-- --123abc--
EOF EOF
command.execute(client) command.execute(client)

View File

@ -1,103 +0,0 @@
#
# Copyright (c) 2015 Rick Olson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
require 'hurley'
require 'hurley/client'
require 'hurley/connection'
require 'net/https'
# Temporary monkey patch for streaming downloads. These are fixed in HEAD,
# but pending a 0.3 release.
if Hurley::VERSION == '0.2'
module Hurley
class Response
def location
@location ||= begin
return unless loc = @header[:location]
verb = STATUS_FORCE_GET.include?(status_code) ? :get : request.verb
statuses, receiver = request.send(:body_receiver)
new_request = Request.new(verb, request.url.join(Url.parse(loc)), request.header, request.body, request.options, request.ssl_options)
new_request.on_body(*statuses, &receiver) unless receiver.is_a?(Hurley::BodyReceiver)
new_request
end
end
end
class Connection
def call(request)
net_http_connection(request) do |http|
begin
Response.new(request) do |res|
perform_request(http, request, res)
# net/http only raises exception on 407 with ssl...?
if res.status_code == 407
raise ConnectionFailed, %(407 "Proxy Authentication Required")
end
end
rescue *NET_HTTP_EXCEPTIONS => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise SSLError, err
else
raise ConnectionFailed, err
end
end
end
rescue ::Timeout::Error => err
raise Timeout, err
end
private
def net_http_request(request)
http_req = Net::HTTPGenericRequest.new(
request.verb.to_s.upcase, # request method
!!request.body, # is there a request body
:head != request.verb, # is there a response body
request.url.request_uri, # request uri path
request.header, # request headers
)
if body = request.body_io
http_req.body_stream = body
end
http_req
end
def perform_request(http, request, res)
http.request(net_http_request(request)) do |http_res|
res.status_code = http_res.code.to_i
http_res.each_header do |key, value|
res.header[key] = value
end
if :get == request.verb
http_res.read_body { |chunk| res.receive_body(chunk) }
else
res.receive_body(http_res.body)
end
end
end
end
end
end