down 4.8.1 → 5.4.0

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/down/http.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- gem "http", ">= 2.1.0", "< 5"
3
+ gem "http", ">= 2.1.0", "< 6"
4
4
 
5
5
  require "http"
6
6
 
@@ -12,12 +12,7 @@ module Down
12
12
  # Provides streaming downloads implemented with HTTP.rb.
13
13
  class Http < Backend
14
14
  # Initializes the backend with common defaults.
15
- def initialize(options = {}, &block)
16
- if options.is_a?(HTTP::Client)
17
- warn "[Down] Passing an HTTP::Client object to Down::Http#initialize is deprecated and won't be supported in Down 5. Use the block initialization instead."
18
- options = options.default_options.to_hash
19
- end
20
-
15
+ def initialize(**options, &block)
21
16
  @method = options.delete(:method) || :get
22
17
  @client = HTTP
23
18
  .headers("User-Agent" => "Down/#{Down::VERSION}")
@@ -30,16 +25,16 @@ module Down
30
25
 
31
26
  # Downlods the remote file to disk. Accepts HTTP.rb options via a hash or a
32
27
  # block, and some additional options as well.
33
- def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, **options, &block)
28
+ def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, **options, &block)
34
29
  response = request(url, **options, &block)
35
30
 
36
31
  content_length_proc.call(response.content_length) if content_length_proc && response.content_length
37
32
 
38
33
  if max_size && response.content_length && response.content_length > max_size
39
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
34
+ raise Down::TooLarge, "file is too large (#{response.content_length/1024/1024}MB, max is #{max_size/1024/1024}MB)"
40
35
  end
41
36
 
42
- extname = File.extname(response.uri.path)
37
+ extname = extension ? ".#{extension}" : File.extname(response.uri.path)
43
38
  tempfile = Tempfile.new(["down-http", extname], binmode: true)
44
39
 
45
40
  stream_body(response) do |chunk|
@@ -49,7 +44,7 @@ module Down
49
44
  progress_proc.call(tempfile.size) if progress_proc
50
45
 
51
46
  if max_size && tempfile.size > max_size
52
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
47
+ raise Down::TooLarge, "file is too large (#{tempfile.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
53
48
  end
54
49
  end
55
50
 
@@ -57,7 +52,7 @@ module Down
57
52
 
58
53
  tempfile.extend Down::Http::DownloadedFile
59
54
  tempfile.url = response.uri.to_s
60
- tempfile.headers = response.headers.to_h
55
+ tempfile.headers = normalize_headers(response.headers.to_h)
61
56
 
62
57
  download_result(tempfile, destination)
63
58
  rescue
@@ -76,7 +71,11 @@ module Down
76
71
  size: response.content_length,
77
72
  encoding: response.content_type.charset,
78
73
  rewindable: rewindable,
79
- data: { status: response.code, headers: response.headers.to_h, response: response },
74
+ data: {
75
+ status: response.code,
76
+ headers: normalize_headers(response.headers.to_h),
77
+ response: response
78
+ },
80
79
  )
81
80
  end
82
81
 
@@ -111,9 +110,10 @@ module Down
111
110
 
112
111
  # Raises non-sucessful response as a Down::ResponseError.
113
112
  def response_error!(response)
114
- args = [response.status.to_s, response: response]
113
+ args = [response.status.to_s, response]
115
114
 
116
115
  case response.code
116
+ when 404 then raise Down::NotFound.new(*args)
117
117
  when 400..499 then raise Down::ClientError.new(*args)
118
118
  when 500..599 then raise Down::ServerError.new(*args)
119
119
  else raise Down::ResponseError.new(*args)
