google-api-client 0.6.4 → 0.7.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,7 +14,6 @@
14
14
 
15
15
 
16
16
  require 'faraday'
17
- require 'faraday/utils'
18
17
  require 'multi_json'
19
18
  require 'compat/multi_json'
20
19
  require 'stringio'
@@ -30,6 +29,8 @@ require 'google/api_client/result'
30
29
  require 'google/api_client/media'
31
30
  require 'google/api_client/service_account'
32
31
  require 'google/api_client/batch'
32
+ require 'google/api_client/gzip'
33
+ require 'google/api_client/client_secrets'
33
34
  require 'google/api_client/railtie' if defined?(Rails::Railtie)
34
35
 
35
36
  module Google
@@ -70,6 +71,9 @@ module Google
70
71
  # The port number used by the client. This rarely needs to be changed.
71
72
  # @option options [String] :discovery_path ("/discovery/v1")
72
73
  # The discovery base path. This rarely needs to be changed.
74
+ # @option options [String] :ca_file
75
+ # Optional set of root certificates to use when validating SSL connections.
76
+ # By default, a bundled set of trusted roots will be used.
73
77
  def initialize(options={})
74
78
  logger.debug { "#{self.class} - Initializing client with options #{options}" }
75
79
 
@@ -94,8 +98,7 @@ module Google
94
98
  end
95
99
  self.user_agent = options[:user_agent] || (
96
100
  "#{application_string} " +
97
- "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} " +
98
- ENV::OS_VERSION
101
+ "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} #{ENV::OS_VERSION} (gzip)"
99
102
  ).strip
100
103
  # The writer method understands a few Symbols and will generate useful
101
104
  # default authentication mechanisms.
@@ -107,7 +110,14 @@ module Google
107
110
  @discovery_uris = {}
108
111
  @discovery_documents = {}
109
112
  @discovered_apis = {}
110
-
113
+ ca_file = options[:ca_file] || File.expand_path('../../cacerts.pem', __FILE__)
114
+ self.connection = Faraday.new do |faraday|
115
+ faraday.response :gzip
116
+ faraday.options.params_encoder = Faraday::FlatParamsEncoder
117
+ faraday.ssl.ca_file = ca_file
118
+ faraday.ssl.verify = true
119
+ faraday.adapter Faraday.default_adapter
120
+ end
111
121
  return self
112
122
  end
113
123
 
@@ -167,6 +177,12 @@ module Google
167
177
  return @authorization
168
178
  end
169
179
 
180
+ ##
181
+ # Default Faraday/HTTP connection.
182
+ #
183
+ # @return [Faraday::Connection]
184
+ attr_accessor :connection
185
+
170
186
  ##
171
187
  # The setting that controls whether or not the api client attempts to
172
188
  # refresh authorization when a 401 is hit in #execute.
@@ -515,6 +531,8 @@ module Google
515
531
  # - (TrueClass, FalseClass) :authenticated (default: true) -
516
532
  # `true` if the request must be signed or somehow
517
533
  # authenticated, `false` otherwise.
534
+ # - (TrueClass, FalseClass) :gzip (default: true) -
535
+ # `true` if gzip enabled, `false` otherwise.
518
536
  #
519
537
  # @return [Google::APIClient::Result] The result from the API, nil if batch.
520
538
  #
@@ -530,10 +548,9 @@ module Google
530
548
  #
531
549
  # @see Google::APIClient#generate_request
532
550
  def execute(*params)
533
- if params.last.kind_of?(Google::APIClient::Request) &&
534
- params.size == 1
535
- request = params.pop
536
- options = {}
551
+ if params.first.kind_of?(Google::APIClient::Request)
552
+ request = params.shift
553
+ options = params.shift || {}
537
554
  else
538
555
  # This block of code allows us to accept multiple parameter passing
539
556
  # styles, and maintaining some backwards compatibility.
@@ -554,10 +571,11 @@ module Google
554
571
  end
555
572
 
556
573
  request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
574
+ request.headers['Accept-Encoding'] ||= 'gzip' unless options[:gzip] == false
557
575
  request.parameters['key'] ||= self.key unless self.key.nil?
558
576
  request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
559
577
 
560
- connection = options[:connection] || Faraday.default_connection
578
+ connection = options[:connection] || self.connection
561
579
  request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
562
580
 
563
581
  result = request.send(connection)
