curb 1.3.4 → 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.
- checksums.yaml +4 -4
- data/README.md +57 -0
- data/Rakefile +24 -2
- data/doc.rb +48 -8
- data/ext/curb.c +24 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +1378 -55
- data/ext/curb_easy.h +26 -0
- data/ext/curb_errors.c +2 -0
- data/ext/curb_errors.h +1 -0
- data/ext/curb_multi.c +330 -74
- data/ext/curb_multi.h +1 -0
- data/ext/extconf.rb +9 -0
- data/lib/curl/download.rb +160 -0
- data/lib/curl/easy.rb +113 -13
- data/lib/curl/multi.rb +172 -39
- data/lib/curl.rb +471 -11
- data/tests/bug_poison.rb +29 -0
- data/tests/tc_curl_download.rb +86 -0
- data/tests/tc_curl_easy.rb +76 -0
- data/tests/tc_curl_maxfilesize.rb +201 -1
- data/tests/tc_curl_multi.rb +293 -0
- data/tests/tc_curl_network_policy.rb +1475 -0
- data/tests/tc_curl_protocols.rb +351 -0
- data/tests/tc_fiber_scheduler.rb +64 -5
- metadata +8 -3
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Curl
|
|
7
|
+
class DownloadTargetExistsError < Errno::EEXIST; end
|
|
8
|
+
DOWNLOAD_OPTION_KEYS = [:download_dir, :overwrite].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def download_options_hash?(value)
|
|
12
|
+
value.is_a?(Hash) && value.keys.all? { |key| DOWNLOAD_OPTION_KEYS.include?(key) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def normalize_download_arguments(filename, options)
|
|
16
|
+
if download_options_hash?(filename) && options.empty?
|
|
17
|
+
options = filename
|
|
18
|
+
filename = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[filename, options || {}]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_download_options(options)
|
|
25
|
+
raise ArgumentError, "download options must be a Hash" unless options.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
options = options.dup
|
|
28
|
+
download_dir = options.delete(:download_dir)
|
|
29
|
+
overwrite = options.key?(:overwrite) ? !!options.delete(:overwrite) : false
|
|
30
|
+
raise ArgumentError, "unsupported download option(s): #{options.keys.join(', ')}" unless options.empty?
|
|
31
|
+
|
|
32
|
+
[download_dir, overwrite]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_download_output(url, filename = nil, options = {})
|
|
36
|
+
filename, options = normalize_download_arguments(filename, options)
|
|
37
|
+
download_dir, overwrite = parse_download_options(options)
|
|
38
|
+
|
|
39
|
+
if filename.is_a?(IO)
|
|
40
|
+
filename.binmode if filename.respond_to?(:binmode)
|
|
41
|
+
return [nil, filename, false, overwrite]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
path = if download_dir
|
|
45
|
+
safe_download_path(url, download_dir, filename: filename)
|
|
46
|
+
elsif filename.nil?
|
|
47
|
+
safe_download_path(url)
|
|
48
|
+
else
|
|
49
|
+
filename.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[path, nil, true, overwrite]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def prepare_download_output(url, filename = nil, options = {})
|
|
56
|
+
path, io, safe_output, overwrite = resolve_download_output(url, filename, options)
|
|
57
|
+
return [path, io, safe_output] unless safe_output
|
|
58
|
+
|
|
59
|
+
[path, open_safe_download_output(path, overwrite: overwrite), true]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def download_filename_from_url(url)
|
|
63
|
+
path = begin
|
|
64
|
+
URI.parse(url.to_s).path
|
|
65
|
+
rescue URI::InvalidURIError
|
|
66
|
+
url.to_s.split(/\?/, 2).first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
basename = File.basename(path.to_s)
|
|
70
|
+
validate_download_filename!(basename)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def safe_download_path(url, destination_dir = Dir.pwd, filename: nil)
|
|
74
|
+
dir = File.expand_path(destination_dir.to_s)
|
|
75
|
+
raise ArgumentError, "download destination directory does not exist: #{destination_dir}" unless File.directory?(dir)
|
|
76
|
+
|
|
77
|
+
File.join(dir, filename.nil? ? download_filename_from_url(url) : validate_download_filename!(filename))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_download_filename!(filename)
|
|
81
|
+
filename = filename.to_s
|
|
82
|
+
invalid = filename.empty? ||
|
|
83
|
+
filename == '.' ||
|
|
84
|
+
filename == '..' ||
|
|
85
|
+
filename == File::SEPARATOR ||
|
|
86
|
+
filename.start_with?('.') ||
|
|
87
|
+
filename.include?("\0") ||
|
|
88
|
+
filename.include?(File::SEPARATOR) ||
|
|
89
|
+
filename.include?('\\') ||
|
|
90
|
+
filename.match?(/\A[A-Za-z]:/) ||
|
|
91
|
+
(File::ALT_SEPARATOR && filename.include?(File::ALT_SEPARATOR))
|
|
92
|
+
|
|
93
|
+
raise ArgumentError, "unsafe download filename derived from URL: #{filename.inspect}" if invalid
|
|
94
|
+
|
|
95
|
+
filename
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def open_safe_download_output(path, overwrite: false)
|
|
99
|
+
SafeDownloadOutput.new(path, overwrite: overwrite)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class SafeDownloadOutput
|
|
104
|
+
def initialize(path, overwrite: false)
|
|
105
|
+
@path = File.expand_path(path.to_s)
|
|
106
|
+
@overwrite = overwrite
|
|
107
|
+
raise DownloadTargetExistsError, @path if !@overwrite && File.exist?(@path)
|
|
108
|
+
|
|
109
|
+
@tmp = Tempfile.new(['.curb-download-', '.tmp'], File.dirname(@path))
|
|
110
|
+
@tmp.binmode
|
|
111
|
+
@closed = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr_reader :path
|
|
115
|
+
|
|
116
|
+
def write(data)
|
|
117
|
+
@tmp.write(data)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def <<(data)
|
|
121
|
+
write(data)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def close(success = false)
|
|
125
|
+
return if @closed
|
|
126
|
+
|
|
127
|
+
@tmp.flush unless @tmp.closed?
|
|
128
|
+
@tmp.close unless @tmp.closed?
|
|
129
|
+
install_tmp_file if success
|
|
130
|
+
ensure
|
|
131
|
+
@tmp.close! if @tmp
|
|
132
|
+
@closed = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def install_tmp_file
|
|
138
|
+
if @overwrite
|
|
139
|
+
File.rename(@tmp.path, @path)
|
|
140
|
+
else
|
|
141
|
+
File.link(@tmp.path, @path)
|
|
142
|
+
end
|
|
143
|
+
rescue Errno::EEXIST
|
|
144
|
+
raise DownloadTargetExistsError, @path
|
|
145
|
+
rescue NotImplementedError
|
|
146
|
+
install_tmp_file_by_copy
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def install_tmp_file_by_copy
|
|
150
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
151
|
+
flags |= File::BINARY if File.const_defined?(:BINARY)
|
|
152
|
+
|
|
153
|
+
File.open(@path, flags) do |dest|
|
|
154
|
+
File.open(@tmp.path, 'rb') { |src| IO.copy_stream(src, dest) }
|
|
155
|
+
end
|
|
156
|
+
rescue Errno::EEXIST
|
|
157
|
+
raise DownloadTargetExistsError, @path
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/curl/easy.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'curl/download'
|
|
2
3
|
module Curl
|
|
3
4
|
class Easy
|
|
4
5
|
class << self
|
|
@@ -83,10 +84,12 @@ module Curl
|
|
|
83
84
|
|
|
84
85
|
alias_method :_curb_native_close, :close
|
|
85
86
|
alias_method :_curb_native_multi_set, :multi=
|
|
87
|
+
alias_method :_curb_native_reset, :reset
|
|
86
88
|
|
|
87
89
|
def close
|
|
88
90
|
previous_multi = self.multi
|
|
89
91
|
result = _curb_native_close
|
|
92
|
+
__curb_clear_safety_override!
|
|
90
93
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
91
94
|
result
|
|
92
95
|
end
|
|
@@ -94,12 +97,22 @@ module Curl
|
|
|
94
97
|
def multi=(multi)
|
|
95
98
|
previous_multi = self.multi
|
|
96
99
|
return multi if previous_multi.equal?(multi)
|
|
97
|
-
|
|
100
|
+
|
|
101
|
+
if previous_multi && previous_multi.requests[self.object_id]
|
|
102
|
+
previous_multi.remove(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
98
105
|
result = _curb_native_multi_set(multi)
|
|
99
106
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
100
107
|
multi.__send__(:__register_idle_easy_reference, self) if multi
|
|
101
108
|
result
|
|
102
109
|
end
|
|
110
|
+
|
|
111
|
+
def reset
|
|
112
|
+
result = _curb_native_reset
|
|
113
|
+
__curb_clear_safety_override!
|
|
114
|
+
result
|
|
115
|
+
end
|
|
103
116
|
|
|
104
117
|
alias post http_post
|
|
105
118
|
alias put http_put
|
|
@@ -154,6 +167,84 @@ module Curl
|
|
|
154
167
|
Curl.const_get("CURLOPT_#{opt.to_s.upcase}")
|
|
155
168
|
end
|
|
156
169
|
|
|
170
|
+
def allowed_protocols=(protocols)
|
|
171
|
+
set_protocol_allowlist('CURLOPT_PROTOCOLS_STR', 'CURLOPT_PROTOCOLS', protocols)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def allowed_redirect_protocols=(protocols)
|
|
175
|
+
set_protocol_allowlist('CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REDIR_PROTOCOLS', protocols)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def safe_http!
|
|
179
|
+
__curb_set_safety_override!(
|
|
180
|
+
:protocols => [:http, :https],
|
|
181
|
+
:redirect_protocols => [:http, :https]
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def __curb_set_safety_override!(options)
|
|
188
|
+
@__curb_safety_override = (defined?(@__curb_safety_override) && @__curb_safety_override) ? @__curb_safety_override.dup : {}
|
|
189
|
+
@__curb_safety_override[:protocols] = Array(options[:protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:protocols)
|
|
190
|
+
@__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
|
|
191
|
+
@__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes) && options[:max_body_bytes]
|
|
192
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
193
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def __curb_clear_safety_override!
|
|
198
|
+
had_override = defined?(@__curb_safety_override) && @__curb_safety_override
|
|
199
|
+
return unless had_override
|
|
200
|
+
return if frozen?
|
|
201
|
+
|
|
202
|
+
@__curb_safety_override = nil
|
|
203
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def __curb_safety_override
|
|
207
|
+
defined?(@__curb_safety_override) ? @__curb_safety_override : nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def __curb_safety_override_generation
|
|
211
|
+
defined?(@__curb_safety_override_generation) ? @__curb_safety_override_generation : 0
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_protocol_allowlist(string_option, bitmask_option, protocols)
|
|
215
|
+
protocol_names = Array(protocols).map { |protocol| protocol.to_s.downcase }
|
|
216
|
+
raise ArgumentError, "at least one protocol is required" if protocol_names.empty?
|
|
217
|
+
|
|
218
|
+
valid_names = %w[
|
|
219
|
+
dict file ftp ftps gopher gophers http https imap imaps ldap ldaps
|
|
220
|
+
mqtt pop3 pop3s rtmp rtmpe rtmps rtmpt rtmpte rtmpts rtsp scp sftp
|
|
221
|
+
smb smbs smtp smtps telnet tftp ws wss
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
if Curl.const_defined?(string_option)
|
|
225
|
+
protocol_names.each do |name|
|
|
226
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless valid_names.include?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
setopt(Curl.const_get(string_option), protocol_names.join(','))
|
|
230
|
+
elsif Curl.const_defined?(bitmask_option)
|
|
231
|
+
protocol_pairs = protocol_names.map do |name|
|
|
232
|
+
const_name = "CURLPROTO_#{name.upcase}"
|
|
233
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless Curl.const_defined?(const_name)
|
|
234
|
+
|
|
235
|
+
[name, Curl.const_get(const_name)]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
setopt(Curl.const_get(bitmask_option), protocol_pairs.inject(0) { |mask, pair| mask | pair.last })
|
|
239
|
+
else
|
|
240
|
+
raise NotImplementedError, "protocol allowlists are not supported by this libcurl"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
protocols
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
public
|
|
247
|
+
|
|
157
248
|
#
|
|
158
249
|
# call-seq:
|
|
159
250
|
# easy.perform => true
|
|
@@ -163,6 +254,7 @@ module Curl
|
|
|
163
254
|
# the configured HTTP Verb.
|
|
164
255
|
#
|
|
165
256
|
def perform
|
|
257
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
166
258
|
self.class.flush_deferred_multi_closes
|
|
167
259
|
|
|
168
260
|
if Curl.scheduler_active? && self.multi.nil?
|
|
@@ -210,6 +302,10 @@ module Curl
|
|
|
210
302
|
raise callback_error
|
|
211
303
|
end
|
|
212
304
|
|
|
305
|
+
if respond_to?(:unsafe_destination_error) && (unsafe_destination_error = self.unsafe_destination_error)
|
|
306
|
+
raise Curl::Err::UnsafeDestinationError, unsafe_destination_error
|
|
307
|
+
end
|
|
308
|
+
|
|
213
309
|
if self.last_result != 0 && self.on_failure.nil?
|
|
214
310
|
err_class, err_summary = Curl::Easy.error(self.last_result)
|
|
215
311
|
err_detail = self.last_error
|
|
@@ -641,11 +737,15 @@ module Curl
|
|
|
641
737
|
end
|
|
642
738
|
|
|
643
739
|
# call-seq:
|
|
644
|
-
# Curl::Easy.download(url, filename =
|
|
740
|
+
# Curl::Easy.download(url, filename = nil, options = {}) { |curl| ... }
|
|
645
741
|
#
|
|
646
742
|
# Stream the specified url (via perform) and save the data directly to the
|
|
647
|
-
# supplied filename
|
|
648
|
-
#
|
|
743
|
+
# supplied filename. The destination is written through a temporary file and
|
|
744
|
+
# existing files are not overwritten unless <tt>:overwrite => true</tt> is
|
|
745
|
+
# passed. When filename is omitted, the destination is safely derived from the
|
|
746
|
+
# last URL path component in the current directory. Pass
|
|
747
|
+
# <tt>:download_dir</tt> to treat the filename as a basename inside a trusted
|
|
748
|
+
# directory and reject absolute, parent-directory, dotfile, and nested names.
|
|
649
749
|
#
|
|
650
750
|
# If a block is supplied, it will be passed the curl instance prior to the
|
|
651
751
|
# perform call.
|
|
@@ -658,16 +758,11 @@ module Curl
|
|
|
658
758
|
# returns a size that differs from the data chunk size - in this case, the
|
|
659
759
|
# offending chunk will *not* be written to the file, the file will be closed,
|
|
660
760
|
# and a Curl::Err::AbortedByCallbackError will be raised.
|
|
661
|
-
def download(url, filename =
|
|
761
|
+
def download(url, filename = nil, download_options = {}, &blk)
|
|
662
762
|
curl = Curl::Easy.new(url, &blk)
|
|
763
|
+
_download_path, output, safe_output = Curl.prepare_download_output(url, filename, download_options)
|
|
663
764
|
|
|
664
|
-
|
|
665
|
-
filename.binmode if filename.respond_to?(:binmode)
|
|
666
|
-
filename
|
|
667
|
-
else
|
|
668
|
-
File.open(filename, 'wb')
|
|
669
|
-
end
|
|
670
|
-
|
|
765
|
+
performed = false
|
|
671
766
|
begin
|
|
672
767
|
old_on_body = curl.on_body do |data|
|
|
673
768
|
result = old_on_body ? old_on_body.call(data) : data.length
|
|
@@ -675,8 +770,13 @@ module Curl
|
|
|
675
770
|
result
|
|
676
771
|
end
|
|
677
772
|
curl.perform
|
|
773
|
+
performed = true
|
|
678
774
|
ensure
|
|
679
|
-
|
|
775
|
+
if safe_output
|
|
776
|
+
output.close(performed)
|
|
777
|
+
else
|
|
778
|
+
output.close rescue IOError
|
|
779
|
+
end
|
|
680
780
|
end
|
|
681
781
|
|
|
682
782
|
return curl
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
227
|
+
url = urlcfg[:url]
|
|
201
228
|
else
|
|
202
229
|
url = urlcfg
|
|
203
230
|
end
|
|
204
231
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
file
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
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 ||=
|
|
361
|
+
@__curb_idle_easy_references ||= __new_idle_easy_references
|
|
275
362
|
end
|
|
276
363
|
|
|
277
364
|
def __register_idle_easy_reference(easy)
|
|
278
|
-
|
|
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
|
|
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
|
-
|
|
304
|
-
|
|
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 =
|
|
413
|
+
@__curb_idle_easy_references = __new_idle_easy_references
|
|
307
414
|
end
|
|
308
415
|
|
|
309
|
-
|
|
310
|
-
|
|
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
|