data/lib/down/httpx.rb ADDED
@@ -0,0 +1,175 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "uri"
4
+ require "tempfile"
5
+ require "httpx"
6
+
7
+ require "down/backend"
8
+
9
+
10
+ module Down
11
+ # Provides streaming downloads implemented with HTTPX.
12
+ class Httpx < Backend
13
+ # Initializes the backend
14
+
15
+ USER_AGENT = "Down/#{Down::VERSION}"
16
+
17
+ def initialize(**options, &block)
18
+ @method = options.delete(:method) || :get
19
+ headers = options.delete(:headers) || {}
20
+ @client = HTTPX
21
+ .plugin(:follow_redirects, max_redirects: 2)
22
+ .plugin(:basic_authentication)
23
+ .plugin(:stream)
24
+ .with(
25
+ headers: { "user-agent": USER_AGENT }.merge(headers),
26
+ timeout: { connect_timeout: 30, write_timeout: 30, read_timeout: 30 },
27
+ **options
28
+ )
29
+
30
+ @client = block.call(@client) if block
31
+ end
32
+
33
+
34
+ # Downlods the remote file to disk. Accepts HTTPX options via a hash or a
35
+ # block, and some additional options as well.
36
+ def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, **options, &block)
37
+ client = @client
38
+
39
+ response = request(client, url, **options, &block)
40
+
41
+ content_length = nil
42
+
43
+ if response.headers.key?("content-length")
44
+ content_length = response.headers["content-length"].to_i
45
+
46
+ content_length_proc.call(content_length) if content_length_proc
47
+
48
+ if max_size && content_length > max_size
49
+ response.close
50
+ raise Down::TooLarge, "file is too large (#{content_length/1024/1024}MB, max is #{max_size/1024/1024}MB)"
51
+ end
52
+ end
53
+
54
+ extname = extension ? ".#{extension}" : File.extname(response.uri.path)
55
+ tempfile = Tempfile.new(["down-http", extname], binmode: true)
56
+
57
+ stream_body(response) do |chunk|
58
+ tempfile.write(chunk)
59
+ chunk.clear # deallocate string
60
+
61
+ progress_proc.call(tempfile.size) if progress_proc
62
+
63
+ if max_size && tempfile.size > max_size
64
+ raise Down::TooLarge, "file is too large (#{tempfile.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
65
+ end
66
+ end
67
+
68
+ tempfile.open # flush written content
69
+
70
+ tempfile.extend DownloadedFile
71
+ tempfile.url = response.uri.to_s
72
+ tempfile.headers = normalize_headers(response.headers.to_h)
73
+ tempfile.content_type = response.content_type.mime_type
74
+ tempfile.charset = response.body.encoding
75
+
76
+ download_result(tempfile, destination)
77
+ rescue
78
+ tempfile.close! if tempfile
79
+ raise
80
+ end
81
+
82
+ # Starts retrieving the remote file and returns an IO-like object which
83
+ # downloads the response body on-demand. Accepts HTTP.rb options via a hash
84
+ # or a block.
85
+ def open(url, rewindable: true, **options, &block)
86
+ response = request(@client, url, stream: true, **options, &block)
87
+ size = response.headers["content-length"]
88
+ size = size.to_i if size
89
+ Down::ChunkedIO.new(
90
+ chunks: enum_for(:stream_body, response),
91
+ size: size,
92
+ encoding: response.body.encoding,
93
+ rewindable: rewindable,
94
+ data: {
95
+ status: response.status,
96
+ headers: normalize_headers(response.headers.to_h),
97
+ response: response
98
+ },
99
+ )
100
+ end
101
+
102
+ private
103
+
104
+ # Yields chunks of the response body to the block.
105
+ def stream_body(response, &block)
106
+ response.each(&block)
107
+ rescue => exception
108
+ request_error!(exception)
109
+ end
110
+
111
+ def request(client, url, method: @method, **options, &block)
112
+ response = send_request(client, method, url, **options, &block)
113
+ response.raise_for_status
114
+ response_error!(response) unless (200..299).include?(response.status)
115
+ response
116
+ rescue HTTPX::HTTPError
117
+ response_error!(response)
118
+ rescue => error
119
+ request_error!(error)
120
+ end
121
+
122
+ def send_request(client, method, url, **options, &block)
123
+ uri = URI(url)
124
+ client = @client
125
+ if uri.user || uri.password
126
+ client = client.basic_auth(uri.user, uri.password)
127
+ uri.user = uri.password = nil
128
+ end
129
+ client = block.call(client) if block
130
+
131
+ client.request(method, uri, stream: true, **options)
132
+ rescue => exception
133
+ request_error!(exception)
134
+ end
135
+
136
+ # Raises non-sucessful response as a Down::ResponseError.
137
+ def response_error!(response)
138
+ args = [response.status.to_s, response]
139
+
140
+ case response.status
141
+ when 300..399 then raise Down::TooManyRedirects, "too many redirects"
142
+ when 404 then raise Down::NotFound.new(*args)
143
+ when 400..499 then raise Down::ClientError.new(*args)
144
+ when 500..599 then raise Down::ServerError.new(*args)
145
+ else raise Down::ResponseError.new(*args)
146
+ end
147
+ end
148
+
149
+ # Re-raise HTTP.rb exceptions as Down::Error exceptions.
150
+ def request_error!(exception)
151
+ case exception
152
+ when URI::Error, HTTPX::UnsupportedSchemeError
153
+ raise Down::InvalidUrl, exception.message
154
+ when HTTPX::ConnectionError
155
+ raise Down::ConnectionError, exception.message
156
+ when HTTPX::TimeoutError
157
+ raise Down::TimeoutError, exception.message
158
+ when OpenSSL::SSL::SSLError
159
+ raise Down::SSLError, exception.message
160
+ else
161
+ raise exception
162
+ end
163
+ end
164
+
165
+ # Defines some additional attributes for the returned Tempfile.
166
+ module DownloadedFile
167
+ attr_accessor :url, :headers, :charset, :content_type
168
+
169
+ def original_filename
170
+ Utils.filename_from_content_disposition(headers["Content-Disposition"]) ||
171
+ Utils.filename_from_path(URI.parse(url).path)
172
+ end
173
+ end
174
+ end
175
+ end
data/lib/down/net_http.rb CHANGED
@@ -12,27 +12,35 @@ require "fileutils"
12
12
  module Down
