cloudinary 1.1.1 → 1.1.2
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/cloudinary.gemspec +1 -0
- data/lib/cloudinary.rb +21 -2
- data/lib/cloudinary/api.rb +126 -94
- data/lib/cloudinary/helper.rb +17 -4
- data/lib/cloudinary/uploader.rb +190 -167
- data/lib/cloudinary/utils.rb +138 -14
- data/lib/cloudinary/version.rb +1 -1
- data/spec/api_spec.rb +168 -115
- data/spec/archive_spec.rb +102 -0
- data/spec/cloudinary_spec.rb +17 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/uploader_spec.rb +90 -44
- data/spec/utils_spec.rb +183 -176
- data/spec/video_tag_spec.rb +7 -2
- metadata +20 -2
data/lib/cloudinary/uploader.rb
CHANGED
@@ -3,117 +3,135 @@ require 'rest_client'
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
class Cloudinary::Uploader
|
6
|
-
|
6
|
+
|
7
|
+
# @deprecated use {Cloudinary::Utils.build_eager} instead
|
7
8
|
def self.build_eager(eager)
|
8
|
-
|
9
|
-
Cloudinary::Utils.build_array(eager).map do
|
10
|
-
|transformation, format|
|
11
|
-
transformation = transformation.clone
|
12
|
-
format = transformation.delete(:format) || format
|
13
|
-
[Cloudinary::Utils.generate_transformation_string(transformation), format].compact.join("/")
|
14
|
-
end.join("|")
|
9
|
+
Cloudinary::Utils.build_eager(eager)
|
15
10
|
end
|
16
|
-
|
11
|
+
|
12
|
+
# @private
|
17
13
|
def self.build_upload_params(options)
|
18
14
|
#symbolize keys
|
19
15
|
options = options.clone
|
20
|
-
options.keys.each{|key| options[key.to_sym] = options.delete(key) if key.is_a?(String)}
|
21
|
-
|
22
|
-
params = {
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
16
|
+
options.keys.each { |key| options[key.to_sym] = options.delete(key) if key.is_a?(String) }
|
17
|
+
|
18
|
+
params = {
|
19
|
+
:allowed_formats => Cloudinary::Utils.build_array(options[:allowed_formats]).join(","),
|
20
|
+
:auto_tagging => options[:auto_tagging] && options[:auto_tagging].to_f,
|
21
|
+
:background_removal => options[:background_removal],
|
22
|
+
:backup => Cloudinary::Utils.as_safe_bool(options[:backup]),
|
23
|
+
:callback => options[:callback],
|
24
|
+
:categorization => options[:categorization],
|
25
|
+
:colors => Cloudinary::Utils.as_safe_bool(options[:colors]),
|
26
|
+
:context => Cloudinary::Utils.encode_hash(options[:context]),
|
27
|
+
:custom_coordinates => Cloudinary::Utils.encode_double_array(options[:custom_coordinates]),
|
28
|
+
:detection => options[:detection],
|
29
|
+
:discard_original_filename => Cloudinary::Utils.as_safe_bool(options[:discard_original_filename]),
|
30
|
+
:eager => Cloudinary::Utils.build_eager(options[:eager]),
|
31
|
+
:eager_async => Cloudinary::Utils.as_safe_bool(options[:eager_async]),
|
32
|
+
:eager_notification_url => options[:eager_notification_url],
|
33
|
+
:exif => Cloudinary::Utils.as_safe_bool(options[:exif]),
|
34
|
+
:face_coordinates => Cloudinary::Utils.encode_double_array(options[:face_coordinates]),
|
35
|
+
:faces => Cloudinary::Utils.as_safe_bool(options[:faces]),
|
36
|
+
:folder => options[:folder],
|
37
|
+
:format => options[:format],
|
38
|
+
:headers => build_custom_headers(options[:headers]),
|
39
|
+
:image_metadata => Cloudinary::Utils.as_safe_bool(options[:image_metadata]),
|
40
|
+
:invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate]),
|
41
|
+
:moderation => options[:moderation],
|
42
|
+
:notification_url => options[:notification_url],
|
43
|
+
:ocr => options[:ocr],
|
44
|
+
:overwrite => Cloudinary::Utils.as_safe_bool(options[:overwrite]),
|
45
|
+
:phash => Cloudinary::Utils.as_safe_bool(options[:phash]),
|
46
|
+
:proxy => options[:proxy],
|
47
|
+
:public_id => options[:public_id],
|
48
|
+
:raw_convert => options[:raw_convert],
|
49
|
+
:responsive_breakpoints => Cloudinary::Utils.generate_responsive_breakpoints_string(options[:responsive_breakpoints]),
|
50
|
+
:return_delete_token => Cloudinary::Utils.as_safe_bool(options[:return_delete_token]),
|
51
|
+
:similarity_search => options[:similarity_search],
|
52
|
+
:tags => options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
|
53
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
54
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options.clone),
|
55
|
+
:type => options[:type],
|
56
|
+
:unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
|
57
|
+
:upload_preset => options[:upload_preset],
|
58
|
+
:use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
|
59
|
+
}
|
60
|
+
params
|
63
61
|
end
|
64
|
-
|
62
|
+
|
63
|
+
# @private
|
64
|
+
def self.build_explicit_api_params(public_id, options = {})
|
65
|
+
options = Cloudinary::Utils.symbolize_keys options
|
66
|
+
params = {
|
67
|
+
:callback => options[:callback],
|
68
|
+
:eager => Cloudinary::Utils.build_eager(options[:eager]),
|
69
|
+
:eager_async => Cloudinary::Utils.as_safe_bool(options[:eager_async]),
|
70
|
+
:eager_notification_url => options[:eager_notification_url],
|
71
|
+
:face_coordinates => options[:face_coordinates] && Cloudinary::Utils.encode_double_array(options[:face_coordinates]),
|
72
|
+
:headers => build_custom_headers(options[:headers]),
|
73
|
+
:invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate]),
|
74
|
+
:public_id => public_id,
|
75
|
+
:responsive_breakpoints => Cloudinary::Utils.generate_responsive_breakpoints_string(options[:responsive_breakpoints]),
|
76
|
+
:tags => options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
|
77
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
78
|
+
:type => options[:type]
|
79
|
+
}
|
80
|
+
params
|
81
|
+
end
|
82
|
+
|
65
83
|
def self.unsigned_upload(file, upload_preset, options={})
|
66
84
|
upload(file, options.merge(:unsigned => true, :upload_preset => upload_preset))
|
67
85
|
end
|
68
|
-
|
86
|
+
|
69
87
|
def self.upload(file, options={})
|
70
|
-
call_api("upload", options) do
|
88
|
+
call_api("upload", options) do
|
71
89
|
params = build_upload_params(options)
|
72
90
|
if file.is_a?(Pathname)
|
73
91
|
params[:file] = File.open(file, "rb")
|
74
92
|
elsif file.respond_to?(:read) || file =~ /^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$/
|
75
93
|
params[:file] = file
|
76
|
-
else
|
94
|
+
else
|
77
95
|
params[:file] = File.open(file, "rb")
|
78
96
|
end
|
79
97
|
[params, [:file]]
|
80
|
-
end
|
98
|
+
end
|
81
99
|
end
|
82
100
|
|
83
101
|
# Upload large files. Note that public_id should include an extension for best results.
|
84
102
|
def self.upload_large(file, public_id_or_options={}, old_options={})
|
85
103
|
if public_id_or_options.is_a?(Hash)
|
86
|
-
options
|
104
|
+
options = public_id_or_options
|
87
105
|
public_id = options[:public_id]
|
88
106
|
else
|
89
107
|
public_id = public_id_or_options
|
90
|
-
options
|
91
|
-
end
|
108
|
+
options = old_options
|
109
|
+
end
|
92
110
|
if file.is_a?(Pathname) || !file.respond_to?(:read)
|
93
111
|
filename = file
|
94
|
-
file
|
112
|
+
file = File.open(file, "rb")
|
95
113
|
else
|
96
114
|
filename = "cloudinaryfile"
|
97
115
|
end
|
98
|
-
upload
|
99
|
-
index
|
116
|
+
upload = nil
|
117
|
+
index = 0
|
100
118
|
chunk_size = options[:chunk_size] || 20_000_000
|
101
|
-
|
102
|
-
buffer
|
119
|
+
until file.eof?
|
120
|
+
buffer = file.read(chunk_size)
|
103
121
|
current_loc = index*chunk_size
|
104
|
-
range
|
105
|
-
upload
|
106
|
-
public_id
|
107
|
-
index
|
122
|
+
range = "bytes #{current_loc}-#{current_loc+buffer.size - 1}/#{file.size}"
|
123
|
+
upload = upload_large_part(Cloudinary::Blob.new(buffer, :original_filename => filename), options.merge(:public_id => public_id, :content_range => range))
|
124
|
+
public_id = upload["public_id"]
|
125
|
+
index += 1
|
108
126
|
end
|
109
127
|
upload
|
110
128
|
end
|
111
|
-
|
129
|
+
|
112
130
|
|
113
131
|
# Upload large files. Note that public_id should include an extension for best results.
|
114
132
|
def self.upload_large_part(file, options={})
|
115
133
|
options[:resource_type] ||= :raw
|
116
|
-
call_api("upload", options) do
|
134
|
+
call_api("upload", options) do
|
117
135
|
params = build_upload_params(options)
|
118
136
|
if file.is_a?(Pathname) || !file.respond_to?(:read)
|
119
137
|
params[:file] = File.open(file, "rb")
|
@@ -121,168 +139,173 @@ class Cloudinary::Uploader
|
|
121
139
|
params[:file] = file
|
122
140
|
end
|
123
141
|
[params, [:file]]
|
124
|
-
end
|
142
|
+
end
|
125
143
|
end
|
126
144
|
|
127
145
|
def self.destroy(public_id, options={})
|
128
|
-
call_api("destroy", options) do
|
146
|
+
call_api("destroy", options) do
|
129
147
|
{
|
130
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
131
|
-
:type=>options[:type],
|
132
|
-
:public_id=> public_id,
|
133
|
-
:invalidate=>options[:invalidate],
|
148
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
149
|
+
:type => options[:type],
|
150
|
+
:public_id => public_id,
|
151
|
+
:invalidate => options[:invalidate],
|
134
152
|
}
|
135
|
-
end
|
153
|
+
end
|
136
154
|
end
|
137
155
|
|
138
156
|
def self.rename(from_public_id, to_public_id, options={})
|
139
|
-
call_api("rename", options) do
|
157
|
+
call_api("rename", options) do
|
140
158
|
{
|
141
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
142
|
-
:type=>options[:type],
|
143
|
-
:overwrite=>options[:overwrite],
|
144
|
-
:from_public_id=>from_public_id,
|
145
|
-
:to_public_id=>to_public_id,
|
159
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
160
|
+
:type => options[:type],
|
161
|
+
:overwrite => Cloudinary::Utils.as_safe_bool(options[:overwrite]),
|
162
|
+
:from_public_id => from_public_id,
|
163
|
+
:to_public_id => to_public_id,
|
164
|
+
:invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate])
|
146
165
|
}
|
147
|
-
end
|
166
|
+
end
|
148
167
|
end
|
149
168
|
|
150
169
|
def self.exists?(public_id, options={})
|
151
170
|
cloudinary_url = Cloudinary::Utils.cloudinary_url(public_id, options)
|
152
171
|
begin
|
153
|
-
RestClient::Request.execute(:method => :head, :url => cloudinary_url, :timeout=>5).code.to_s =~ /2\d{2}/
|
154
|
-
rescue RestClient::ResourceNotFound
|
172
|
+
RestClient::Request.execute(:method => :head, :url => cloudinary_url, :timeout => 5).code.to_s =~ /2\d{2}/
|
173
|
+
rescue RestClient::ResourceNotFound
|
155
174
|
return false
|
156
175
|
end
|
157
|
-
|
176
|
+
|
158
177
|
end
|
159
178
|
|
160
179
|
def self.explicit(public_id, options={})
|
161
|
-
call_api("explicit", options) do
|
162
|
-
|
163
|
-
|
164
|
-
:type=>options[:type],
|
165
|
-
:public_id=> public_id,
|
166
|
-
:callback=> options[:callback],
|
167
|
-
:eager=>build_eager(options[:eager]),
|
168
|
-
:eager_notification_url=>options[:eager_notification_url],
|
169
|
-
:eager_async=>Cloudinary::Utils.as_safe_bool(options[:eager_async]),
|
170
|
-
:headers=>build_custom_headers(options[:headers]),
|
171
|
-
:tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
|
172
|
-
:face_coordinates => options[:face_coordinates] && Cloudinary::Utils.encode_double_array(options[:face_coordinates])
|
173
|
-
}
|
174
|
-
end
|
180
|
+
call_api("explicit", options) do
|
181
|
+
self.build_explicit_api_params(public_id, options)
|
182
|
+
end
|
175
183
|
end
|
176
|
-
|
177
|
-
|
184
|
+
|
185
|
+
# Creates a new archive in the server and returns information in JSON format
|
186
|
+
def self.create_archive(options={}, target_format = nil)
|
187
|
+
call_api("generate_archive", options) do
|
188
|
+
opt = Cloudinary::Utils.archive_params(options)
|
189
|
+
opt[:target_format] = target_format if target_format
|
190
|
+
opt
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Creates a new zip archive in the server and returns information in JSON format
|
195
|
+
def self.create_zip(options={})
|
196
|
+
create_archive(options, "zip")
|
197
|
+
end
|
198
|
+
|
199
|
+
TEXT_PARAMS = [:public_id, :font_family, :font_size, :font_color, :text_align, :font_weight, :font_style, :background, :opacity, :text_decoration, :line_spacing]
|
200
|
+
|
178
201
|
def self.text(text, options={})
|
179
202
|
call_api("text", options) do
|
180
|
-
params = {:timestamp => Time.now.to_i, :text=>text}
|
181
|
-
TEXT_PARAMS.each{|k| params[k] = options[k]
|
203
|
+
params = { :timestamp => Time.now.to_i, :text => text }
|
204
|
+
TEXT_PARAMS.each { |k| params[k] = options[k] unless options[k].nil? }
|
182
205
|
params
|
183
206
|
end
|
184
|
-
end
|
185
|
-
|
207
|
+
end
|
208
|
+
|
186
209
|
def self.generate_sprite(tag, options={})
|
187
210
|
version_store = options.delete(:version_store)
|
188
|
-
|
211
|
+
|
189
212
|
result = call_api("sprite", options) do
|
190
213
|
{
|
191
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
192
|
-
:tag=>tag,
|
193
|
-
:async=>options[:async],
|
194
|
-
:notification_url=>options[:notification_url],
|
195
|
-
:transformation
|
196
|
-
}
|
214
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
215
|
+
:tag => tag,
|
216
|
+
:async => options[:async],
|
217
|
+
:notification_url => options[:notification_url],
|
218
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options.merge(:fetch_format => options[:format]))
|
219
|
+
}
|
197
220
|
end
|
198
|
-
|
221
|
+
|
199
222
|
if version_store == :file && result && result["version"]
|
200
223
|
if defined?(Rails) && defined?(Rails.root)
|
201
224
|
FileUtils.mkdir_p("#{Rails.root}/tmp/cloudinary")
|
202
|
-
File.open("#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{tag}.version", "w"){|file| file.print result["version"].to_s}
|
203
|
-
end
|
204
|
-
end
|
225
|
+
File.open("#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{tag}.version", "w") { |file| file.print result["version"].to_s }
|
226
|
+
end
|
227
|
+
end
|
205
228
|
return result
|
206
229
|
end
|
207
230
|
|
208
231
|
def self.multi(tag, options={})
|
209
232
|
call_api("multi", options) do
|
210
233
|
{
|
211
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
212
|
-
:tag=>tag,
|
213
|
-
:format=>options[:format],
|
214
|
-
:async=>options[:async],
|
215
|
-
:notification_url=>options[:notification_url],
|
216
|
-
:transformation
|
217
|
-
}
|
234
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
235
|
+
:tag => tag,
|
236
|
+
:format => options[:format],
|
237
|
+
:async => options[:async],
|
238
|
+
:notification_url => options[:notification_url],
|
239
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options.clone)
|
240
|
+
}
|
218
241
|
end
|
219
242
|
end
|
220
|
-
|
221
|
-
def self.explode(public_id, options={})
|
243
|
+
|
244
|
+
def self.explode(public_id, options={})
|
222
245
|
call_api("explode", options) do
|
223
246
|
{
|
224
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
225
|
-
:public_id=>public_id,
|
226
|
-
:type=>options[:type],
|
227
|
-
:format=>options[:format],
|
228
|
-
:notification_url=>options[:notification_url],
|
229
|
-
:transformation
|
230
|
-
}
|
247
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
248
|
+
:public_id => public_id,
|
249
|
+
:type => options[:type],
|
250
|
+
:format => options[:format],
|
251
|
+
:notification_url => options[:notification_url],
|
252
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options.clone)
|
253
|
+
}
|
231
254
|
end
|
232
255
|
end
|
233
|
-
|
256
|
+
|
234
257
|
# options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
|
235
258
|
def self.add_tag(tag, public_ids = [], options = {})
|
236
259
|
exclusive = options.delete(:exclusive)
|
237
|
-
command
|
238
|
-
return self.call_tags_api(tag, command, public_ids, options)
|
260
|
+
command = exclusive ? "set_exclusive" : "add"
|
261
|
+
return self.call_tags_api(tag, command, public_ids, options)
|
239
262
|
end
|
240
263
|
|
241
264
|
def self.remove_tag(tag, public_ids = [], options = {})
|
242
|
-
return self.call_tags_api(tag, "remove", public_ids, options)
|
265
|
+
return self.call_tags_api(tag, "remove", public_ids, options)
|
243
266
|
end
|
244
267
|
|
245
268
|
def self.replace_tag(tag, public_ids = [], options = {})
|
246
|
-
return self.call_tags_api(tag, "replace", public_ids, options)
|
269
|
+
return self.call_tags_api(tag, "replace", public_ids, options)
|
247
270
|
end
|
248
|
-
|
271
|
+
|
249
272
|
private
|
250
|
-
|
273
|
+
|
251
274
|
def self.call_tags_api(tag, command, public_ids = [], options = {})
|
252
275
|
return call_api("tags", options) do
|
253
276
|
{
|
254
|
-
:timestamp=>(options[:timestamp] || Time.now.to_i),
|
255
|
-
:tag=>tag,
|
277
|
+
:timestamp => (options[:timestamp] || Time.now.to_i),
|
278
|
+
:tag => tag,
|
256
279
|
:public_ids => Cloudinary::Utils.build_array(public_ids),
|
257
|
-
:command
|
258
|
-
:type
|
259
|
-
}
|
260
|
-
end
|
280
|
+
:command => command,
|
281
|
+
:type => options[:type]
|
282
|
+
}
|
283
|
+
end
|
261
284
|
end
|
262
|
-
|
285
|
+
|
263
286
|
def self.call_api(action, options)
|
264
|
-
options
|
287
|
+
options = options.clone
|
265
288
|
return_error = options.delete(:return_error)
|
266
289
|
|
267
290
|
params, non_signable = yield
|
268
|
-
non_signable
|
269
|
-
|
291
|
+
non_signable ||= []
|
292
|
+
|
270
293
|
unless options[:unsigned]
|
271
|
-
api_key
|
272
|
-
api_secret
|
273
|
-
params[:signature] = Cloudinary::Utils.api_sign_request(params.reject{|k,v| non_signable.include?(k)}, api_secret)
|
274
|
-
params[:api_key]
|
294
|
+
api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
|
295
|
+
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
|
296
|
+
params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
|
297
|
+
params[:api_key] = api_key
|
275
298
|
end
|
276
299
|
timeout = options[:timeout] || Cloudinary.config.timeout || 60
|
277
300
|
|
278
301
|
result = nil
|
279
|
-
|
280
|
-
api_url
|
281
|
-
headers
|
302
|
+
|
303
|
+
api_url = Cloudinary::Utils.cloudinary_api_url(action, options)
|
304
|
+
headers = { "User-Agent" => Cloudinary::USER_AGENT }
|
282
305
|
headers['Content-Range'] = options[:content_range] if options[:content_range]
|
283
|
-
RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject{|k, v| v.nil? || v==""}, :timeout=> timeout, :headers => headers) do
|
284
|
-
|
285
|
-
raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}"
|
306
|
+
RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers) do
|
307
|
+
|response, request, tmpresult|
|
308
|
+
raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" unless [200, 400, 401, 403, 404, 500].include?(response.code)
|
286
309
|
begin
|
287
310
|
result = Cloudinary::Utils.json_decode(response.body)
|
288
311
|
rescue => e
|
@@ -295,13 +318,13 @@ class Cloudinary::Uploader
|
|
295
318
|
else
|
296
319
|
raise CloudinaryException, result["error"]["message"]
|
297
320
|
end
|
298
|
-
end
|
321
|
+
end
|
299
322
|
end
|
300
|
-
|
301
|
-
result
|
323
|
+
|
324
|
+
result
|
302
325
|
end
|
303
|
-
|
326
|
+
|
304
327
|
def self.build_custom_headers(headers)
|
305
|
-
Array(headers).map{|*a| a.join(": ")}.join("\n")
|
328
|
+
Array(headers).map { |*a| a.join(": ") }.join("\n")
|
306
329
|
end
|
307
330
|
end
|