down 4.8.1 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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.