13
13
  # Provides streaming downloads implemented with Net::HTTP and open-uri.
14
14
  class NetHttp < Backend
15
+ URI_NORMALIZER = -> (url) do
16
+ addressable_uri = Addressable::URI.parse(url)
17
+ addressable_uri.normalize.to_s
18
+ end
19
+
15
20
  # Initializes the backend with common defaults.
16
- def initialize(options = {})
17
- @options = {
18
- "User-Agent" => "Down/#{Down::VERSION}",
19
- max_redirects: 2,
20
- open_timeout: 30,
21
- read_timeout: 30,
22
- }.merge(options)
21
+ def initialize(*args, **options)
22
+ @options = merge_options({
23
+ headers: { "User-Agent" => "Down/#{Down::VERSION}" },
24
+ max_redirects: 2,
25
+ open_timeout: 30,
26
+ read_timeout: 30,
27
+ uri_normalizer: URI_NORMALIZER,
28
+ }, *args, **options)
23
29
  end
24
30
 
25
31
  # Downloads a remote file to disk using open-uri. Accepts any open-uri
26
32
  # options, and a few more.
27
- def download(url, options = {})
28
- options = @options.merge(options)
33
+ def download(url, *args, **options)
34
+ options = merge_options(@options, *args, **options)
29
35
 
30
36
  max_size = options.delete(:max_size)
31
37
  max_redirects = options.delete(:max_redirects)
32
38
  progress_proc = options.delete(:progress_proc)
33
39
  content_length_proc = options.delete(:content_length_proc)
34
40
  destination = options.delete(:destination)
35
- headers = options.delete(:headers) || {}
41
+ headers = options.delete(:headers)
42
+ uri_normalizer = options.delete(:uri_normalizer)
43
+ extension = options.delete(:extension)
36
44
 
37
45
  # Use open-uri's :content_lenth_proc or :progress_proc to raise an
38
46
  # exception early if the file is too large.
