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