curb 1.3.5 → 1.3.6

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.
data/lib/curl/multi.rb CHANGED
@@ -1,9 +1,19 @@
1
1
  # frozen_string_literal: true
2
+ require 'curl/download'
2
3
  module Curl
3
4
  class Multi
4
5
  class DownloadError < RuntimeError
5
6
  attr_accessor :errors
6
7
  end
8
+
9
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP = begin
10
+ probe = ObjectSpace::WeakMap.new
11
+ probe[Object.new.freeze] = true
12
+ true
13
+ rescue ArgumentError, FrozenError, NameError
14
+ false
15
+ end
16
+
7
17
  class << self
8
18
  # call-seq:
9
19
  # Curl::Multi.get(['url1','url2','url3','url4','url5'], :follow_location => true) do|easy|
@@ -102,6 +112,7 @@ module Curl
102
112
  url = c.delete(:url)
103
113
  method = c.delete(:method)
104
114
  headers = c.delete(:headers)
115
+ internal_info = c.delete(:__curb_internal_info)
105
116
 
106
117
  easy = Curl::Easy.new if easy.nil?
107
118
 
@@ -140,7 +151,13 @@ module Curl
140
151
 
141
152
  easy.on_complete {|curl|
142
153
  free_handles << curl
143
- blk.call(curl,curl.response_code,method) if blk
154
+ if blk
155
+ if internal_info
156
+ blk.call(curl,curl.response_code,method,internal_info)
157
+ else
158
+ blk.call(curl,curl.response_code,method)
159
+ end
160
+ end
144
161
  }
145
162
  m.add(easy)
146
163
  end
@@ -182,73 +199,122 @@ module Curl
182
199
  #
183
200
  # Curl::Multi.download(['http://example.com/p/a/t/h/file1.txt','http://example.com/p/a/t/h/file2.txt']){|c|}
184
201
  #
185
- # will create 2 new files file1.txt and file2.txt
202
+ # will create 2 new files file1.txt and file2.txt, unless either file
203
+ # already exists. Auto-derived filenames are safely derived from the last
204
+ # URL path component. Pass <tt>:download_dir</tt> as the fifth argument to
205
+ # treat download_paths as basenames inside a trusted directory and reject
206
+ # absolute, parent-directory, dotfile, and nested names.
186
207
  #
187
208
  # 2 files will be opened, and remain open until the call completes
188
209
  #
189
210
  # when using the :post or :put method, urls should be a hash, including the individual post fields per post
190
211
  #
191
- def download(urls,easy_options={},multi_options={},download_paths=nil,&blk)
212
+ def download(urls,easy_options={},multi_options={},download_paths=nil,download_options={},&blk)
192
213
  errors = []
193
214
  procs = []
194
215
  files = []
195
216
  urls_with_config = []
196
- url_to_download_paths = {}
217
+ seen_download_paths = {}
218
+ download_infos = []
219
+
220
+ if Curl.download_options_hash?(download_paths) && download_options.empty?
221
+ download_options = download_paths
222
+ download_paths = nil
223
+ end
197
224
 
198
225
  urls.each_with_index do|urlcfg,i|
199
226
  if urlcfg.is_a?(Hash)
200
- url = url[:url]
227
+ url = urlcfg[:url]
201
228
  else
202
229
  url = urlcfg
203
230
  end
204
231
 
205
- if download_paths and download_paths[i]
206
- download_path = download_paths[i]
207
- else
208
- download_path = File.basename(url)
232
+ download_path_arg = download_paths && download_paths[i]
233
+ download_path, file, safe_output, overwrite = Curl.resolve_download_output(url, download_path_arg, download_options)
234
+
235
+ if safe_output
236
+ expanded_path = File.expand_path(download_path)
237
+ raise ArgumentError, "duplicate download destination: #{download_path}" if seen_download_paths[expanded_path]
238
+
239
+ seen_download_paths[expanded_path] = true
209
240
  end
210
241
 
211
- file = lambda do|dp|
212
- file = File.open(dp,"wb")
213
- procs << (lambda {|data| file.write data; data.size })
214
- files << file
215
- file
216
- end.call(download_path)
242
+ download_infos << {
243
+ :url => url,
244
+ :urlcfg => urlcfg,
245
+ :path => download_path,
246
+ :file => file,
247
+ :safe_output => safe_output,
248
+ :overwrite => overwrite
249
+ }
250
+ end
217
251
 