@@ -42,13 +50,13 @@ module Down
42
50
  open_uri_options = {
43
51
  content_length_proc: proc { |size|
44
52
  if size && max_size && size > max_size
45
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
53
+ raise Down::TooLarge, "file is too large (#{size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
46
54
  end
47
55
  content_length_proc.call(size) if content_length_proc
48
56
  },
49
57
  progress_proc: proc { |current_size|
50
58
  if max_size && current_size > max_size
51
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
59
+ raise Down::TooLarge, "file is too large (#{current_size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
52
60
  end
53
61
  progress_proc.call(current_size) if progress_proc
54
62
  },
@@ -74,7 +82,7 @@ module Down
74
82
  open_uri_options.merge!(options)
75
83
  open_uri_options.merge!(headers)
76
84
 
77
- uri = ensure_uri(addressable_normalize(url))
85
+ uri = ensure_uri(normalize_uri(url, uri_normalizer: uri_normalizer))
78
86
 
79
87
  # Handle basic authentication in the remote URL.
80
88
  if uri.user || uri.password
@@ -86,7 +94,8 @@ module Down
86
94
  open_uri_file = open_uri(uri, open_uri_options, follows_remaining: max_redirects)
87
95
 
88
96
  # Handle the fact that open-uri returns StringIOs for small files.
89
- tempfile = ensure_tempfile(open_uri_file, File.extname(open_uri_file.base_uri.path))
97
+ extname = extension ? ".#{extension}" : File.extname(open_uri_file.base_uri.path)
98
+ tempfile = ensure_tempfile(open_uri_file, extname)
90
99
  OpenURI::Meta.init tempfile, open_uri_file # add back open-uri methods
91
100
  tempfile.extend Down::NetHttp::DownloadedFile
92
101
 
@@ -95,13 +104,17 @@ module Down
95
104
 
96
105
  # Starts retrieving the remote file using Net::HTTP and returns an IO-like
97
106
  # object which downloads the response body on-demand.
98
- def open(url, options = {})
99
- uri = ensure_uri(addressable_normalize(url))
100
- options = @options.merge(options)
107
+ def open(url, *args, **options)
108
+ options = merge_options(@options, *args, **options)
109
+
110
+ max_redirects = options.delete(:max_redirects)
111
+ uri_normalizer = options.delete(:uri_normalizer)
112
+
113
+ uri = ensure_uri(normalize_uri(url, uri_normalizer: uri_normalizer))
101
114
 
102
115
  # Create a Fiber that halts when response headers are received.
103
116
  request = Fiber.new do
104
- net_http_request(uri, options) do |response|
117
+ net_http_request(uri, options, follows_remaining: max_redirects) do |response|
105
118
  Fiber.yield response
106
119
  end
107
120
  end
@@ -119,10 +132,7 @@ module Down
119
132
  on_close: -> { request.resume }, # close HTTP connnection
120
133
  data: {
121
134
  status: response.code.to_i,
122
- headers: response.each_header.inject({}) { |headers, (downcased_name, value)|
123
- name = downcased_name.split("-").map(&:capitalize).join("-")
124
- headers.merge!(name => value)
125
- },
135
+ headers: normalize_headers(response.each_header),
126
136
  response: response,
127
137
  },
128
138
  )
@@ -131,7 +141,7 @@ module Down
131
141
  private
132
142
 
133
143
  # Calls open-uri's URI::HTTP#open method. Additionally handles redirects.
134
- def open_uri(uri, options, follows_remaining: 0)
144
+ def open_uri(uri, options, follows_remaining:)
135
145
  uri.open(options)
136
146
  rescue OpenURI::HTTPRedirect => exception
137
147
  raise Down::TooManyRedirects, "too many redirects" if follows_remaining == 0
@@ -147,7 +157,11 @@ module Down
147
157
 
148
158
  # forward cookies on the redirect
149
159
  if !exception.io.meta["set-cookie"].to_s.empty?
150
- options["Cookie"] = exception.io.meta["set-cookie"]
160
+ options["Cookie"] ||= ''
161
+ # Add new cookies avoiding duplication
162
+ new_cookies = exception.io.meta["set-cookie"].to_s.split(';').map(&:strip)
163
+ old_cookies = options["Cookie"].split(';')
164
+ options["Cookie"] = (old_cookies | new_cookies).join(';')
151
165
  end
152
166
 
153
167
  follows_remaining -= 1