@@ -565,7 +583,7 @@ module Google
565
583
  begin
566
584
  logger.debug("Attempting refresh of access token & retry of request")
567
585
  request.authorization.fetch_access_token!
568
- result = request.send(connection)
586
+ result = request.send(connection, true)
569
587
  rescue Signet::AuthorizationError
570
588
  # Ignore since we want the original error
571
589
  end
@@ -0,0 +1,28 @@
1
+ # Copyright 2013 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'faraday'
16
+ require 'signet/oauth_2/client'
17
+
18
+ module Google
19
+ class APIClient
20
+ class ComputeServiceAccount < Signet::OAuth2::Client
21
+ def fetch_access_token(options={})
22
+ connection = options[:connection] || Faraday.default_connection
23
+ response = connection.get 'http://metadata/computeMetadata/v1beta1/instance/service-accounts/default/token'
24
+ Signet::OAuth2.parse_json_credentials(response.body)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,87 @@
1
+ # Copyright 2013 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+ require 'signet/oauth_2/client'
17
+
18
+ module Google
19
+ class APIClient
20
+ ##
21
+ # Represents cached OAuth 2 tokens stored on local disk in a
22
+ # JSON serialized file. Meant to resemble the serialized format
23
+ # http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
24
+ #
25
+ class FileStorage
26
+ # @return [String] Path to the credentials file.
27
+ attr_accessor :path
28
+
29
+ # @return [Signet::OAuth2::Client] Path to the credentials file.
30
+ attr_reader :authorization
31
+
32
+ ##
33
+ # Initializes the FileStorage object.
34
+ #
35
+ # @param [String] path
36
+ # Path to the credentials file.
37
+ def initialize(path)
38
+ @path = path
39
+ self.load_credentials
40
+ end
41
+
42
+ ##
43
+ # Attempt to read in credentials from the specified file.
44
+ def load_credentials
45
+ if File.exist? self.path
46
+ File.open(self.path, 'r') do |file|
47
+ cached_credentials = JSON.load(file)
48
+ @authorization = Signet::OAuth2::Client.new(cached_credentials)
49
+ @authorization.issued_at = Time.at(cached_credentials['issued_at'])
50
+ if @authorization.expired?
51
+ @authorization.fetch_access_token!
52
+ self.write_credentials
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Write the credentials to the specified file.
60
+ #
61
+ # @param [Signet::OAuth2::Client] authorization
62
+ # Optional authorization instance. If not provided, the authorization
63
+ # already associated with this instance will be written.
64
+ def write_credentials(authorization=nil)
65
+ @authorization = authorization unless authorization.nil?
66
+
67
+ unless @authorization.refresh_token.nil?
68
+ hash = {}
69
+ %w'access_token
70
+ authorization_uri
71
+ client_id
72
+ client_secret
73
+ expires_in
74
+ refresh_token
75
+ token_credential_uri'.each do |var|
76
+ hash[var] = @authorization.instance_variable_get("@#{var}")
77
+ end
78
+ hash['issued_at'] = @authorization.issued_at.to_i
79
+
80
+ File.open(self.path, 'w', 0600) do |file|
81
+ file.write(hash.to_json)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -77,9 +77,13 @@ module Google
77
77
  ##
78
78
  # Request authorization. Opens a browser and waits for response.
79
79
  #
80
+ # @param [Google::APIClient::FileStorage] storage
81
+ # Optional object that responds to :write_credentials, used to serialize
82
+ # the OAuth 2 credentials after completing the flow.
83
+ #
80
84
  # @return [Signet::OAuth2::Client]
81
85
  # Authorization instance, nil if user cancelled.
82
- def authorize
86
+ def authorize(storage=nil)
83
87
  auth = @authorization
84
88
 