218
- if urlcfg.is_a?(Hash)
219
- urls_with_config << urlcfg.merge({:on_body => procs.last}.merge(easy_options))
252
+ download_infos.each do |info|
253
+ info[:file] ||= Curl.open_safe_download_output(info[:path], :overwrite => info[:overwrite])
254
+ file = info[:file]
255
+ procs << (lambda {|data| file.write data; data.size })
256
+ files << file
257
+
258
+ if info[:urlcfg].is_a?(Hash)
259
+ urls_with_config << info[:urlcfg].merge({:on_body => procs.last, :__curb_internal_info => info}.merge(easy_options))
220
260
  else
221
- urls_with_config << {:url => url, :on_body => procs.last, :method => :get}.merge(easy_options)
261
+ urls_with_config << {:url => info[:url], :on_body => procs.last, :method => :get, :__curb_internal_info => info}.merge(easy_options)
222
262
  end
223
- url_to_download_paths[url] = {:path => download_path, :file => file} # store for later
224
263
  end
225
264
 
226
- if blk
227
- # when injecting the block, ensure file is closed before yielding
228
- Curl::Multi.http(urls_with_config, multi_options) do |c,code,method|
229
- info = url_to_download_paths[c.url]
265
+ finalize_download = lambda do |curl, info|
266
+ file = info[:file]
267
+ files.reject!{|f| f == file }
268
+
269
+ if curl.last_result != 0
230
270
  begin
231
- file = info[:file]
232
- files.reject!{|f| f == file }
233
- file.close
271
+ if info[:safe_output]
272
+ file.close(false)
273
+ else
274
+ file.close
275
+ end
234
276
  rescue => e
235
277
  errors << e
236
278
  end
279
+ err_class, err_summary = Curl::Easy.error(curl.last_result)
280
+ err_detail = curl.last_error
281
+ errors << err_class.new([err_summary, err_detail].compact.join(": "))
282
+ false
283
+ else
284
+ begin
285
+ if info[:safe_output]
286
+ file.close(true)
287
+ else
288
+ file.close
289
+ end
290
+ true
291
+ rescue => e
292
+ errors << e
293
+ false
294
+ end
295
+ end
296
+ end
297
+
298
+ Curl::Multi.http(urls_with_config, multi_options) do |c,code,method,info|
299
+ if finalize_download.call(c, info) && blk
237
300
  blk.call(c,info[:path])
238
301
  end
239
- else
240
- Curl::Multi.http(urls_with_config, multi_options)
241
302
  end
242
303
 
243
304
  ensure
305
+ pending_exception = $!
244
306
  files.each {|f|
245
307
  begin
246
- f.close
308
+ if f.is_a?(Curl::SafeDownloadOutput)
309
+ f.close(false)
310
+ else
311
+ f.close
312
+ end
247
313
  rescue => e
248
314
  errors << e
249
315
  end
250
316
  }
251
- if errors.any?
317
+ if errors.any? && !pending_exception
252
318
  de = Curl::Multi::DownloadError.new
253
319
  de.errors = errors
254
320
  raise de
@@ -270,19 +336,46 @@ module Curl
270
336
  @requests ||= {}
271
337
  end
272
338
 
339
+ def __curb_native_safety_signatures
340
+ @__curb_native_safety_signatures ||= {}
341
+ end
342
+
343
+ def __curb_safety_signature_for(easy)
344
+ safety_signature = if Curl.respond_to?(:safety_signature_for, true)
345
+ Curl.__send__(:safety_signature_for, easy)
346
+ end
347
+
348
+ [
349
+ safety_signature,
350
+ easy.respond_to?(:network_policy) ? easy.network_policy : nil,
351
+ easy.respond_to?(:allowed_hosts) ? easy.allowed_hosts : nil,
352
+ easy.respond_to?(:allowed_cidrs) ? easy.allowed_cidrs : nil
353
+ ]
354
+ end
355
+
356
+ def __record_native_safety_signature(easy)
357
+ __curb_native_safety_signatures[easy.object_id] = __curb_safety_signature_for(easy)
358
+ end
359
+
273
360
  def __idle_easy_references