@@ -186,18 +200,18 @@ module Down
186
200
  end
187
201
 
188
202
  # Makes a Net::HTTP request and follows redirects.
189
- def net_http_request(uri, options, follows_remaining: options.fetch(:max_redirects, 2), &block)
203
+ def net_http_request(uri, options, follows_remaining:, &block)
190
204
  http, request = create_net_http(uri, options)
191
205
 
192
206
  begin
193
207
  response = http.start do
194
- http.request(request) do |response|
195
- unless response.is_a?(Net::HTTPRedirection)
196
- yield response
208
+ http.request(request) do |resp|
209
+ unless resp.is_a?(Net::HTTPRedirection)
210
+ yield resp
197
211
  # In certain cases the caller wants to download only one portion
198
212
  # of the file and close the connection, so we tell Net::HTTP that
199
213
  # it shouldn't continue retrieving it.
200
- response.instance_variable_set("@read", true)
214
+ resp.instance_variable_set("@read", true)
201
215
  end
202
216
  end
203
217
  end
@@ -205,7 +219,9 @@ module Down
205
219
  request_error!(exception)
206
220
  end
207
221
 
208
- if response.is_a?(Net::HTTPRedirection)
222
+ if response.is_a?(Net::HTTPNotModified)
223
+ raise Down::NotModified
224
+ elsif response.is_a?(Net::HTTPRedirection)
209
225
  raise Down::TooManyRedirects if follows_remaining == 0
210
226
 
211
227
  # fail if redirect URI is not a valid http or https URL
@@ -251,12 +267,13 @@ module Down
251
267
  http.read_timeout = options[:read_timeout] if options.key?(:read_timeout)
252
268
  http.open_timeout = options[:open_timeout] if options.key?(:open_timeout)
253
269
 
254
- headers = options.select { |key, value| key.is_a?(String) }
255
- headers.merge!(options[:headers]) if options[:headers]
270
+ headers = options[:headers].to_h
256
271
  headers["Accept-Encoding"] = "" # Net::HTTP's inflater causes FiberErrors
257
272
 
258
273
  get = Net::HTTP::Get.new(uri.request_uri, headers)
259
- get.basic_auth(uri.user, uri.password) if uri.user || uri.password
274
+
275
+ user, password = options[:http_basic_authentication] || [uri.user, uri.password]
276
+ get.basic_auth(user, password) if user || password
260
277
 
261
278
  [http, get]
262
279
  end
@@ -284,9 +301,10 @@ module Down
284
301
  end
285
302
 
286
303
  # Makes sure that the URL is properly encoded.
287
- def addressable_normalize(url)
288
- addressable_uri = Addressable::URI.parse(url)
289
- addressable_uri.normalize.to_s
304
+ def normalize_uri(url, uri_normalizer:)
305
+ URI(url)
306
+ rescue URI::InvalidURIError
307
+ uri_normalizer.call(url)
290
308
  end
291
309
 
292
310
  # When open-uri raises an exception, it doesn't expose the response object.
@@ -295,7 +313,11 @@ module Down
295
313
  def rebuild_response_from_open_uri_exception(exception)
296
314
  code, message = exception.io.status
297
315
 
298
- response_class = Net::HTTPResponse::CODE_TO_OBJ.fetch(code)
316
+ response_class = Net::HTTPResponse::CODE_TO_OBJ.fetch(code) do |c|
317
+ Net::HTTPResponse::CODE_CLASS_TO_OBJ.fetch(c[0]) do
318
+ Net::HTTPUnknownResponse
319
+ end
320
+ end
299
321
  response = response_class.new(nil, code, message)
300
322
 
301
323
  exception.io.metas.each do |name, values|
@@ -310,9 +332,10 @@ module Down
310
332
  code = response.code.to_i
311
333
  message = response.message.split(" ").map(&:capitalize).join(" ")
312
334
 
313
- args = ["#{code} #{message}", response: response]
335
+ args = ["#{code} #{message}", response]
314
336
 
315
337
  case response.code.to_i
338
+ when 404 then raise Down::NotFound.new(*args)
316
339
  when 400..499 then raise Down::ClientError.new(*args)
