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.
@@ -1,3 +1,9 @@
1
+ # 0.4.3
2
+
3
+ * Added media upload capabilities
4
+ * Support serializing OAuth credentials to client_secrets.json
5
+ * Fixed OS name/version string on JRuby
6
+
1
7
  # 0.4.2
2
8
 
3
9
  * Fixed incompatibility with Ruby 1.8.7
@@ -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 = options["authorization"] || :oauth_2
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. Should almost always be 'www.googleapis.com'.
182
+ # The API hostname. Should almost always be 'www.googleapis.com'.
168
183
  attr_accessor :host
169
184
 
170
185
  ##
171
- # The user agent used by the client.
186
+ # The port number used by the client.
172
187
  #
173
188
  # @return [String]
174
- # The user agent string used in the User-Agent header.
175
- attr_accessor :user_agent
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
- template = Addressable::Template.new(
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}"] ||= (begin
211
- template = Addressable::Template.new(
212
- "https://{host}/discovery/v1/apis/" +
213
- "{api}/{version}/rest"
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
- template.expand({
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.respond_to?(:error) &&
683
- result.data.error.respond_to?(:message)
684
- # You're going to get a terrible error message if the response isn't
685
- # parsed successfully as an error.
686
- error_message = result.data.error.message
687
- elsif result.data['error'] && result.data['error']['message']
688
- error_message = result.data['error']['message']
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 + @discovery_document['path']
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
- uri = self.uri_template.expand(parameters)
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) && data['$ref']
51
- reference = data['$ref']
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 = if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
20
- # TODO(bobaman)
21
- # Confirm that all of these Windows environments actually have access
22
- # to the `ver` command.
23
- `ver`.sub(/\s*\[Version\s*/, '/').sub(']', '').strip
24
- elsif RUBY_PLATFORM =~ /darwin/i
25
- "Mac OS X/#{`sw_vers -productVersion`}"
26
- else
27
- `uname -sr`.sub(' ', '/')
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
- if options[:body]
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
- if options[:body_object].respond_to?(:to_json)
47
- serialized_body = options[:body_object].to_json
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
- _, content_type = self.headers.detect do |h, v|
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'
@@ -22,7 +22,7 @@ if !defined?(::Google::APIClient::VERSION)
22
22
  module VERSION
23
23
  MAJOR = 0
24
24
  MINOR = 4
25
- TINY = 2
25
+ TINY = 3
26
26
 
27
27
  STRING = [MAJOR, MINOR, TINY].join('.')
28
28
  end
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.2
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-02-22 00:00:00 Z
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