85
89
  server = WEBrick::HTTPServer.new(
@@ -103,6 +107,9 @@ module Google
103
107
  Launchy.open(auth.authorization_uri.to_s)
104
108
  server.start
105
109
  if @authorization.access_token
110
+ if storage.respond_to?(:write_credentials)
111
+ storage.write_credentials(@authorization)
112
+ end
106
113
  return @authorization
107
114
  else
108
115
  return nil
@@ -34,17 +34,6 @@ module Google
34
34
  # client.authorization.fetch_access_token!
35
35
  # client.execute(...)
36
36
  #
37
- # @example Deprecated version
38
- #
39
- # client = Google::APIClient.new
40
- # key = Google::APIClient::PKCS12.load_key('client.p12', 'notasecret')
41
- # service_account = Google::APIClient::JWTAsserter.new(
42
- # '123456-abcdef@developer.gserviceaccount.com',
43
- # 'https://www.googleapis.com/auth/prediction',
44
- # key)
45
- # client.authorization = service_account.authorize
46
- # client.execute(...)
47
- #
48
37
  # @deprecated
49
38
  # Service accounts are now supported directly in Signet
50
39
  # @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount
@@ -146,7 +146,7 @@ module Google
146
146
  end
147
147
 
148
148
  def to_authorization
149
- gem 'signet', '~> 0.4.0'
149
+ gem 'signet', '>= 0.4.0'
150
150
  require 'signet/oauth_2/client'
151
151
  # NOTE: Do not rely on this default value, as it may change
152
152
  new_authorization = Signet::OAuth2::Client.new
@@ -187,6 +187,7 @@ module Google
187
187
  # @return [Addressable::URI] The URI after expansion.
188
188
  def generate_uri(parameters={})
189
189
  parameters = self.normalize_parameters(parameters)
190
+
190
191
  self.validate_parameters(parameters)
191
192
  template_variables = self.uri_template.variables
192
193
  upload_type = parameters.assoc('uploadType') || parameters.assoc('upload_type')
@@ -0,0 +1,28 @@
1
+ require 'faraday'
2
+ require 'zlib'
3
+
4
+ module Google
5
+ class APIClient
6
+ class Gzip < Faraday::Response::Middleware
7
+ include Google::APIClient::Logging
8
+
9
+ def on_complete(env)
10
+ encoding = env[:response_headers]['content-encoding'].to_s.downcase
11
+ case encoding
12
+ when 'gzip'
13
+ logger.debug { "Decompressing gzip encoded response (#{env[:body].length} bytes)" }
14
+ env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body])).read
15
+ env[:response_headers].delete('content-encoding')
16
+ logger.debug { "Decompressed (#{env[:body].length} bytes)" }
17
+ when 'deflate'
18
+ logger.debug{ "Decompressing deflate encoded response (#{env[:body].length} bytes)" }
19
+ env[:body] = Zlib::Inflate.inflate(env[:body])
20
+ env[:response_headers].delete('content-encoding')
21
+ logger.debug { "Decompressed (#{env[:body].length} bytes)" }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Faraday::Response.register_middleware :gzip => Google::APIClient::Gzip
@@ -21,7 +21,11 @@ module Google
21
21
  # @see Faraday::UploadIO
22
22
  # @example
23
23
  # media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
24
- class UploadIO < Faraday::UploadIO
24
+ class UploadIO < Faraday::UploadIO
25
+
26
+ # @return [Fixnum] Size of chunks to upload. Default is nil, meaning upload the entire file in a single request
27
+ attr_accessor :chunk_size
28
+
25
29
  ##
26
30
  # Get the length of the stream
27
31
  #
@@ -32,6 +36,77 @@ module Google
32
36
  end
33
37
  end
34
38
 
39
+ ##
40
+ # Wraps an input stream and limits data to a given range
41
+ #
42
+ # @example
43
+ # chunk = Google::APIClient::RangedIO.new(io, 0, 1000)
44
+ class RangedIO
45
+ ##
46
+ # Bind an input stream to a specific range.
47
+ #
48
+ # @param [IO] io
49
+ # Source input stream
50
+ # @param [Fixnum] offset
51
+ # Starting offset of the range
52
+ # @param [Fixnum] length
53
+ # Length of range
54
+ def initialize(io, offset, length)
55
+ @io = io
56
+ @offset = offset
57
+ @length = length
58
+ self.rewind
59
+ end
60
+
61
+ ##
62
+ # @see IO#read
63
+ def read(amount = nil, buf = nil)
64
+ buffer = buf || ''
65
+ if amount.nil?
66
+ size = @length - @pos
67
+ done = ''
68
+ elsif amount == 0
69
+ size = 0
70
+ done = ''
71
+ else
72
+ size = [@length - @pos, amount].min
73
+ done = nil
74
+ end
75
+
76
+ if size > 0
77
+ result = @io.read(size)
78
+ result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
79
+ buffer << result if result
80
+ @pos = @pos + size
81
+ end
82
+
83
+ if buffer.length > 0
84
+ buffer
85
+ else
86
+ done
87
+ end
88
+ end
89
+
90
+ ##
91
+ # @see IO#rewind
92
+ def rewind
93
+ self.pos = 0
94
+ end
95
+
96
+ ##
97
+ # @see IO#pos
98
+ def pos
99
+ @pos
100
+ end
101
+
102
+ ##
103
+ # @see IO#pos=
104
+ def pos=(pos)
105
+ @pos = pos
106
+ @io.pos = @offset + pos
107
+ end
108
+ end
109
+
35
110
  ##