274
- @__curb_idle_easy_references ||= ObjectSpace::WeakMap.new
361
+ @__curb_idle_easy_references ||= __new_idle_easy_references
275
362
  end
276
363
 
277
364
  def __register_idle_easy_reference(easy)
278
- __idle_easy_references[easy] = true
365
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
366
+ __idle_easy_references[easy] = true
367
+ else
368
+ __idle_easy_references[easy.object_id] = true
369
+ end
279
370
  self
280
371
  end
281
372
 
282
373
  def __unregister_idle_easy_reference(easy)
283
374
  return self unless instance_variable_defined?(:@__curb_idle_easy_references)
284
375
 
285
- if @__curb_idle_easy_references.respond_to?(:delete)
376
+ if !IDLE_EASY_REFERENCES_USE_WEAK_MAP
377
+ @__curb_idle_easy_references.delete(easy.object_id)
378
+ elsif @__curb_idle_easy_references.respond_to?(:delete)
286
379
  @__curb_idle_easy_references.delete(easy)
287
380
  else
288
381
  retained_references = ObjectSpace::WeakMap.new
@@ -299,30 +392,70 @@ module Curl
299
392
 
300
393
  def __clear_idle_easy_references
301
394
  return unless instance_variable_defined?(:@__curb_idle_easy_references)
302
-
303
- @__curb_idle_easy_references.keys.each do |easy|
304
- easy.multi = nil if easy.multi.equal?(self)
395
+
396
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
397
+ @__curb_idle_easy_references.keys.each do |easy|
398
+ easy.multi = nil if easy.multi.equal?(self)
399
+ end
400
+ else
401
+ @__curb_idle_easy_references.keys.each do |easy_object_id|
402
+ begin
403
+ easy = ObjectSpace._id2ref(easy_object_id)
404
+ rescue RangeError
405
+ next
406
+ end
407
+
408
+ next unless easy.is_a?(Curl::Easy)
409
+
410
+ easy.multi = nil if easy.multi.equal?(self)
411
+ end
305
412
  end
306
- @__curb_idle_easy_references = ObjectSpace::WeakMap.new
413
+ @__curb_idle_easy_references = __new_idle_easy_references
307
414
  end
308
415
 
309
- private :__idle_easy_references, :__register_idle_easy_reference,
310
- :__unregister_idle_easy_reference, :__clear_idle_easy_references
416
+ def __new_idle_easy_references
417
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP ? ObjectSpace::WeakMap.new : {}
418
+ end
419
+
420
+ private :__idle_easy_references, :__curb_native_safety_signatures,
421
+ :__curb_safety_signature_for, :__record_native_safety_signature,
422
+ :__register_idle_easy_reference,
423
+ :__unregister_idle_easy_reference, :__clear_idle_easy_references,
424
+ :__new_idle_easy_references
425
+
426
+ alias_method :_curb_native_perform, :perform
427
+
428
+ def perform(*args, &block)
429
+ requests.each_value do |easy|
430
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
431
+ signature = __curb_safety_signature_for(easy)
432
+ if __curb_native_safety_signatures[easy.object_id] != signature &&
433
+ easy.respond_to?(:__curb_native_setup!, true)
434
+ easy.__send__(:__curb_native_setup!)
435
+ __curb_native_safety_signatures[easy.object_id] = signature
436
+ end
437
+ end
438
+
439
+ _curb_native_perform(*args, &block)
440
+ end
311
441
 
312
442
  def add(easy)
313
443
  return self if requests[easy.object_id]
314
444
  # Once a deferred callback exception is pending, Multi#perform is
315
445
  # draining existing transfers only and must not start replacement work.
316
446
  return self if instance_variable_defined?(:@__curb_deferred_exception)
447
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
317
448
  _add(easy)
318
449
  __unregister_idle_easy_reference(easy)
319
450
  requests[easy.object_id] = easy
451
+ __record_native_safety_signature(easy)
320
452
  self
321
453
  end
322
454
 
323
455
  def remove(easy)
324
456
  return self if !requests[easy.object_id]
325
457
  requests.delete(easy.object_id)
458
+ __curb_native_safety_signatures.delete(easy.object_id)
326
459
  _remove(easy)
327
460
  self
328
461
  end