google-api-client 0.6.4 → 0.7.0.rc2
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.
- 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])
|