google-api-client 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +6 -0
- data/lib/google/api_client.rb +72 -30
- data/lib/google/api_client/client_secrets.rb +105 -0
- data/lib/google/api_client/discovery/api.rb +3 -4
- data/lib/google/api_client/discovery/media.rb +77 -0
- data/lib/google/api_client/discovery/method.rb +28 -6
- data/lib/google/api_client/discovery/schema.rb +7 -2
- data/lib/google/api_client/environment.rb +20 -9
- data/lib/google/api_client/media.rb +172 -0
- data/lib/google/api_client/reference.rb +67 -15
- data/lib/google/api_client/result.rb +16 -4
- data/lib/google/api_client/version.rb +1 -1
- metadata +5 -2
data/CHANGELOG.md
CHANGED
data/lib/google/api_client.rb
CHANGED
@@ -25,7 +25,7 @@ require 'google/api_client/environment'
|
|
25
25
|
require 'google/api_client/discovery'
|
26
26
|
require 'google/api_client/reference'
|
27
27
|
require 'google/api_client/result'
|
28
|
-
|
28
|
+
require 'google/api_client/media'
|
29
29
|
|
30
30
|
module Google
|
31
31
|
# TODO(bobaman): Document all this stuff.
|
@@ -47,8 +47,6 @@ module Google
|
|
47
47
|
# <li><code>:oauth_1</code></li>
|
48
48
|
# <li><code>:oauth_2</code></li>
|
49
49
|
# </ul>
|
50
|
-
# @option options [String] :host ("www.googleapis.com")
|
51
|
-
# The API hostname used by the client. This rarely needs to be changed.
|
52
50
|
# @option options [String] :application_name
|
53
51
|
# The name of the application using the client.
|
54
52
|
# @option options [String] :application_version
|
@@ -57,6 +55,12 @@ module Google
|
|
57
55
|
# ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
|
58
56
|
# The user agent used by the client. Most developers will want to
|
59
57
|
# leave this value alone and use the `:application_name` option instead.
|
58
|
+
# @option options [String] :host ("www.googleapis.com")
|
59
|
+
# The API hostname used by the client. This rarely needs to be changed.
|
60
|
+
# @option options [String] :port (443)
|
61
|
+
# The port number used by the client. This rarely needs to be changed.
|
62
|
+
# @option options [String] :discovery_path ("/discovery/v1")
|
63
|
+
# The discovery base path. This rarely needs to be changed.
|
60
64
|
def initialize(options={})
|
61
65
|
# Normalize key to String to allow indifferent access.
|
62
66
|
options = options.inject({}) do |accu, (key, value)|
|
@@ -65,6 +69,9 @@ module Google
|
|
65
69
|
end
|
66
70
|
# Almost all API usage will have a host of 'www.googleapis.com'.
|
67
71
|
self.host = options["host"] || 'www.googleapis.com'
|
72
|
+
self.port = options["port"] || 443
|
73
|
+
self.discovery_path = options["discovery_path"] || '/discovery/v1'
|
74
|
+
|
68
75
|
# Most developers will want to leave this value alone and use the
|
69
76
|
# application_name option.
|
70
77
|
application_string = (
|
@@ -80,7 +87,8 @@ module Google
|
|
80
87
|
).strip
|
81
88
|
# The writer method understands a few Symbols and will generate useful
|
82
89
|
# default authentication mechanisms.
|
83
|
-
self.authorization =
|
90
|
+
self.authorization =
|
91
|
+
options.key?("authorization") ? options["authorization"] : :oauth_2
|
84
92
|
self.key = options["key"]
|
85
93
|
self.user_ip = options["user_ip"]
|
86
94
|
@discovery_uris = {}
|
@@ -160,29 +168,65 @@ module Google
|
|
160
168
|
# @return [String] The user's IP address.
|
161
169
|
attr_accessor :user_ip
|
162
170
|
|
171
|
+
##
|
172
|
+
# The user agent used by the client.
|
173
|
+
#
|
174
|
+
# @return [String]
|
175
|
+
# The user agent string used in the User-Agent header.
|
176
|
+
attr_accessor :user_agent
|
177
|
+
|
163
178
|
##
|
164
179
|
# The API hostname used by the client.
|
165
180
|
#
|
166
181
|
# @return [String]
|
167
|
-
# The API hostname.
|
182
|
+
# The API hostname. Should almost always be 'www.googleapis.com'.
|
168
183
|
attr_accessor :host
|
169
184
|
|
170
185
|
##
|
171
|
-
# The
|
186
|
+
# The port number used by the client.
|
172
187
|
#
|
173
188
|
# @return [String]
|
174
|
-
# The
|
175
|
-
attr_accessor :
|
189
|
+
# The port number. Should almost always be 443.
|
190
|
+
attr_accessor :port
|
191
|
+
|
192
|
+
##
|
193
|
+
# The base path used by the client for discovery.
|
194
|
+
#
|
195
|
+
# @return [String]
|
196
|
+
# The base path. Should almost always be '/discovery/v1'.
|
197
|
+
attr_accessor :discovery_path
|
198
|
+
|
199
|
+
##
|
200
|
+
# Resolves a URI template against the client's configured base.
|
201
|
+
#
|
202
|
+
# @param [String, Addressable::URI, Addressable::Template] template
|
203
|
+
# The template to resolve.
|
204
|
+
# @param [Hash] mapping The mapping that corresponds to the template.
|
205
|
+
# @return [Addressable::URI] The expanded URI.
|
206
|
+
def resolve_uri(template, mapping={})
|
207
|
+
@base_uri ||= Addressable::URI.new(
|
208
|
+
:scheme => 'https',
|
209
|
+
:host => self.host,
|
210
|
+
:port => self.port
|
211
|
+
).normalize
|
212
|
+
template = if template.kind_of?(Addressable::Template)
|
213
|
+
template.pattern
|
214
|
+
elsif template.respond_to?(:to_str)
|
215
|
+
template.to_str
|
216
|
+
else
|
217
|
+
raise TypeError,
|
218
|
+
"Expected String, Addressable::URI, or Addressable::Template, " +
|
219
|
+
"got #{template.class}."
|
220
|
+
end
|
221
|
+
return Addressable::Template.new(@base_uri + template).expand(mapping)
|
222
|
+
end
|
176
223
|
|
177
224
|
##
|
178
225
|
# Returns the URI for the directory document.
|
179
226
|
#
|
180
227
|
# @return [Addressable::URI] The URI of the directory document.
|
181
228
|
def directory_uri
|
182
|
-
|
183
|
-
"https://{host}/discovery/v1/apis"
|
184
|
-
)
|
185
|
-
return template.expand({"host" => self.host})
|
229
|
+
return resolve_uri(self.discovery_path + '/apis')
|
186
230
|
end
|
187
231
|
|
188
232
|
##
|
@@ -207,17 +251,13 @@ module Google
|
|
207
251
|
def discovery_uri(api, version=nil)
|
208
252
|
api = api.to_s
|
209
253
|
version = version || 'v1'
|
210
|
-
return @discovery_uris["#{api}:#{version}"] ||= (
|
211
|
-
|
212
|
-
|
213
|
-
|
254
|
+
return @discovery_uris["#{api}:#{version}"] ||= (
|
255
|
+
resolve_uri(
|
256
|
+
self.discovery_path + '/apis/{api}/{version}/rest',
|
257
|
+
'api' => api,
|
258
|
+
'version' => version
|
214
259
|
)
|
215
|
-
|
216
|
-
"host" => self.host,
|
217
|
-
"api" => api,
|
218
|
-
"version" => version
|
219
|
-
})
|
220
|
-
end)
|
260
|
+
)
|
221
261
|
end
|
222
262
|
|
223
263
|
##
|
@@ -596,7 +636,7 @@ module Google
|
|
596
636
|
unless headers.kind_of?(Enumerable)
|
597
637
|
# We need to use some Enumerable methods, relying on the presence of
|
598
638
|
# the #each method.
|
599
|
-
class <<headers
|
639
|
+
class << headers
|
600
640
|
include Enumerable
|
601
641
|
end
|
602
642
|
end
|
@@ -679,13 +719,15 @@ module Google
|
|
679
719
|
# @see Google::APIClient#execute
|
680
720
|
def execute!(*params)
|
681
721
|
result = self.execute(*params)
|
682
|
-
if result.data
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
722
|
+
if result.data?
|
723
|
+
if result.data.respond_to?(:error) &&
|
724
|
+
result.data.error.respond_to?(:message)
|
725
|
+
# You're going to get a terrible error message if the response isn't
|
726
|
+
# parsed successfully as an error.
|
727
|
+
error_message = result.data.error.message
|
728
|
+
elsif result.data['error'] && result.data['error']['message']
|
729
|
+
error_message = result.data['error']['message']
|
730
|
+
end
|
689
731
|
end
|
690
732
|
if result.response.status >= 400
|
691
733
|
case result.response.status
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# Copyright 2010 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
|
+
|
16
|
+
require 'multi_json'
|
17
|
+
|
18
|
+
|
19
|
+
module Google
|
20
|
+
class APIClient
|
21
|
+
##
|
22
|
+
# Manages the persistence of client configuration data and secrets.
|
23
|
+
class ClientSecrets
|
24
|
+
def self.load(filename=nil)
|
25
|
+
if filename && File.directory?(filename)
|
26
|
+
search_path = File.expand_path(filename)
|
27
|
+
filename = nil
|
28
|
+
end
|
29
|
+
while filename == nil
|
30
|
+
search_path ||= File.expand_path('.')
|
31
|
+
puts search_path
|
32
|
+
if File.exist?(File.join(search_path, 'client_secrets.json'))
|
33
|
+
filename = File.join(search_path, 'client_secrets.json')
|
34
|
+
elsif search_path == '/' || search_path =~ /[a-zA-Z]:[\/\\]/
|
35
|
+
raise ArgumentError,
|
36
|
+
'No client_secrets.json filename supplied ' +
|
37
|
+
'and/or could not be found in search path.'
|
38
|
+
else
|
39
|
+
search_path = File.expand_path(File.join(search_path, '..'))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
data = File.open(filename, 'r') { |file| MultiJson.decode(file.read) }
|
43
|
+
return self.new(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(options={})
|
47
|
+
# Client auth configuration
|
48
|
+
@flow = options[:flow] || options.keys.first.to_s || 'web'
|
49
|
+
fdata = options[@flow]
|
50
|
+
@client_id = fdata[:client_id] || fdata["client_id"]
|
51
|
+
@client_secret = fdata[:client_secret] || fdata["client_secret"]
|
52
|
+
@redirect_uris = fdata[:redirect_uris] || fdata["redirect_uris"]
|
53
|
+
@redirect_uris ||= [fdata[:redirect_uri]]
|
54
|
+
@javascript_origins = (
|
55
|
+
fdata[:javascript_origins] ||
|
56
|
+
fdata["javascript_origins"]
|
57
|
+
)
|
58
|
+
@javascript_origins ||= [fdata[:javascript_origin]]
|
59
|
+
@authorization_uri = fdata[:auth_uri] || fdata["auth_uri"]
|
60
|
+
@authorization_uri ||= fdata[:authorization_uri]
|
61
|
+
@token_credential_uri = fdata[:token_uri] || fdata["token_uri"]
|
62
|
+
@token_credential_uri ||= fdata[:token_credential_uri]
|
63
|
+
|
64
|
+
# Associated token info
|
65
|
+
@access_token = fdata[:access_token] || fdata["access_token"]
|
66
|
+
@refresh_token = fdata[:refresh_token] || fdata["refresh_token"]
|
67
|
+
@id_token = fdata[:id_token] || fdata["id_token"]
|
68
|
+
@expires_in = fdata[:expires_in] || fdata["expires_in"]
|
69
|
+
@expires_at = fdata[:expires_at] || fdata["expires_at"]
|
70
|
+
@issued_at = fdata[:issued_at] || fdata["issued_at"]
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_reader(
|
74
|
+
:flow, :client_id, :client_secret, :redirect_uris, :javascript_origins,
|
75
|
+
:authorization_uri, :token_credential_uri, :access_token,
|
76
|
+
:refresh_token, :id_token, :expires_in, :expires_at, :issued_at
|
77
|
+
)
|
78
|
+
|
79
|
+
def to_json
|
80
|
+
return MultiJson.encode({
|
81
|
+
self.flow => ({
|
82
|
+
'client_id' => self.client_id,
|
83
|
+
'client_secret' => self.client_secret,
|
84
|
+
'redirect_uris' => self.redirect_uris,
|
85
|
+
'javascript_origins' => self.javascript_origins,
|
86
|
+
'auth_uri' => self.authorization_uri,
|
87
|
+
'token_uri' => self.token_credential_uri,
|
88
|
+
'access_token' => self.access_token,
|
89
|
+
'refresh_token' => self.refresh_token,
|
90
|
+
'id_token' => self.id_token,
|
91
|
+
'expires_in' => self.expires_in,
|
92
|
+
'expires_at' => self.expires_at,
|
93
|
+
'issued_at' => self.issued_at
|
94
|
+
}).inject({}) do |accu, (k, v)|
|
95
|
+
# Prunes empty values from JSON output.
|
96
|
+
unless v == nil || (v.respond_to?(:empty?) && v.empty?)
|
97
|
+
accu[k] = v
|
98
|
+
end
|
99
|
+
accu
|
100
|
+
end
|
101
|
+
})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -18,7 +18,7 @@ require 'addressable/uri'
|
|
18
18
|
require 'google/inflection'
|
19
19
|
require 'google/api_client/discovery/resource'
|
20
20
|
require 'google/api_client/discovery/method'
|
21
|
-
|
21
|
+
require 'google/api_client/discovery/media'
|
22
22
|
|
23
23
|
module Google
|
24
24
|
class APIClient
|
@@ -43,7 +43,7 @@ module Google
|
|
43
43
|
def initialize(document_base, discovery_document)
|
44
44
|
@document_base = Addressable::URI.parse(document_base)
|
45
45
|
@discovery_document = discovery_document
|
46
|
-
metaclass = (class <<self; self; end)
|
46
|
+
metaclass = (class << self; self; end)
|
47
47
|
self.discovered_resources.each do |resource|
|
48
48
|
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
|
49
49
|
if !self.respond_to?(method_name)
|
@@ -149,8 +149,7 @@ module Google
|
|
149
149
|
def method_base
|
150
150
|
if @discovery_document['basePath']
|
151
151
|
return @method_base ||= (
|
152
|
-
self.document_base
|
153
|
-
Addressable::URI.parse(@discovery_document['basePath'])
|
152
|
+
self.document_base.join(Addressable::URI.parse(@discovery_document['basePath']))
|
154
153
|
).normalize
|
155
154
|
else
|
156
155
|
return nil
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# Copyright 2010 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
|
+
|
16
|
+
require 'addressable/uri'
|
17
|
+
require 'addressable/template'
|
18
|
+
|
19
|
+
require 'google/api_client/errors'
|
20
|
+
|
21
|
+
|
22
|
+
module Google
|
23
|
+
class APIClient
|
24
|
+
##
|
25
|
+
# Media upload elements for discovered methods
|
26
|
+
class MediaUpload
|
27
|
+
|
28
|
+
##
|
29
|
+
# Creates a description of a particular method.
|
30
|
+
#
|
31
|
+
# @param [Google::APIClient::API] api
|
32
|
+
# Base discovery document for the API
|
33
|
+
# @param [Addressable::URI] method_base
|
34
|
+
# The base URI for the service.
|
35
|
+
# @param [Hash] discovery_document
|
36
|
+
# The media upload section of the discovery document.
|
37
|
+
#
|
38
|
+
# @return [Google::APIClient::Method] The constructed method object.
|
39
|
+
def initialize(api, method_base, discovery_document)
|
40
|
+
@api = api
|
41
|
+
@method_base = method_base
|
42
|
+
@discovery_document = discovery_document
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# List of acceptable mime types
|
47
|
+
#
|
48
|
+
# @return [Array]
|
49
|
+
# List of acceptable mime types for uploaded content
|
50
|
+
def accepted_types
|
51
|
+
@discovery_document['accept']
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Maximum size of an uplad
|
56
|
+
# TODO: Parse & convert to numeric value
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def max_size
|
60
|
+
@discovery_document['maxSize']
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the URI template for the method. A parameter list can be
|
65
|
+
# used to expand this into a URI.
|
66
|
+
#
|
67
|
+
# @return [Addressable::Template] The URI template.
|
68
|
+
def uri_template
|
69
|
+
return @uri_template ||= Addressable::Template.new(
|
70
|
+
@api.method_base.join(Addressable::URI.parse(@discovery_document['protocols']['simple']['path']))
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -95,15 +95,23 @@ module Google
|
|
95
95
|
#
|
96
96
|
# @return [Addressable::Template] The URI template.
|
97
97
|
def uri_template
|
98
|
-
# TODO(bobaman) We shouldn't be calling #to_s here, this should be
|
99
|
-
# a join operation on a URI, but we have to treat these as Strings
|
100
|
-
# because of the way the discovery document provides the URIs.
|
101
|
-
# This should be fixed soon.
|
102
98
|
return @uri_template ||= Addressable::Template.new(
|
103
|
-
self.method_base
|
99
|
+
self.method_base.join(Addressable::URI.parse(@discovery_document['path']))
|
104
100
|
)
|
105
101
|
end
|
106
102
|
|
103
|
+
##
|
104
|
+
# Returns media upload information for this method, if supported
|
105
|
+
#
|
106
|
+
# @return [Google::APIClient::MediaUpload] Description of upload endpoints
|
107
|
+
def media_upload
|
108
|
+
if @discovery_document['mediaUpload']
|
109
|
+
return @media_upload ||= Google::APIClient::MediaUpload.new(self, self.method_base, @discovery_document['mediaUpload'])
|
110
|
+
else
|
111
|
+
return nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
107
115
|
##
|
108
116
|
# Returns the Schema object for the method's request, if any.
|
109
117
|
#
|
@@ -168,7 +176,20 @@ module Google
|
|
168
176
|
parameters = self.normalize_parameters(parameters)
|
169
177
|
self.validate_parameters(parameters)
|
170
178
|
template_variables = self.uri_template.variables
|
171
|
-
|
179
|
+
upload_type = parameters.assoc('uploadType') || parameters.assoc('upload_type')
|
180
|
+
if upload_type
|
181
|
+
unless self.media_upload
|
182
|
+
raise ArgumentException, "Media upload not supported for this method"
|
183
|
+
end
|
184
|
+
case upload_type.last
|
185
|
+
when 'media', 'multipart', 'resumable'
|
186
|
+
uri = self.media_upload.uri_template.expand(parameters)
|
187
|
+
else
|
188
|
+
raise ArgumentException, "Invalid uploadType '#{upload_type}'"
|
189
|
+
end
|
190
|
+
else
|
191
|
+
uri = self.uri_template.expand(parameters)
|
192
|
+
end
|
172
193
|
query_parameters = parameters.reject do |k, v|
|
173
194
|
template_variables.include?(k)
|
174
195
|
end
|
@@ -211,6 +232,7 @@ module Google
|
|
211
232
|
req.body = body
|
212
233
|
end
|
213
234
|
end
|
235
|
+
|
214
236
|
|
215
237
|
##
|
216
238
|
# Returns a <code>Hash</code> of the parameter descriptions for
|
@@ -47,8 +47,13 @@ module Google
|
|
47
47
|
# and excess object creation, but this hopefully shouldn't be an
|
48
48
|
# issue since it should only be called only once per schema per
|
49
49
|
# process.
|
50
|
-
if data.kind_of?(Hash) &&
|
51
|
-
|
50
|
+
if data.kind_of?(Hash) &&
|
51
|
+
data['$ref'] && !data['$ref'].kind_of?(Hash)
|
52
|
+
if data['$ref'].respond_to?(:to_str)
|
53
|
+
reference = data['$ref'].to_str
|
54
|
+
else
|
55
|
+
raise TypeError, "Expected String, got #{data['$ref'].class}"
|
56
|
+
end
|
52
57
|
reference = '#' + reference if reference[0..0] != '#'
|
53
58
|
data.merge({
|
54
59
|
'$ref' => reference
|
@@ -16,15 +16,26 @@
|
|
16
16
|
module Google
|
17
17
|
class APIClient
|
18
18
|
module ENV
|
19
|
-
OS_VERSION =
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
19
|
+
OS_VERSION = begin
|
20
|
+
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
|
21
|
+
# TODO(bobaman)
|
22
|
+
# Confirm that all of these Windows environments actually have access
|
23
|
+
# to the `ver` command.
|
24
|
+
`ver`.sub(/\s*\[Version\s*/, '/').sub(']', '').strip
|
25
|
+
elsif RUBY_PLATFORM =~ /darwin/i
|
26
|
+
"Mac OS X/#{`sw_vers -productVersion`}"
|
27
|
+
elsif RUBY_PLATFORM == 'java'
|
28
|
+
# Get the information from java system properties to avoid spawning a
|
29
|
+
# sub-process, which is not friendly in some contexts (web servers).
|
30
|
+
require 'java'
|
31
|
+
name = java.lang.System.getProperty('os.name')
|
32
|
+
version = java.lang.System.getProperty('os.version')
|
33
|
+
"#{name}/#{version}"
|
34
|
+
else
|
35
|
+
`uname -sr`.sub(' ', '/')
|
36
|
+
end
|
37
|
+
rescue Exception
|
38
|
+
RUBY_PLATFORM
|
28
39
|
end
|
29
40
|
end
|
30
41
|
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# Copyright 2010 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
|
+
module Google
|
16
|
+
class APIClient
|
17
|
+
##
|
18
|
+
# Uploadable media support. Holds an IO stream & content type.
|
19
|
+
#
|
20
|
+
# @see Faraday::UploadIO
|
21
|
+
# @example
|
22
|
+
# media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
|
23
|
+
class UploadIO < Faraday::UploadIO
|
24
|
+
##
|
25
|
+
# Get the length of the stream
|
26
|
+
# @return [Integer]
|
27
|
+
# Length of stream, in bytes
|
28
|
+
def length
|
29
|
+
io.respond_to?(:length) ? io.length : File.size(local_path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Resumable uploader.
|
35
|
+
#
|
36
|
+
class ResumableUpload
|
37
|
+
attr_reader :result
|
38
|
+
attr_accessor :client
|
39
|
+
attr_accessor :chunk_size
|
40
|
+
attr_accessor :media
|
41
|
+
attr_accessor :location
|
42
|
+
|
43
|
+
##
|
44
|
+
# Creates a new uploader.
|
45
|
+
#
|
46
|
+
# @param [Google::APIClient::Result] result
|
47
|
+
# Result of the initial request that started the upload
|
48
|
+
# @param [Google::APIClient::UploadIO] media
|
49
|
+
# Media to upload
|
50
|
+
# @param [String] location
|
51
|
+
# URL to upload to
|
52
|
+
def initialize(result, media, location)
|
53
|
+
self.media = media
|
54
|
+
self.location = location
|
55
|
+
self.chunk_size = 256 * 1024
|
56
|
+
|
57
|
+
@api_method = result.reference.api_method
|
58
|
+
@result = result
|
59
|
+
@offset = 0
|
60
|
+
@complete = false
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Sends all remaining chunks to the server
|
65
|
+
#
|
66
|
+
# @param [Google::APIClient] api_client
|
67
|
+
# API Client instance to use for sending
|
68
|
+
def send_all(api_client)
|
69
|
+
until complete?
|
70
|
+
send_chunk(api_client)
|
71
|
+
break unless result.status == 308
|
72
|
+
end
|
73
|
+
return result
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
##
|
78
|
+
# Sends the next chunk to the server
|
79
|
+
#
|
80
|
+
# @param [Google::APIClient] api_client
|
81
|
+
# API Client instance to use for sending
|
82
|
+
def send_chunk(api_client)
|
83
|
+
if @offset.nil?
|
84
|
+
return resync_range(api_client)
|
85
|
+
end
|
86
|
+
|
87
|
+
start_offset = @offset
|
88
|
+
self.media.io.pos = start_offset
|
89
|
+
chunk = self.media.io.read(chunk_size)
|
90
|
+
content_length = chunk.bytesize
|
91
|
+
|
92
|
+
end_offset = start_offset + content_length - 1
|
93
|
+
@result = api_client.execute(
|
94
|
+
:uri => self.location,
|
95
|
+
:http_method => :put,
|
96
|
+
:headers => {
|
97
|
+
'Content-Length' => "#{content_length}",
|
98
|
+
'Content-Type' => self.media.content_type,
|
99
|
+
'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" },
|
100
|
+
:body => chunk)
|
101
|
+
return process_result(@result)
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Check if upload is complete
|
106
|
+
#
|
107
|
+
# @return [TrueClass, FalseClass]
|
108
|
+
# Whether or not the upload complete successfully
|
109
|
+
def complete?
|
110
|
+
return @complete
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Check if the upload URL expired (upload not completed in alotted time.)
|
115
|
+
# Expired uploads must be restarted from the beginning
|
116
|
+
#
|
117
|
+
# @return [TrueClass, FalseClass]
|
118
|
+
# Whether or not the upload has expired and can not be resumed
|
119
|
+
def expired?
|
120
|
+
return @result.status == 404 || @result.status == 410
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Get the last saved range from the server in case an error occurred
|
125
|
+
# and the offset is not known.
|
126
|
+
#
|
127
|
+
# @param [Google::APIClient] api_client
|
128
|
+
# API Client instance to use for sending
|
129
|
+
def resync_range(api_client)
|
130
|
+
r = api_client.execute(
|
131
|
+
:uri => self.location,
|
132
|
+
:http_method => :put,
|
133
|
+
:headers => {
|
134
|
+
'Content-Length' => "0",
|
135
|
+
'Content-Range' => "bytes */#{media.length}" })
|
136
|
+
return process_result(r)
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Check the result from the server, updating the offset and/or location
|
141
|
+
# if available.
|
142
|
+
#
|
143
|
+
# @param [Google::APIClient::Result] r
|
144
|
+
# Result of a chunk upload or range query
|
145
|
+
def process_result(result)
|
146
|
+
case result.status
|
147
|
+
when 200...299
|
148
|
+
@complete = true
|
149
|
+
if @api_method
|
150
|
+
# Inject the original API method so data is parsed correctly
|
151
|
+
result.reference.api_method = @api_method
|
152
|
+
end
|
153
|
+
return result
|
154
|
+
when 308
|
155
|
+
range = result.headers['range']
|
156
|
+
if range
|
157
|
+
@offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
|
158
|
+
end
|
159
|
+
if result.headers['location']
|
160
|
+
self.location = result.headers['location']
|
161
|
+
end
|
162
|
+
when 500...599
|
163
|
+
# Invalidate the offset to mark it needs to be queried on the
|
164
|
+
# next request
|
165
|
+
@offset = nil
|
166
|
+
end
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -25,6 +25,8 @@ require 'google/api_client/discovery'
|
|
25
25
|
module Google
|
26
26
|
class APIClient
|
27
27
|
class Reference
|
28
|
+
|
29
|
+
MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
|
28
30
|
def initialize(options={})
|
29
31
|
# We only need this to do lookups on method ID String values
|
30
32
|
# It's optional, but method ID lookups will fail if the client is
|
@@ -39,20 +41,53 @@ module Google
|
|
39
41
|
# parameters to the API method, but rather to the API system.
|
40
42
|
self.parameters['key'] ||= options[:key] if options[:key]
|
41
43
|
self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
|
42
|
-
self.headers = options[:headers] ||
|
43
|
-
|
44
|
+
self.headers = options[:headers] || {}
|
45
|
+
|
46
|
+
if options[:media]
|
47
|
+
self.media = options[:media]
|
48
|
+
upload_type = parameters['uploadType'] || parameters['upload_type']
|
49
|
+
case upload_type
|
50
|
+
when "media"
|
51
|
+
if options[:body] || options[:body_object]
|
52
|
+
raise ArgumentError, "Can not specify body & body object for simple uploads"
|
53
|
+
end
|
54
|
+
self.headers['Content-Type'] ||= self.media.content_type
|
55
|
+
self.body = self.media
|
56
|
+
when "multipart"
|
57
|
+
unless options[:body_object]
|
58
|
+
raise ArgumentError, "Multipart requested but no body object"
|
59
|
+
end
|
60
|
+
# This is all a bit of a hack due to signet requiring body to be a string
|
61
|
+
# Ideally, update signet to delay serialization so we can just pass
|
62
|
+
# streams all the way down through to the HTTP lib
|
63
|
+
metadata = StringIO.new(serialize_body(options[:body_object]))
|
64
|
+
env = {
|
65
|
+
:request_headers => {'Content-Type' => "multipart/related;boundary=#{MULTIPART_BOUNDARY}"},
|
66
|
+
:request => { :boundary => MULTIPART_BOUNDARY }
|
67
|
+
}
|
68
|
+
multipart = Faraday::Request::Multipart.new
|
69
|
+
self.body = multipart.create_multipart(env, [
|
70
|
+
[nil,Faraday::UploadIO.new(metadata, 'application/json', 'file.json')],
|
71
|
+
[nil, self.media]])
|
72
|
+
self.headers.update(env[:request_headers])
|
73
|
+
when "resumable"
|
74
|
+
file_length = self.media.length
|
75
|
+
self.headers['X-Upload-Content-Type'] = self.media.content_type
|
76
|
+
self.headers['X-Upload-Content-Length'] = file_length.to_s
|
77
|
+
if options[:body_object]
|
78
|
+
self.headers['Content-Type'] ||= 'application/json'
|
79
|
+
self.body = serialize_body(options[:body_object])
|
80
|
+
else
|
81
|
+
self.body = ''
|
82
|
+
end
|
83
|
+
else
|
84
|
+
raise ArgumentError, "Invalid uploadType for media"
|
85
|
+
end
|
86
|
+
elsif options[:body]
|
44
87
|
self.body = options[:body]
|
45
88
|
elsif options[:body_object]
|
46
|
-
|
47
|
-
|
48
|
-
elsif options[:body_object].respond_to?(:to_hash)
|
49
|
-
serialized_body = MultiJson.encode(options[:body_object].to_hash)
|
50
|
-
else
|
51
|
-
raise TypeError,
|
52
|
-
'Could not convert body object to JSON.' +
|
53
|
-
'Must respond to :to_json or :to_hash.'
|
54
|
-
end
|
55
|
-
self.body = serialized_body
|
89
|
+
self.headers['Content-Type'] ||= 'application/json'
|
90
|
+
self.body = serialize_body(options[:body_object])
|
56
91
|
else
|
57
92
|
self.body = ''
|
58
93
|
end
|
@@ -65,7 +100,22 @@ module Google
|
|
65
100
|
end
|
66
101
|
end
|
67
102
|
end
|
68
|
-
|
103
|
+
|
104
|
+
def serialize_body(body)
|
105
|
+
return body.to_json if body.respond_to?(:to_json)
|
106
|
+
return MultiJson.encode(options[:body_object].to_hash) if body.respond_to?(:to_hash)
|
107
|
+
raise TypeError, 'Could not convert body object to JSON.' +
|
108
|
+
'Must respond to :to_json or :to_hash.'
|
109
|
+
end
|
110
|
+
|
111
|
+
def media
|
112
|
+
return @media
|
113
|
+
end
|
114
|
+
|
115
|
+
def media=(media)
|
116
|
+
@media = (media)
|
117
|
+
end
|
118
|
+
|
69
119
|
def connection
|
70
120
|
return @connection
|
71
121
|
end
|
@@ -132,18 +182,20 @@ module Google
|
|
132
182
|
def body=(new_body)
|
133
183
|
if new_body.respond_to?(:to_str)
|
134
184
|
@body = new_body.to_str
|
185
|
+
elsif new_body.respond_to?(:read)
|
186
|
+
@body = new_body.read()
|
135
187
|
elsif new_body.respond_to?(:inject)
|
136
188
|
@body = (new_body.inject(StringIO.new) do |accu, chunk|
|
137
189
|
accu.write(chunk)
|
138
190
|
accu
|
139
191
|
end).string
|
140
192
|
else
|
141
|
-
raise TypeError, "Expected body to be String or Enumerable chunks."
|
193
|
+
raise TypeError, "Expected body to be String, IO, or Enumerable chunks."
|
142
194
|
end
|
143
195
|
end
|
144
196
|
|
145
197
|
def headers
|
146
|
-
return @headers ||=
|
198
|
+
return @headers ||= {}
|
147
199
|
end
|
148
200
|
|
149
201
|
def headers=(new_headers)
|
@@ -42,12 +42,24 @@ module Google
|
|
42
42
|
return @response.body
|
43
43
|
end
|
44
44
|
|
45
|
+
def resumable_upload
|
46
|
+
@media_upload ||= Google::APIClient::ResumableUpload.new(self, reference.media, self.headers['location'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def media_type
|
50
|
+
_, content_type = self.headers.detect do |h, v|
|
51
|
+
h.downcase == 'Content-Type'.downcase
|
52
|
+
end
|
53
|
+
content_type[/^([^;]*);?.*$/, 1].strip.downcase
|
54
|
+
end
|
55
|
+
|
56
|
+
def data?
|
57
|
+
self.media_type == 'application/json'
|
58
|
+
end
|
59
|
+
|
45
60
|
def data
|
46
61
|
return @data ||= (begin
|
47
|
-
|
48
|
-
h.downcase == 'Content-Type'.downcase
|
49
|
-
end
|
50
|
-
media_type = content_type[/^([^;]*);?.*$/, 1].strip.downcase
|
62
|
+
media_type = self.media_type
|
51
63
|
data = self.body
|
52
64
|
case media_type
|
53
65
|
when 'application/json'
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: google-api-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.4.
|
5
|
+
version: 0.4.3
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Bob Aman
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-03-27 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: signet
|
@@ -156,13 +156,16 @@ extensions: []
|
|
156
156
|
extra_rdoc_files:
|
157
157
|
- README.md
|
158
158
|
files:
|
159
|
+
- lib/google/api_client/client_secrets.rb
|
159
160
|
- lib/google/api_client/discovery/api.rb
|
161
|
+
- lib/google/api_client/discovery/media.rb
|
160
162
|
- lib/google/api_client/discovery/method.rb
|
161
163
|
- lib/google/api_client/discovery/resource.rb
|
162
164
|
- lib/google/api_client/discovery/schema.rb
|
163
165
|
- lib/google/api_client/discovery.rb
|
164
166
|
- lib/google/api_client/environment.rb
|
165
167
|
- lib/google/api_client/errors.rb
|
168
|
+
- lib/google/api_client/media.rb
|
166
169
|
- lib/google/api_client/reference.rb
|
167
170
|
- lib/google/api_client/result.rb
|
168
171
|
- lib/google/api_client/version.rb
|