36
111
  # Resumable uploader.
37
112
  #
@@ -124,11 +199,11 @@ module Google
124
199
  'Content-Range' => "bytes */#{media.length}" })
125
200
  else
126
201
  start_offset = @offset
127
- self.media.io.pos = start_offset
128
- chunk = self.media.io.read(chunk_size)
129
- content_length = chunk.bytesize
202
+ remaining = self.media.length - start_offset
203
+ chunk_size = self.media.chunk_size || self.chunk_size || self.media.length
204
+ content_length = [remaining, chunk_size].min
205
+ chunk = RangedIO.new(self.media.io, start_offset, content_length)
130
206
  end_offset = start_offset + content_length - 1
131
-
132
207
  self.headers.update({
133
208
  'Content-Length' => "#{content_length}",
134
209
  'Content-Type' => self.media.content_type,
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require 'faraday'
16
- require 'faraday/utils'
16
+ require 'faraday/request/multipart'
17
17
  require 'multi_json'
18
18
  require 'compat/multi_json'
19
19
  require 'addressable/uri'
@@ -71,8 +71,10 @@ module Google
71
71
  # @option options [String, Symbol] :http_method
72
72
  # HTTP method when requesting a URI
73
73
  def initialize(options={})
74
- @parameters = Hash[options[:parameters] || {}]
74
+ @parameters = Faraday::Utils::ParamsHash.new
75
75
  @headers = Faraday::Utils::Headers.new
76
+
77
+ self.parameters.merge!(options[:parameters]) unless options[:parameters].nil?
76
78
  self.headers.merge!(options[:headers]) unless options[:headers].nil?
77
79
  self.api_method = options[:api_method]
78
80
  self.authenticated = options[:authenticated]
@@ -150,10 +152,13 @@ module Google
150
152
  #
151
153
  # @param [Faraday::Connection] connection
152
154
  # the connection to transmit with
155
+ # @param [TrueValue,FalseValue] is_retry
156
+ # True if request has been previous sent
153
157
  #
154
158
  # @return [Google::APIClient::Result]
155
159
  # result of API request
156
- def send(connection)
160
+ def send(connection, is_retry = false)
161
+ self.body.rewind if is_retry && self.body.respond_to?(:rewind)
157
162
  env = self.to_env(connection)
158
163
  logger.debug { "#{self.class} Sending API request #{env[:method]} #{env[:url].to_s} #{env[:request_headers]}" }
159
164
  http_response = connection.app.call(env)
@@ -163,8 +168,8 @@ module Google
163
168
 
164
169
  # Resumamble slightly different than other upload protocols in that it requires at least
165
170
  # 2 requests.
166
- if self.upload_type == 'resumable'
167
- upload = result.resumable_upload
171
+ if result.status == 200 && self.upload_type == 'resumable'
172
+ upload = result.resumable_upload
168
173
  unless upload.complete?
169
174
  logger.debug { "#{self.class} Sending upload body" }
170
175
  result = upload.send(connection)
@@ -314,10 +319,10 @@ module Google
314
319
  # @param [String] boundary
315
320
  # Boundary for separating each part of the message
316
321
  def build_multipart(parts, mime_type = 'multipart/related', boundary = MULTIPART_BOUNDARY)
317
- env = {
318
- :request_headers => {'Content-Type' => "#{mime_type};boundary=#{boundary}"},
319
- :request => { :boundary => boundary }
320
- }
322
+ env = Faraday::Env.new
323
+ env.request = Faraday::RequestOptions.new
324
+ env.request.boundary = boundary
325
+ env.request_headers = {'Content-Type' => "#{mime_type};boundary=#{boundary}"}
321
326
  multipart = Faraday::Request::Multipart.new
322
327
  self.body = multipart.create_multipart(env, parts.map {|part| [nil, part]})
323
328
  self.headers.update(env[:request_headers])