317
340
  when 500..599 then raise Down::ServerError.new(*args)
318
341
  else raise Down::ResponseError.new(*args)
@@ -335,6 +358,24 @@ module Down
335
358
  end
336
359
  end
337
360
 
361
+ # Merge default and ad-hoc options, merging nested headers.
362
+ def merge_options(options, headers = {}, **new_options)
363
+ # Deprecate passing headers as top-level options, taking into account
364
+ # that Ruby 2.7+ accepts kwargs with string keys.
365
+ if headers.any?
366
+ warn %([Down::NetHttp] Passing headers as top-level options has been deprecated, use the :headers option instead, e.g: `Down::NetHttp.download(headers: { "Key" => "Value", ... }, ...)`)
367
+ new_options[:headers] = headers
368
+ elsif new_options.any? { |key, value| key.is_a?(String) }
369
+ warn %([Down::NetHttp] Passing headers as top-level options has been deprecated, use the :headers option instead, e.g: `Down::NetHttp.download(headers: { "Key" => "Value", ... }, ...)`)
370
+ new_options[:headers] = new_options.select { |key, value| key.is_a?(String) }
371
+ new_options.reject! { |key, value| key.is_a?(String) }
372
+ end
373
+
374
+ options.merge(new_options) do |key, value1, value2|
375
+ key == :headers ? value1.merge(value2) : value2
376
+ end
377
+ end
378
+
338
379
  # Defines some additional attributes for the returned Tempfile (on top of what
339
380
  # OpenURI::Meta already defines).
340
381
  module DownloadedFile
data/lib/down/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Down
4
- VERSION = "4.8.1"
4
+ VERSION = "5.4.0"
5
5
  end
data/lib/down/wget.rb CHANGED
@@ -29,16 +29,16 @@ module Down
29
29
 
30
30
  # Downlods the remote file to disk. Accepts wget command-line options and
31
31
  # some additional options as well.
32
- def download(url, *args, max_size: nil, content_length_proc: nil, progress_proc: nil, destination: nil, **options)
32
+ def download(url, *args, max_size: nil, content_length_proc: nil, progress_proc: nil, destination: nil, extension: nil, **options)
33
33
  io = open(url, *args, **options, rewindable: false)
34
34
 
35
35
  content_length_proc.call(io.size) if content_length_proc && io.size
36
36
 
37
37
  if max_size && io.size && io.size > max_size
38
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
38
+ raise Down::TooLarge, "file is too large (#{io.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
39
39
  end
40
40
 
41
- extname = File.extname(URI(url).path)
41
+ extname = extension ? ".#{extension}" : File.extname(URI(url).path)
42
42
  tempfile = Tempfile.new(["down-wget", extname], binmode: true)
43
43
 
44
44
  until io.eof?
@@ -49,7 +49,7 @@ module Down
49
49
  progress_proc.call(tempfile.size) if progress_proc
50
50
 
51
51
  if max_size && tempfile.size > max_size
52
- raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
52
+ raise Down::TooLarge, "file is too large (#{tempfile.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
53
53
  end
54
54
  end
55
55
 
@@ -94,7 +94,7 @@ module Down
94
94
  raise Down::Error, "failed to parse response headers"
95
95
  end
96
96
 
97
- headers = parser.headers
97
+ headers = normalize_headers(parser.headers)
98
98
  status = parser.status_code
99
99
 
100
100
  content_length = headers["Content-Length"].to_i if headers["Content-Length"]
data/lib/down.rb CHANGED
@@ -6,12 +6,12 @@ require "down/net_http"
6
6
  module Down
7
7
  module_function
8
8
 
9
- def download(*args, &block)
10
- backend.download(*args, &block)
9
+ def download(*args, **options, &block)
10
+ backend.download(*args, **options, &block)
11
11
  end
12
12
 
13
- def open(*args, &block)
14
- backend.open(*args, &block)
13
+ def open(*args, **options, &block)
14
+ backend.open(*args, **options, &block)
15
15
  end
16
16
 
17
17
  # Allows setting a backend via a symbol or a downloader object.