chore: Split google-apis-core into a separate gem (#2134)

This commit is contained in:
Daniel Azuma authored and GitHub committed 2020-12-27 17:31:47 -08:00
1 parent 1bc1737dc1
commit a6a5b6071e
66 files changed
+763 -151

No files matched your search

@@ -1,143 +0,0 @@
# Copyright 2010 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 'webrick'
launchy_available =
begin
require 'launchy'
true
rescue LoadError
warn "Attempted to require google/api_client/auth/installed_app.rb when" \
" launchy is not available. The InstalledAppFlow class is disabled."
false
end
module Google
class APIClient
# Small helper for the sample apps for performing OAuth 2.0 flows from the command
# line or in any other installed app environment.
#
# This class is used in some sample apps and tests but is not really part
# of the client libraries, and probably does not belong here. As such, it
# is deprecated. If you do choose to use it, note that you must include the
# `launchy` gem in your bundle, as it is required by this class but not
# listed in the google-api-client gem's requirements.
#
# @example
#
# flow = Google::APIClient::InstalledAppFlow.new(
# :client_id => '691380668085.apps.googleusercontent.com',
# :client_secret => '...',
# :scope => 'https://www.googleapis.com/auth/drive'
# )
# authorization = flow.authorize
# Drive = Google::Apis::DriveV2
# drive = Drive::DriveService.new
# drive.authorization = authorization
#
# @deprecated Use google-auth-library-ruby instead
class InstalledAppFlow
RESPONSE_BODY = <<-HTML
<html>
<head>
<script>
function closeWindow() {
window.open('', '_self', '');
window.close();
}
setTimeout(closeWindow, 10);
</script>
</head>
<body>You may close this window.</body>
</html>
HTML
##
# Configure the flow
#
# @param [Hash] options The configuration parameters for the client.
# @option options [Fixnum] :port
# Port to run the embedded server on. Defaults to 9292
# @option options [String] :client_id
# A unique identifier issued to the client to identify itself to the
# authorization server.
# @option options [String] :client_secret
# A shared symmetric secret issued by the authorization server,
# which is used to authenticate the client.
# @option options [String] :scope
# The scope of the access request, expressed either as an Array
# or as a space-delimited String.
#
# @see Signet::OAuth2::Client
def initialize(options)
@port = options[:port] || 9292
@authorization = Signet::OAuth2::Client.new({
:authorization_uri => 'https://accounts.google.com/o/oauth2/auth',
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:redirect_uri => "http://localhost:#{@port}/"}.update(options)
)
end
##
# Request authorization. Opens a browser and waits for response.
#
# @param [Google::APIClient::Storage] storage
# Optional object that responds to :write_credentials, used to serialize
# the OAuth 2 credentials after completing the flow.
#
# @return [Signet::OAuth2::Client]
# Authorization instance, nil if user cancelled.
def authorize(storage=nil, options={})
auth = @authorization
server = WEBrick::HTTPServer.new(
:Port => @port,
:BindAddress =>"localhost",
:Logger => WEBrick::Log.new(STDOUT, 0),
:AccessLog => []
)
begin
trap("INT") { server.shutdown }
server.mount_proc '/' do |req, res|
auth.code = req.query['code']
if auth.code
auth.fetch_access_token!
end
res.status = WEBrick::HTTPStatus::RC_ACCEPTED
res.body = RESPONSE_BODY
server.stop
end
Launchy.open(auth.authorization_uri(options).to_s)
server.start
ensure
server.shutdown
end
if @authorization.access_token
if storage.respond_to?(:write_credentials)
storage.write_credentials(@authorization)
end
return @authorization
else
return nil
end
end
end
end
end if launchy_available
@@ -1,94 +0,0 @@
# Copyright 2010 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.
module Google
class APIClient
##
# Helper for loading keys from the PKCS12 files downloaded when
# setting up service accounts at the APIs Console.
#
# @deprecated Use google-auth-library-ruby instead
module KeyUtils
##
# Loads a key from PKCS12 file, assuming a single private key
# is present.
#
# @param [String] keyfile
# Path of the PKCS12 file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_from_pkcs12(keyfile, passphrase)
load_key(keyfile, passphrase) do |content, pass_phrase|
OpenSSL::PKCS12.new(content, pass_phrase).key
end
end
##
# Loads a key from a PEM file.
#
# @param [String] keyfile
# Path of the PEM file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
#
def self.load_from_pem(keyfile, passphrase)
load_key(keyfile, passphrase) do | content, pass_phrase|
OpenSSL::PKey::RSA.new(content, pass_phrase)
end
end
private
##
# Helper for loading keys from file or memory. Accepts a block
# to handle the specific file format.
#
# @param [String] keyfile
# Path of thefile to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @yield [String, String]
# Key file & passphrase to extract key from
# @yieldparam [String] keyfile
# Contents of the file
# @yieldparam [String] passphrase
# Passphrase to unlock key
# @yieldreturn [OpenSSL::PKey]
# Private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_key(keyfile, passphrase, &block)
begin
begin
content = File.open(keyfile, 'rb') { |io| io.read }
rescue
content = keyfile
end
block.call(content, passphrase)
rescue OpenSSL::OpenSSLError
raise ArgumentError.new("Invalid keyfile or passphrase")
end
end
end
end
end
@@ -1,104 +0,0 @@
# Copyright 2013 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 'signet/oauth_2/client'
module Google
class APIClient
##
# Represents cached OAuth 2 tokens stored on local disk in a
# JSON serialized file. Meant to resemble the serialized format
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
#
# @deprecated Use google-auth-library-ruby instead
class Storage
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'
TOKEN_CREDENTIAL_URI = 'https://accounts.google.com/o/oauth2/token'
# @return [Object] Storage object.
attr_accessor :store
# @return [Signet::OAuth2::Client]
attr_reader :authorization
##
# Initializes the Storage object.
#
# @param [Object] store
# Storage object
def initialize(store)
@store= store
@authorization = nil
end
##
# Write the credentials to the specified store.
#
# @param [Signet::OAuth2::Client] authorization
# Optional authorization instance. If not provided, the authorization
# already associated with this instance will be written.
def write_credentials(authorization=nil)
@authorization = authorization if authorization
if @authorization.respond_to?(:refresh_token) && @authorization.refresh_token
store.write_credentials(credentials_hash)
end
end
##
# Loads credentials and authorizes an client.
# @return [Object] Signet::OAuth2::Client or NIL
def authorize
@authorization = nil
cached_credentials = load_credentials
if cached_credentials && cached_credentials.size > 0
@authorization = Signet::OAuth2::Client.new(cached_credentials)
@authorization.issued_at = Time.at(cached_credentials['issued_at'].to_i)
self.refresh_authorization if @authorization.expired?
end
return @authorization
end
##
# refresh credentials and save them to store
def refresh_authorization
authorization.refresh!
self.write_credentials
end
private
##
# Attempt to read in credentials from the specified store.
def load_credentials
store.load_credentials
end
##
# @return [Hash] with credentials
def credentials_hash
{
:access_token => authorization.access_token,
:authorization_uri => AUTHORIZATION_URI,
:client_id => authorization.client_id,
:client_secret => authorization.client_secret,
:expires_in => authorization.expires_in,
:refresh_token => authorization.refresh_token,
:token_credential_uri => TOKEN_CREDENTIAL_URI,
:issued_at => authorization.issued_at.to_i
}
end
end
end
end
@@ -1,57 +0,0 @@
# Copyright 2013 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 'json'
module Google
class APIClient
##
# Represents cached OAuth 2 tokens stored on local disk in a
# JSON serialized file. Meant to resemble the serialized format
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
#
# @deprecated Use google-auth-library-ruby instead
class FileStore
attr_accessor :path
##
# Initializes the FileStorage object.
#
# @param [String] path
# Path to the credentials file.
def initialize(path)
@path= path
end
##
# Attempt to read in credentials from the specified file.
def load_credentials
open(path, 'r') { |f| JSON.parse(f.read) }
rescue
nil
end
##
# Write the credentials to the specified file.
#
# @param [Hash] credentials_hash
def write_credentials(credentials_hash)
open(self.path, 'w+') do |f|
f.write(credentials_hash.to_json)
end
end
end
end
end
@@ -1,59 +0,0 @@
# Copyright 2013 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 'json'
module Google
class APIClient
# @deprecated Use google-auth-library-ruby instead
class RedisStore
DEFAULT_REDIS_CREDENTIALS_KEY = "google_api_credentials"
attr_accessor :redis
##
# Initializes the RedisStore object.
#
# @param [Object] redis
# Redis instance
# @param [Object] key
# Optional key to store credentials under. Defaults to 'google_api_credentials'
def initialize(redis, key = nil)
@redis= redis
@redis_credentials_key = key
end
##
# Attempt to read in credentials from redis.
# @return [Hash]
def load_credentials
credentials = redis.get redis_credentials_key
JSON.parse(credentials) if credentials
end
def redis_credentials_key
@redis_credentials_key || DEFAULT_REDIS_CREDENTIALS_KEY
end
##
# Write the credentials to redis.
#
# @param [Hash] credentials_hash
def write_credentials(credentials_hash)
redis.set(redis_credentials_key, credentials_hash.to_json)
end
end
end
end
@@ -1,176 +0,0 @@
# Copyright 2010 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 'json'
require 'googleauth'
module Google
class APIClient
##
# Manages the persistence of client configuration data and secrets. Format
# inspired by the Google API Python client.
#
# @see https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
# @deprecated Use google-auth-library-ruby instead
# @example
# {
# "web": {
# "client_id": "asdfjasdljfasdkjf",
# "client_secret": "1912308409123890",
# "redirect_uris": ["https://www.example.com/oauth2callback"],
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
# "token_uri": "https://accounts.google.com/o/oauth2/token"
# }
# }
#
# @example
# {
# "installed": {
# "client_id": "837647042410-75ifg...usercontent.com",
# "client_secret":"asdlkfjaskd",
# "redirect_uris": ["http://localhost", "urn:ietf:oauth:2.0:oob"],
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
# "token_uri": "https://accounts.google.com/o/oauth2/token"
# }
# }
class ClientSecrets
##
# Reads client configuration from a file
#
# @param [String] filename
# Path to file to load
#
# @return [Google::APIClient::ClientSecrets]
# OAuth client settings
def self.load(filename=nil)
if filename && File.directory?(filename)
search_path = File.expand_path(filename)
filename = nil
end
while filename == nil
search_path ||= File.expand_path('.')
if File.exists?(File.join(search_path, 'client_secrets.json'))
filename = File.join(search_path, 'client_secrets.json')
elsif search_path == File.expand_path('..', search_path)
raise ArgumentError,
'No client_secrets.json filename supplied ' +
'and/or could not be found in search path.'
else
search_path = File.expand_path(File.join(search_path, '..'))
end
end
data = File.open(filename, 'r') { |file| JSON.load(file.read) }
return self.new(data)
end
##
# Initialize OAuth client settings.
#
# @param [Hash] options
# Parsed client secrets files
def initialize(options={})
# Client auth configuration
@flow = options[:flow] || options.keys.first.to_s || 'web'
fdata = options[@flow.to_sym] || options[@flow]
@client_id = fdata[:client_id] || fdata["client_id"]
@client_secret = fdata[:client_secret] || fdata["client_secret"]
@redirect_uris = fdata[:redirect_uris] || fdata["redirect_uris"]
@redirect_uris ||= [fdata[:redirect_uri] || fdata["redirect_uri"]].compact
@javascript_origins = (
fdata[:javascript_origins] ||
fdata["javascript_origins"]
)
@javascript_origins ||= [fdata[:javascript_origin] || fdata["javascript_origin"]].compact
@authorization_uri = fdata[:auth_uri] || fdata["auth_uri"]
@authorization_uri ||= fdata[:authorization_uri]
@token_credential_uri = fdata[:token_uri] || fdata["token_uri"]
@token_credential_uri ||= fdata[:token_credential_uri]
# Associated token info
@access_token = fdata[:access_token] || fdata["access_token"]
@refresh_token = fdata[:refresh_token] || fdata["refresh_token"]
@id_token = fdata[:id_token] || fdata["id_token"]
@expires_in = fdata[:expires_in] || fdata["expires_in"]
@expires_at = fdata[:expires_at] || fdata["expires_at"]
@issued_at = fdata[:issued_at] || fdata["issued_at"]
end
attr_reader(
:flow, :client_id, :client_secret, :redirect_uris, :javascript_origins,
:authorization_uri, :token_credential_uri, :access_token,
:refresh_token, :id_token, :expires_in, :expires_at, :issued_at
)
##
# Serialize back to the original JSON form
#
# @return [String]
# JSON
def to_json
return Json.dump(to_hash)
end
def to_hash
{
self.flow => ({
'client_id' => self.client_id,
'client_secret' => self.client_secret,
'redirect_uris' => self.redirect_uris,
'javascript_origins' => self.javascript_origins,
'auth_uri' => self.authorization_uri,
'token_uri' => self.token_credential_uri,
'access_token' => self.access_token,
'refresh_token' => self.refresh_token,
'id_token' => self.id_token,
'expires_in' => self.expires_in,
'expires_at' => self.expires_at,
'issued_at' => self.issued_at
}).inject({}) do |accu, (k, v)|
# Prunes empty values from JSON output.
unless v == nil || (v.respond_to?(:empty?) && v.empty?)
accu[k] = v
end
accu
end
}
end
def to_authorization
# NOTE: Do not rely on this default value, as it may change
new_authorization = Signet::OAuth2::Client.new
new_authorization.client_id = self.client_id
new_authorization.client_secret = self.client_secret
new_authorization.authorization_uri = (
self.authorization_uri ||
'https://accounts.google.com/o/oauth2/auth'
)
new_authorization.token_credential_uri = (
self.token_credential_uri ||
'https://accounts.google.com/o/oauth2/token'
)
new_authorization.redirect_uri = self.redirect_uris.first
# These are supported, but unlikely.
new_authorization.access_token = self.access_token
new_authorization.refresh_token = self.refresh_token
new_authorization.id_token = self.id_token
new_authorization.expires_in = self.expires_in
new_authorization.issued_at = self.issued_at if self.issued_at
new_authorization.expires_at = self.expires_at if self.expires_at
return new_authorization
end
end
end
end
-58
View File
@@ -1,58 +0,0 @@
# 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/version'
require 'logger'
module Google
module Apis
ROOT = File.expand_path('..', File.dirname(__dir__))
# @!attribute [rw] logger
# @return [Logger] The logger.
def self.logger
@logger ||= rails_logger || default_logger
end
class << self
attr_writer :logger
end
private
# Create and configure a logger
# @return [Logger]
def self.default_logger
logger = Logger.new($stdout)
logger.level = Logger::WARN
logger
end
# Check to see if client is being used in a Rails environment and get the logger if present.
# Setting the ENV variable 'GOOGLE_API_USE_RAILS_LOGGER' to false will force the client
# to use its own logger.
#
# @return [Logger]
def self.rails_logger
if 'true' == ENV.fetch('GOOGLE_API_USE_RAILS_LOGGER', 'true') &&
defined?(::Rails) &&
::Rails.respond_to?(:logger) &&
!::Rails.logger.nil?
::Rails.logger
else
nil
end
end
end
end
@@ -1,209 +0,0 @@
# 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 'addressable/uri'
require 'addressable/template'
require 'google/apis/core/http_command'
require 'google/apis/errors'
require 'json'
require 'retriable'
module Google
module Apis
module Core
# Command for executing most basic API request with JSON requests/responses
class ApiCommand < HttpCommand
JSON_CONTENT_TYPE = 'application/json'
FIELDS_PARAM = 'fields'
ERROR_REASON_MAPPING = {
'rateLimitExceeded' => Google::Apis::RateLimitError,
'userRateLimitExceeded' => Google::Apis::RateLimitError,
'projectNotLinked' => Google::Apis::ProjectNotLinkedError
}
# JSON serializer for request objects
# @return [Google::Apis::Core::JsonRepresentation]
attr_accessor :request_representation
# Request body to serialize
# @return [Object]
attr_accessor :request_object
# JSON serializer for response objects
# @return [Google::Apis::Core::JsonRepresentation]
attr_accessor :response_representation
# Class to instantiate when de-serializing responses
# @return [Object]
attr_accessor :response_class
# Serialize the request body
#
# @return [void]
def prepare!
set_api_client_header
set_user_project_header
if options&.api_format_version
header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s
end
query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
if request_representation && request_object
header['Content-Type'] ||= JSON_CONTENT_TYPE
if options && options.skip_serialization
self.body = request_object
else
self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
end
end
super
end
# Deserialize the response body if present
#
# @param [String] content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
# noinspection RubyUnusedLocalVariable
def decode_response_body(content_type, body)
return super unless response_representation
return super if options && options.skip_deserialization
return super if content_type.nil?
return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
body = "{}" if body.empty?
instance = response_class.new
response_representation.new(instance).from_json(body, unwrap: response_class)
instance
end
# Check the response and raise error if needed
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# HTTP response headers
# @param [String] body
# HTTP response body
# @param [String] message
# Error message text
# @return [void]
# @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 check_status(status, header = nil, body = nil, message = nil)
case status
when 400, 402...500
reason, message = parse_error(body)
if reason
message = sprintf('%s: %s', reason, message)
raise ERROR_REASON_MAPPING[reason].new(
message,
status_code: status,
header: header,
body: body
) if ERROR_REASON_MAPPING.key?(reason)
end
super(status, header, body, message)
else
super(status, header, body, message)
end
end
def allow_form_encoding?
request_representation.nil? && super
end
private
def set_api_client_header
old_xgac = header
.find_all { |k, v| k.downcase == 'x-goog-api-client' }
.map { |(a, b)| b }
.join(' ')
.split
.find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
.join(' ')
xgac = "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}"
xgac = old_xgac.empty? ? xgac : "#{old_xgac} #{xgac}"
header.delete_if { |k, v| k.downcase == 'x-goog-api-client' }
header['X-Goog-Api-Client'] = xgac
end
def set_user_project_header
quota_project_id = options.quota_project
if !quota_project_id && options&.authorization.respond_to?(:quota_project_id)
quota_project_id = options.authorization.quota_project_id
end
header['X-Goog-User-Project'] = quota_project_id if quota_project_id
end
# Attempt to parse a JSON error message
# @param [String] body
# HTTP response body
# @return [Array<(String, String)>]
# Error reason and message
def parse_error(body)
obj = JSON.load(body)
error = obj['error']
if error['details']
return extract_v2_error_details(error)
elsif error['errors']
return extract_v1_error_details(error)
else
fail 'Can not parse error message. No "details" or "errors" detected'
end
rescue
return [nil, nil]
end
# Extracts details from a v1 error message
# @param [Hash] error
# Parsed JSON
# @return [Array<(String, String)>]
# Error reason and message
def extract_v1_error_details(error)
reason = error['errors'].first['reason']
message = error['message']
return [reason, message]
end
# Extracts details from a v2error message
# @param [Hash] error
# Parsed JSON
# @return [Array<(String, String)>]
# Error reason and message
def extract_v2_error_details(error)
reason = error['status']
message = error['message']
return [reason, message]
end
# Convert field names from ruby conventions to original names in JSON
#
# @param [String] fields
# Value of 'fields' param
# @return [String]
# Updated header value
def normalize_fields_param(fields)
# TODO: Generate map of parameter names during code gen. Small possibility that camelization fails
fields.gsub(/:/, '').gsub(/\w+/) do |str|
str.gsub(/(?:^|_)([a-z])/){ Regexp.last_match.begin(0) == 0 ? $1 : $1.upcase }
end
end
end
end
end
end
@@ -1,447 +0,0 @@
# 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 'addressable/uri'
require 'addressable/template'
require 'google/apis/version'
require 'google/apis/core/api_command'
require 'google/apis/core/batch'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'google/apis/options'
require 'googleauth'
require 'httpclient'
module Google
module Apis
module Core
# Helper class for enumerating over a result set requiring multiple fetches
class PagedResults
include Enumerable
attr_reader :last_result
# @param [BaseService] service
# Current service instance
# @param [Fixnum] max
# Maximum number of items to iterate over. Nil if no limit
# @param [Boolean] cache
# True (default) if results should be cached so multiple iterations can be used.
# @param [Symbol] items
# Name of the field in the result containing the items. Defaults to :items
def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
@service = service
@block = block
@max = max
@items_field = items
@response_page_token_field = response_page_token
if cache
@result_cache = Hash.new do |h, k|
h[k] = @block.call(k, @service)
end
@fetch_proc = Proc.new { |token| @result_cache[token] }
else
@fetch_proc = Proc.new { |token| @block.call(token, @service) }
end
end
# Iterates over result set, fetching additional pages as needed
def each
page_token = nil
item_count = 0
loop do
@last_result = @fetch_proc.call(page_token)
items = @last_result.send(@items_field)
if items.kind_of?(Array)
for item in items
item_count = item_count + 1
break if @max && item_count > @max
yield item
end
elsif items.kind_of?(Hash)
items.each do |key, val|
item_count = item_count + 1
break if @max && item_count > @max
yield key, val
end
elsif items
# yield singular non-nil items (for genomics API)
yield items
end
break if @max && item_count >= @max
next_page_token = @last_result.send(@response_page_token_field)
break if next_page_token.nil? || next_page_token == page_token
page_token = next_page_token
end
end
end
# Base service for all APIs. Not to be used directly.
#
class BaseService
include Logging
# Root URL (host/port) for the API
# @return [Addressable::URI]
attr_accessor :root_url
# Additional path prefix for all API methods
# @return [Addressable::URI]
attr_accessor :base_path
# Alternate path prefix for media uploads
# @return [Addressable::URI]
attr_accessor :upload_path
# Alternate path prefix for all batch methods
# @return [Addressable::URI]
attr_accessor :batch_path
# HTTP client
# @return [HTTPClient]
attr_accessor :client
# General settings
# @return [Google::Apis::ClientOptions]
attr_accessor :client_options
# Default options for all requests
# @return [Google::Apis::RequestOptions]
attr_accessor :request_options
# @param [String,Addressable::URI] root_url
# Root URL for the API
# @param [String,Addressable::URI] base_path
# Additional path prefix for all API methods
# @api private
def initialize(root_url, base_path)
self.root_url = root_url
self.base_path = base_path
self.upload_path = "upload/#{base_path}"
self.batch_path = 'batch'
self.client_options = Google::Apis::ClientOptions.default.dup
self.request_options = Google::Apis::RequestOptions.default.dup
end
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client]
# OAuth2 credentials
def authorization=(authorization)
request_options.authorization = authorization
end
def authorization
request_options.authorization
end
# TODO: with(options) method
# Perform a batch request. Calls made within the block are sent in a single network
# request to the server.
#
# @example
# service.batch do |s|
# s.get_item(id1) do |res, err|
# # process response for 1st call
# end
# # ...
# s.get_item(idN) do |res, err|
# # process response for Nth call
# end
# end
#
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @yield [self]
# @return [void]
def batch(options = nil)
batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
batch_command.options = request_options.merge(options)
apply_command_defaults(batch_command)
begin
start_batch(batch_command)
yield self
ensure
end_batch
end
batch_command.execute(client)
end
# Perform a batch upload request. Calls made within the block are sent in a single network
# request to the server. Batch uploads are useful for uploading multiple small files. For larger
# files, use single requests which use a resumable upload protocol.
#
# @example
# service.batch do |s|
# s.insert_item(upload_source: 'file1.txt') do |res, err|
# # process response for 1st call
# end
# # ...
# s.insert_item(upload_source: 'fileN.txt') do |res, err|
# # process response for Nth call
# end
# end
#
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @yield [self]
# @return [void]
def batch_upload(options = nil)
batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
batch_command.options = request_options.merge(options)
apply_command_defaults(batch_command)
begin
start_batch(batch_command)
yield self
ensure
end_batch
end
batch_command.execute(client)
end
# Get the current HTTP client
# @return [HTTPClient]
def client
@client ||= new_client
end
# Simple escape hatch for making API requests directly to a given
# URL. This is not intended to be used as a generic HTTP client
# and should be used only in cases where no service method exists
# (e.g. fetching an export link for a Google Drive file.)
#
# @param [Symbol] method
# HTTP method as symbol (e.g. :get, :post, :put, ...)
# @param [String] url
# URL to call
# @param [Hash<String,String>] params
# Optional hash of query parameters
# @param [#read] body
# Optional body for POST/PUT
# @param [IO, String] download_dest
# IO stream or filename to receive content download
# @param [Google::Apis::RequestOptions] options
# Request-specific options
#
# @yield [result, err] Result & error if block supplied
# @yieldparam result [String] HTTP response body
# @yieldparam err [StandardError] error object if request failed
#
# @return [String] HTTP response body
def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
if download_dest
command = DownloadCommand.new(method, url, body: body)
else
command = HttpCommand.new(method, url, body: body)
end
command.options = request_options.merge(options)
apply_command_defaults(command)
command.query.merge(Hash(params))
execute_or_queue_command(command, &block)
end
# Executes a given query with paging, automatically retrieving
# additional pages as necessary. Requires a block that returns the
# result set of a page. The current page token is supplied as an argument
# to the block.
#
# Note: The returned enumerable also contains a `last_result` field
# containing the full result of the last query executed.
#
# @param [Fixnum] max
# Maximum number of items to iterate over. Defaults to nil -- no upper bound.
# @param [Symbol] items
# Name of the field in the result containing the items. Defaults to :items
# @param [Boolean] cache
# True (default) if results should be cached so multiple iterations can be used.
# @return [Enumerble]
# @yield [token, service]
# Current page token & service instance
# @yieldparam [String] token
# Current page token to be used in the query
# @yieldparam [service]
# Current service instance
# @since 0.9.4
#
# @example Retrieve all files,
# file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
# file_list.each { |f| ... }
def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
fail "fetch_all may not be used inside a batch" if batch?
return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
end
protected
# Create a new upload command.
#
# @param [symbol] method
# HTTP method for uploading (typically :put or :post)
# @param [String] path
# Additional path to upload endpoint, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::UploadCommand]
def make_upload_command(method, path, options)
template = Addressable::Template.new(root_url + upload_path + path)
if batch?
command = MultipartUploadCommand.new(method, template)
else
command = ResumableUploadCommand.new(method, template)
end
command.options = request_options.merge(options)
apply_command_defaults(command)
command
end
# Create a new download command.
#
# @param [symbol] method
# HTTP method for uploading (typically :get)
# @param [String] path
# Additional path to download endpoint, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::DownloadCommand]
def make_download_command(method, path, options)
template = Addressable::Template.new(root_url + base_path + path)
command = DownloadCommand.new(method, template)
command.options = request_options.merge(options)
command.query['alt'] = 'media'
apply_command_defaults(command)
command
end
# Create a new command.
#
# @param [symbol] method
# HTTP method (:get, :post, :delete, etc...)
# @param [String] path
# Additional path, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::DownloadCommand]
def make_simple_command(method, path, options)
full_path =
if path.start_with? "/"
path[1..-1]
else
base_path + path
end
template = Addressable::Template.new(root_url + full_path)
command = ApiCommand.new(method, template)
command.options = request_options.merge(options)
apply_command_defaults(command)
command
end
# Execute the request. If a batch is in progress, the request is added to the batch instead.
#
# @param [Google::Apis::Core::HttpCommand] command
# Command to execute
# @return [Object] response object if command executed and no callback supplied
# @yield [result, err] Result & error if block supplied
# @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_or_queue_command(command, &callback)
batch_command = current_batch
if batch_command
fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
batch_command.add(command, &callback)
nil
else
command.execute(client, &callback)
end
end
# Update commands with service-specific options. To be implemented by subclasses
# @param [Google::Apis::Core::HttpCommand] _command
def apply_command_defaults(_command)
end
private
# Get the current batch context
#
# @return [Google:Apis::Core::BatchRequest]
def current_batch
Thread.current[:google_api_batch]
end
# Check if a batch is in progress
# @return [Boolean]
def batch?
!current_batch.nil?
end
# Start a new thread-local batch context
# @param [Google::Apis::Core::BatchCommand] cmd
def start_batch(cmd)
fail "Batch already in progress" if batch?
Thread.current[:google_api_batch] = cmd
Thread.current[:google_api_batch_service] = self
end
# Clear thread-local batch context
def end_batch
Thread.current[:google_api_batch] = nil
Thread.current[:google_api_batch_service] = nil
end
# Create a new HTTP client
# @return [HTTPClient]
def new_client
client = ::HTTPClient.new
if client_options.transparent_gzip_decompression
client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
end
client.proxy = client_options.proxy_url if client_options.proxy_url
if client_options.open_timeout_sec
client.connect_timeout = client_options.open_timeout_sec
end
if client_options.read_timeout_sec
client.receive_timeout = client_options.read_timeout_sec
end
if client_options.send_timeout_sec
client.send_timeout = client_options.send_timeout_sec
end
client.follow_redirect_count = 5
client.default_header = { 'User-Agent' => user_agent }
client.debug_dev = logger if client_options.log_http_requests
client
end
# Build the user agent header
# @return [String]
def user_agent
sprintf('%s/%s google-api-ruby-client/%s %s (gzip)',
client_options.application_name,
client_options.application_version,
Google::Apis::VERSION,
Google::Apis::OS_VERSION)
end
end
end
end
end
@@ -1,236 +0,0 @@
# 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
@@ -1,97 +0,0 @@
# 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
@@ -1,118 +0,0 @@
# 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/api_command'
require 'google/apis/errors'
require 'addressable/uri'
module Google
module Apis
module Core
# Streaming/resumable media download support
class DownloadCommand < ApiCommand
RANGE_HEADER = 'Range'
OK_STATUS = [200, 201, 206]
# File or IO to write content to
# @return [String, File, #write]
attr_accessor :download_dest
# Ensure the download destination is a writable stream.
#
# @return [void]
def prepare!
@state = :start
@download_url = nil
@offset = 0
if download_dest.respond_to?(:write)
@download_io = download_dest
@close_io_on_finish = false
elsif download_dest.is_a?(String)
@download_io = File.open(download_dest, 'wb')
@close_io_on_finish = true
else
@download_io = StringIO.new('', 'wb')
@close_io_on_finish = false
end
super
end
# Close IO stream when command done. Only closes the stream if it was opened by the command.
def release!
@download_io.close if @close_io_on_finish
end
# Execute the upload request once. Overrides the default implementation to handle streaming/chunking
# of file content.
#
# @private
# @param [HTTPClient] client
# 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)
request_header = header.dup
apply_request_options(request_header)
download_offset = nil
if @offset > 0
logger.debug { sprintf('Resuming download from offset %d', @offset) }
request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
end
http_res = client.get(url.to_s,
query: query,
header: request_header,
follow_redirect: true) do |res, chunk|
status = res.http_header.status_code.to_i
next unless OK_STATUS.include?(status)
download_offset ||= (status == 206 ? @offset : 0)
download_offset += chunk.bytesize
if download_offset - chunk.bytesize == @offset
next_chunk = chunk
else
# Oh no! Requested a chunk, but received the entire content
chunk_index = @offset - (download_offset - chunk.bytesize)
next_chunk = chunk.byteslice(chunk_index..-1)
next if next_chunk.nil?
end
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
@download_io.write(next_chunk)
@offset += next_chunk.bytesize
end
@download_io.flush if @download_io.respond_to?(:flush)
if @close_io_on_finish
result = nil
else
result = @download_io
end
check_status(http_res.status.to_i, http_res.header, http_res.body)
success(result, &block)
rescue => e
@download_io.flush if @download_io.respond_to?(:flush)
error(e, rethrow: true, &block)
end
end
end
end
end
@@ -1,44 +0,0 @@
# 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.
module Google
module Apis
module Core
# Adds to_hash to objects
module Hashable
# Convert object to hash representation
#
# @return [Hash]
def to_h
Hash[instance_variables.map { |k| [k[1..-1].to_sym, Hashable.process_value(instance_variable_get(k))] }]
end
# Recursively serialize an object
#
# @param [Object] val
# @return [Hash]
def self.process_value(val)
case val
when Hash
Hash[val.map {|k, v| [k.to_sym, Hashable.process_value(v)] }]
when Array
val.map{ |v| Hashable.process_value(v) }
else
val.respond_to?(:to_h) ? val.to_h : val
end
end
end
end
end
end
@@ -1,447 +0,0 @@
# 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 'addressable/uri'
require 'addressable/template'
require 'google/apis/options'
require 'google/apis/errors'
require 'retriable'
require 'google/apis/core/logging'
require 'pp'
module Google
module Apis
module Core
# Command for HTTP request/response.
class HttpCommand
include Logging
RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError]
begin
require 'opencensus'
OPENCENSUS_AVAILABLE = true
rescue LoadError
OPENCENSUS_AVAILABLE = false
end
# Request options
# @return [Google::Apis::RequestOptions]
attr_accessor :options
# HTTP request URL
# @return [String, Addressable::URI]
attr_accessor :url
# HTTP headers
# @return [Hash]
attr_accessor :header
# Request body
# @return [#read]
attr_accessor :body
# HTTP method
# @return [symbol]
attr_accessor :method
# HTTP Client
# @return [HTTPClient]
attr_accessor :connection
# Query params
# @return [Hash]
attr_accessor :query
# Path params for URL Template
# @return [Hash]
attr_accessor :params
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
# @param [String, #read] body
# Request body
def initialize(method, url, body: nil)
self.options = Google::Apis::RequestOptions.default.dup
self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method
self.header = Hash.new
self.body = body
self.query = {}
self.params = {}
@opencensus_span = nil
end
# Execute the command, retrying as necessary
#
# @param [HTTPClient] client
# 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(client)
prepare!
opencensus_begin_span
begin
Retriable.retriable tries: options.retries + 1,
base_interval: 1,
multiplier: 2,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
execute_once(client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
ensure
opencensus_end_span
@http_res = nil
release!
end
# Refresh the authorization authorization after a 401 error
#
# @private
# @return [void]
def refresh_authorization
# Handled implicitly by auth lib, here in case need to override
logger.debug('Retrying after authentication failure')
end
# Check if attached credentials can be automatically refreshed
# @return [Boolean]
def authorization_refreshable?
options.authorization.respond_to?(:apply!)
end
# Prepare the request (e.g. calculate headers, add query params, serialize data, etc) before sending
#
# @private
# @return [void]
def prepare!
normalize_unicode = true
if options
header.update(options.header) if options.header
query.update(options.query) if options.query
normalize_unicode = options.normalize_unicode
end
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
url.query_values = normalize_query_values(query).merge(url.query_values || {})
if allow_form_encoding?
@form_encoded = true
self.body = Addressable::URI.form_encode(url.query_values(Array))
self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
self.url.query_values = {}
else
@form_encoded = false
end
self.body = '' unless self.body
end
# Release any resources used by this command
# @private
# @return [void]
def release!
end
# Check the response and either decode body or raise error
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# 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)
check_status(status, header, body)
decode_response_body(header['Content-Type'].first, body)
end
# Check the response and raise error if needed
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# HTTP response headers
# @param [String] body
# HTTP response body
# @param [String] message
# Error message text
# @return [void]
# @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 check_status(status, header = nil, body = nil, message = nil)
# TODO: 304 Not Modified depends on context...
case status
when 200...300
nil
when 301, 302, 303, 307
message ||= sprintf('Redirect to %s', header['Location'])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401
message ||= 'Unauthorized'
raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
when 429
message ||= 'Rate limit exceeded'
raise Google::Apis::RateLimitError.new(message, status_code: status, header: header, body: body)
when 304, 400, 402...500
message ||= 'Invalid request'
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
when 500...600
message ||= 'Server error'
raise Google::Apis::ServerError.new(message, status_code: status, header: header, body: body)
else
logger.warn(sprintf('Encountered unexpected status code %s', status))
message ||= 'Unknown error'
raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
end
end
# Process the actual response body. Intended to be overridden by subclasses
#
# @param [String] _content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
def decode_response_body(_content_type, body)
body
end
# Process a success response
# @param [Object] result
# Result object
# @return [Object] result if no block given
# @yield [result, nil] if block given
def success(result, &block)
logger.debug { sprintf('Success - %s', safe_object_representation(result)) }
block.call(result, nil) if block_given?
result
end
# Process an error response
# @param [StandardError] err
# Error object
# @param [Boolean] rethrow
# True if error should be raised again after handling
# @return [void]
# @yield [nil, err] if block given
# @raise [StandardError] if no block
def error(err, rethrow: false, &block)
logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
if err.is_a?(HTTPClient::BadResponseError)
begin
res = err.res
raise Google::Apis::TransmissionError.new(err) if res.nil?
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?
fail err if rethrow || block.nil?
end
# Execute the command once.
#
# @private
# @param [HTTPClient] client
# HTTP client
# @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)
body.rewind if body.respond_to?(:rewind)
begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
request_header = header.dup
apply_request_options(request_header)
@http_res = client.request(method.to_s.upcase,
url.to_s,
query: nil,
body: body,
header: request_header,
follow_redirect: true)
logger.debug { @http_res.status }
logger.debug { safe_response_representation @http_res }
response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body)
success(response)
rescue => e
logger.debug { sprintf('Caught error %s', e) }
error(e, rethrow: true)
end
end
# Update the request with any specified options.
# @param [Hash] req_header
# HTTP headers
# @return [void]
def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String)
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end
req_header.update(header)
end
def allow_form_encoding?
[:post, :put].include?(method) && body.nil?
end
private
UNSAFE_CLASS_NAMES = [
"Google::Apis::CloudkmsV1::DecryptResponse"
]
def safe_object_representation obj
name = obj.class.name
if UNSAFE_CLASS_NAMES.include? name
"#<#{name} (fields redacted)>"
else
PP.pp(obj, "")
end
end
def safe_response_representation http_res
if respond_to?(:response_class) && response_class.is_a?(Class) &&
UNSAFE_CLASS_NAMES.include?(response_class.name)
return "#<#{http_res.class.name} (fields redacted)>"
end
http_res.inspect
end
def opencensus_begin_span
return unless OPENCENSUS_AVAILABLE && options.use_opencensus
return if @opencensus_span
return unless OpenCensus::Trace.span_context
@opencensus_span = OpenCensus::Trace.start_span url.path.to_s
@opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
@opencensus_span.put_attribute "http.host", url.host.to_s
@opencensus_span.put_attribute "http.method", method.to_s.upcase
@opencensus_span.put_attribute "http.path", url.path.to_s
if body.respond_to? :bytesize
@opencensus_span.put_message_event \
OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize
end
formatter = OpenCensus::Trace.config.http_formatter
if formatter.respond_to? :header_name
header[formatter.header_name] = formatter.serialize @opencensus_span.context.trace_context
end
rescue StandardError => e
# Log exceptions and continue, so opencensus failures don't cause
# the entire request to fail.
logger.debug { sprintf('Error opening OpenCensus span: %s', e) }
end
def opencensus_end_span
return unless OPENCENSUS_AVAILABLE
return unless @opencensus_span
return unless OpenCensus::Trace.span_context
if @http_res
if @http_res.body.respond_to? :bytesize
@opencensus_span.put_message_event \
OpenCensus::Trace::SpanBuilder::RECEIVED, 1, @http_res.body.bytesize
end
status = @http_res.status.to_i
if status > 0
@opencensus_span.set_status map_http_status status
@opencensus_span.put_attribute "http.status_code", status
end
end
OpenCensus::Trace.end_span @opencensus_span
@opencensus_span = nil
rescue StandardError => e
# Log exceptions and continue, so failures don't cause leaks by
# aborting cleanup.
logger.debug { sprintf('Error finishing OpenCensus span: %s', e) }
end
def form_encoded?
@form_encoded
end
def map_http_status http_status
case http_status
when 200..399 then 0 # OK
when 400 then 3 # INVALID_ARGUMENT
when 401 then 16 # UNAUTHENTICATED
when 403 then 7 # PERMISSION_DENIED
when 404 then 5 # NOT_FOUND
when 429 then 8 # RESOURCE_EXHAUSTED
when 501 then 12 # UNIMPLEMENTED
when 503 then 14 # UNAVAILABLE
when 504 then 4 # DEADLINE_EXCEEDED
else 2 # UNKNOWN
end
end
def normalize_query_values(input)
input.inject({}) do |h, (k, v)|
h[k] = normalize_query_value(v)
h
end
end
def normalize_query_value(v)
case v
when Array
v.map { |v2| normalize_query_value(v2) }
when nil
nil
else
v.to_s
end
end
end
end
end
end
@@ -1,153 +0,0 @@
# 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 'representable/json'
require 'representable/json/hash'
require 'base64'
require 'date'
module Google
module Apis
module Core
# Support for serializing hashes + property value/nil/unset tracking
# To be included in representers as a feature.
# @private
module JsonRepresentationSupport
def self.included(base)
base.extend(JsonSupport)
end
# @private
module JsonSupport
# Returns a customized getter function for Representable. Allows
# indifferent hash/attribute access.
#
# @param [String] name Property name
# @return [Proc]
def getter_fn(name)
ivar_name = "@#{name}".to_sym
lambda do |_|
if respond_to?(:fetch)
fetch(name, instance_variable_get(ivar_name))
else
instance_variable_get(ivar_name)
end
end
end
# Returns a customized function for Representable that checks whether or not
# an attribute should be serialized. Allows proper patch semantics by distinguishing
# between nil & unset values
#
# @param [String] name Property name
# @return [Proc]
def if_fn(name)
ivar_name = "@#{name}".to_sym
lambda do |opts|
if opts[:user_options] && opts[:user_options][:skip_undefined]
if respond_to?(:key?)
self.key?(name) || instance_variable_defined?(ivar_name)
else
instance_variable_defined?(ivar_name)
end
else
true
end
end
end
def set_default_options(name, options)
if options[:base64]
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) }
options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) }
end
if options[:numeric_string]
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
options[:parse_filter] = ->(fragment, _doc, *_args) { fragment.to_i }
end
if options[:type] == DateTime
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.is_a?(DateTime) ? value.rfc3339(3) : value.to_s }
options[:parse_filter] = ->(fragment, _doc, *_args) { DateTime.parse(fragment) }
end
if options[:type] == Date
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
options[:parse_filter] = ->(fragment, _doc, *_args) { Date.parse(fragment) }
end
options[:render_nil] = true
options[:getter] = getter_fn(name)
options[:if] = if_fn(name)
end
# Define a single value property
#
# @param [String] name
# Property name
# @param [Hash] options
def property(name, options = {})
set_default_options(name, options)
super(name, options)
end
# Define a collection property
#
# @param [String] name
# Property name
# @param [Hash] options
def collection(name, options = {})
set_default_options(name, options)
super(name, options)
end
# Define a hash property
#
# @param [String] name
# Property name
# @param [Hash] options
def hash(name = nil, options = nil)
return super() unless name # Allow Object.hash
set_default_options(name, options)
super(name, options)
end
end
end
# Base decorator for JSON representers
#
# @see https://github.com/apotonick/representable
class JsonRepresentation < Representable::Decorator
include Representable::JSON
feature JsonRepresentationSupport
end
module JsonObjectSupport
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def from_json(json)
representation = self.const_get(:Representation)
representation.new(self.new).from_json(json, unwrap: self)
end
end
def to_json(*a)
representation = self.class.const_get(:Representation)
representation.new(self).to_hash(user_options: { skip_undefined: true }).to_json(*a)
end
end
end
end
end
@@ -1,30 +0,0 @@
# 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'
module Google
module Apis
module Core
# Logging support
module Logging
# Get the logger instance
# @return [Logger]
def logger
Google::Apis.logger
end
end
end
end
end
@@ -1,135 +0,0 @@
# 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.
module Google
module Apis
module Core
# Part of a multipart request for holding JSON data
#
# @private
class JsonPart
# @param [String] value
# JSON content
# @param [Hash] header
# Additional headers
def initialize(value, header = {})
@value = value
@header = header
end
def to_io(boundary)
part = ''
part << "--#{boundary}\r\n"
part << "Content-Type: application/json\r\n"
@header.each do |(k, v)|
part << "#{k}: #{v}\r\n"
end
part << "\r\n"
part << "#{@value}\r\n"
StringIO.new(part)
end
end
# Part of a multipart request for holding arbitrary content.
#
# @private
class FilePart
# @param [IO] io
# IO stream
# @param [Hash] header
# Additional headers
def initialize(io, header = {})
@io = io
@header = header
@length = io.respond_to?(:size) ? io.size : nil
end
def to_io(boundary)
head = ''
head << "--#{boundary}\r\n"
@header.each do |(k, v)|
head << "#{k}: #{v}\r\n"
end
head << "Content-Length: #{@length}\r\n" unless @length.nil?
head << "Content-Transfer-Encoding: binary\r\n"
head << "\r\n"
Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
end
end
# Helper for building multipart requests
class Multipart
MULTIPART_RELATED = 'multipart/related'
# @return [String]
# Content type header
attr_reader :content_type
# @param [String] content_type
# Content type for the multipart request
# @param [String] boundary
# Part delimiter
def initialize(content_type: MULTIPART_RELATED, boundary: nil)
@parts = []
@boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
@content_type = "#{content_type}; boundary=#{@boundary}"
end
# Append JSON data part
#
# @param [String] body
# JSON text
# @param [String] content_id
# Optional unique ID of this part
# @return [self]
def add_json(body, content_id: nil)
header = {}
header['Content-ID'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
self
end
# Append arbitrary data as a part
#
# @param [IO] upload_io
# IO stream
# @param [String] content_id
# Optional unique ID of this part
# @return [self]
def add_upload(upload_io, content_type: nil, content_id: nil)
header = {
'Content-Type' => content_type || 'application/octet-stream'
}
header['Content-Id'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::FilePart.new(upload_io,
header).to_io(@boundary)
self
end
# Assemble the multipart requests
#
# @return [IO]
# IO stream
def assemble
@parts << StringIO.new("--#{@boundary}--\r\n\r\n")
Google::Apis::Core::CompositeIO.new(*@parts)
end
end
end
end
end
@@ -1,273 +0,0 @@
# 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'
require 'tempfile'
require 'mini_mime'
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'
CONTENT_TYPE_HEADER = 'Content-Type'
# 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
# Ensure the content is readable and wrapped in an IO instance.
#
# @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid
def prepare!
super
if streamable?(upload_source)
self.upload_io = upload_source
@close_io_on_finish = false
elsif self.upload_source.is_a?(String)
self.upload_io = File.new(upload_source, 'r')
if self.upload_content_type.nil?
type = MiniMime.lookup_by_filename(upload_source)
self.upload_content_type = type && type.content_type
end
@close_io_on_finish = true
else
fail Google::Apis::ClientError, 'Invalid upload source'
end
if self.upload_content_type.nil? || self.upload_content_type.empty?
self.upload_content_type = 'application/octet-stream'
end
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
private
def streamable?(upload_source)
upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
end
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
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
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
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
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
# 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
super
end
# Check the to see if the upload is complete or needs to be resumed.
#
# @param [Fixnum] status
# HTTP status code of response
# @param [HTTP::Message::Headers] header
# 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)
@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
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) }
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)
rescue => e
raise Google::Apis::ServerError, e.message
end
# Query for the status of an incomplete upload
#
# @param [HTTPClient] client
# HTTP client
# @return [HTTP::Message]
# @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) }
request_header = header.dup
apply_request_options(request_header)
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
end
# Send the actual content
#
# @param [HTTPClient] client
# HTTP client
# @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request
def send_upload_command(client)
logger.debug { sprintf('Sending upload command to %s', @upload_url) }
content = upload_io
content.pos = @offset
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
request_header[CONTENT_TYPE_HEADER] = upload_content_type
client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
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
# @param [HTTPClient] client
# 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)
case @state
when :start
response = send_start_command(client)
result = process_response(response.status_code, response.header, response.body)
when :active
response = send_query_command(client)
result = process_response(response.status_code, response.header, response.body)
when :cancelled, :final
error(@last_error, rethrow: true, &block)
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
# 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
error(e, rethrow: true, &block)
end
end
end
end
end
@@ -1,89 +0,0 @@
# 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.
module Google
module Apis
# Base error, capable of wrapping another
class Error < StandardError
attr_reader :status_code
attr_reader :header
attr_reader :body
def initialize(err, status_code: nil, header: nil, body: nil)
@cause = nil
if err.respond_to?(:backtrace)
super(err.message)
@cause = err
else
super(err.to_s)
end
@status_code = status_code
@header = header.dup unless header.nil?
@body = body
end
def backtrace
if @cause
@cause.backtrace
else
super
end
end
def inspect
extra = ""
extra << " status_code: #{status_code.inspect}" unless status_code.nil?
extra << " header: #{header.inspect}" unless header.nil?
extra << " body: #{body.inspect}" unless body.nil?
"#<#{self.class.name}: #{message}#{extra}>"
end
end
# An error which is raised when there is an unexpected response or other
# transport error that prevents an operation from succeeding.
class TransmissionError < Error
end
# An exception that is raised if a redirect is required
#
class RedirectError < Error
end
# A 4xx class HTTP error occurred.
class ClientError < Error
end
# A 4xx class HTTP error occurred.
class RateLimitError < Error
end
# A 403 HTTP error occurred.
class ProjectNotLinkedError < Error
end
# A 401 HTTP error occurred.
class AuthorizationError < Error
end
# A 5xx class HTTP error occurred.
class ServerError < Error
end
# Error class for problems in batch requests.
class BatchError < Error
end
end
end
@@ -1,116 +0,0 @@
# 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.
module Google
module Apis
# General options for API requests
ClientOptions = Struct.new(
:application_name,
:application_version,
:proxy_url,
:open_timeout_sec,
:read_timeout_sec,
:send_timeout_sec,
:log_http_requests,
:transparent_gzip_decompression)
RequestOptions = Struct.new(
:authorization,
:retries,
:header,
:normalize_unicode,
:skip_serialization,
:skip_deserialization,
:api_format_version,
:use_opencensus,
:quota_project,
:query)
# General client options
class ClientOptions
# @!attribute [rw] application_name
# @return [String] Name of the application, for identification in the User-Agent header
# @!attribute [rw] application_version
# @return [String] Version of the application, for identification in the User-Agent header
# @!attribute [rw] proxy_url
# @return [String] URL of a proxy server
# @!attribute [rw] log_http_requests
# @return [Boolean] True if raw HTTP requests should be logged
# @!attribute [rw] open_timeout_sec
# @return [Fixnum] How long, in seconds, before failed connections time out
# @!attribute [rw] read_timeout_sec
# @return [Fixnum] How long, in seconds, before requests time out
# @!attribute [rw] transparent_gzip_decompression
# @return [Boolean] True if gzip compression needs to be enabled
# Get the default options
# @return [Google::Apis::ClientOptions]
def self.default
@options ||= ClientOptions.new
end
end
# Request options
class RequestOptions
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
# @!attribute [rw] retries
# @return [Fixnum] Number of times to retry requests on server error.
# @!attribute [rw] header
# @return [Hash<String,String>] Additional HTTP headers to include in requests.
# @!attribute [rw] normalize_unicode
# @return [Boolean] True if unicode strings should be normalized in path parameters.
# @!attribute [rw] skip_serialization
# @return [Boolean] True if body object should be treated as raw text instead of an object.
# @!attribute [rw] skip_deserialization
# @return [Boolean] True if response should be returned in raw form instead of deserialized.
# @!attribute [rw] api_format_version
# @return [Fixnum] Version of the error format to request/expect.
# @!attribute [rw] use_opencensus
# @return [Boolean] Whether OpenCensus spans should be generated for requests. Default is true.
# @!attribute [rw] quota_project
# @return [String] Project ID to charge quota, or `nil` to default to the credentials-specified project.
# @!attribute [rw] query
# @return [Hash<String,String>] Additional HTTP URL query parameters to include in requests.
# Get the default options
# @return [Google::Apis::RequestOptions]
def self.default
@options ||= RequestOptions.new
end
def merge(options)
return self if options.nil?
new_options = dup
members.each do |opt|
opt = opt.to_sym
new_options[opt] = options[opt] unless options[opt].nil?
end
new_options
end
end
ClientOptions.default.log_http_requests = false
ClientOptions.default.application_name = 'unknown'
ClientOptions.default.application_version = '0.0.0'
ClientOptions.default.transparent_gzip_decompression = true
RequestOptions.default.retries = 0
RequestOptions.default.normalize_unicode = false
RequestOptions.default.skip_serialization = false
RequestOptions.default.skip_deserialization = false
RequestOptions.default.api_format_version = nil
RequestOptions.default.use_opencensus = true
RequestOptions.default.quota_project = nil
end
end
@@ -18,27 +18,5 @@ module Google
module Apis
# Client library version
VERSION = "0.52.0".freeze
# Current operating system
# @private
OS_VERSION = begin
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
output, _ = Open3.capture2('ver')
output.sub(/\s*\[Version\s*/, '/').sub(']', '')
elsif RUBY_PLATFORM =~ /darwin/i
output, _ = Open3.capture2('sw_vers', '-productVersion')
"Mac OS X/#{output}"
elsif RUBY_PLATFORM == 'java'
require 'java'
name = java.lang.System.getProperty('os.name')
version = java.lang.System.getProperty('os.version')
"#{name}/#{version}"
else
output, _ = Open3.capture2('uname', '-sr')
output.sub(' ', '/')
end.strip
rescue
RUBY_PLATFORM
end
end
end