google-api-client 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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