down 5.4.2 → 5.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8a87deb869d776663b7fa4a32a87fcf73ffe2434e2fbe4fdef1ce7d6e48cf55
4
- data.tar.gz: 294efe7598f57309b0cbc3a3190685663384c9b3374c9afa621e728205bc367e
3
+ metadata.gz: 04d70935026de1e61cbda54578c360164e4ab252658c2b7a5531701e8195f55d
4
+ data.tar.gz: 047bbd998f10b26ad7865f4dc445a5ddc4ad811c4639ba51b8691eb1436cb6af
5
5
  SHA512:
6
- metadata.gz: 1d7d3f067b9de755a9976d572e668cad8d39e38968da71f209a5044debd3b0db14c7f70bf96b4237eae1da73178cdcafd454f28cc1bba34e6d92f02194ee94e0
7
- data.tar.gz: 9638590a3e4ee48ccd9c6409aadd140182315a0a648896b52a14fd5419c67bba1a4266cf00e51ec9dca6b17e77520373373ef7693d8155bfeb5409078c3ca9f6
6
+ metadata.gz: '0924393fcdee5795751393b9965e3cb2221da29baa5797d30bdd19115edb63972e8c6119310e1f65f77feeae7aae61ef5f03452394c77b57a57aaa1d8663a0da'
7
+ data.tar.gz: 808f8ac45f19f7a54e708b9b51ca4e659474eebe2bd14d894157a9454d4a2726d1260d323c61445218d8cc00f4b25497fb1c680c527e0fbc51a9cf1f374d605c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## 5.6.1 (2026-10-18)
2
+
3
+ * Avoid warnings on requiring `cgi` on Ruby 4.0+ (@janko)
4
+
5
+ ## 5.6.0 (2026-04-25)
6
+
7
+ * Require Ruby 3.2+ (@janko)
8
+
9
+ * Return `Down::Chunked#gets(nil, n)` in specified encoding for `IO` compatibility (@janko)
10
+
11
+ * Allow net/http to transparently decompress response bodies again (@janko)
12
+
13
+ * Handle `:auth_on_redirect` for absolute redirects in net/http backend (@janko)
14
+
15
+ * Fix `#pos` not always being updated after `Down::ChunkedIO#gets` (@janko)
16
+
17
+ * Add `#eof` alias to `Down::ChunkedIO#eof?` for `IO` compatibility
18
+
19
+ * Add `#length` alias for `Down::ChunkedIO#size` for `multipart-post` gem compatibility (@janko)
20
+
21
+ ## 5.5.0 (2026-03-18)
22
+
23
+ * Add support for http.rb 6.0 (@sferik)
24
+
25
+ * Fix `Down::TooManyRedirects` error with `Down::Httpx` happening on first redirect (@artrybalko)
26
+
27
+ * Add `:auth_on_redirect` option to `Down::NetHttp` backend for skipping authentication on redirects (@makrsmark)
28
+
29
+ * Add `:tempfile_name` keyword argument to `.download` for overriding tempfile prefix (@softwaregravy)
30
+
31
+ * Drop support for Ruby 2.6 and older (@janko)
32
+
33
+ * Deprecate wget backend (@janko)
34
+
1
35
  ## 5.4.2 (2024-04-19)
2
36
 
3
37
  * Add support for HTTPX 1.x (@HoneyryderChuck)
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Down
2
2
 
3
3
  Down is a utility tool for streaming, flexible and safe downloading of remote
4
- files. It can use [open-uri] + `Net::HTTP`, [http.rb], [HTTPX], or `wget` as
5
- the backend HTTP library.
4
+ files. It can use [open-uri] + `Net::HTTP`, [http.rb] or [HTTPX] as the backend
5
+ HTTP library.
6
6
 
7
7
  ## Installation
8
8
 
@@ -70,6 +70,13 @@ tempfile = Down.download("http://example.com/some/file", extension: "txt")
70
70
  File.extname(tempfile.path) #=> ".txt"
71
71
  ```
72
72
 
73
+ You can also override the default tempfile prefix:
74
+
75
+ ```rb
76
+ tempfile = Down.download("http://example.com/image.jpg", tempfile_name: "custom-prefix")
77
+ File.basename(tempfile.path) #=> "custom-prefix20150925-55456-z7vxqz.jpg"
78
+ ```
79
+
73
80
  ### Basic authentication
74
81
 
75
82
  `Down.download` and `Down.open` will automatically detect and apply HTTP basic
@@ -235,7 +242,6 @@ The following backends are available:
235
242
  * [Down::NetHttp](#downnethttp) (default)
236
243
  * [Down::Http](#downhttp)
237
244
  * [Down::Httpx](#downhttpx)
238
- * [Down::Wget](#downwget)
239
245
 
240
246
  You can use the backend directly:
241
247
 
@@ -450,7 +456,7 @@ supports the HTTP/2 protocol, in addition to many other features.
450
456
 
451
457
  ```rb
452
458
  gem "down", "~> 5.0"
453
- gem "httpx", "~> 0.22"
459
+ gem "httpx", "~> 1.0"
454
460
  ```
455
461
  ```rb
456
462
  require "down/httpx"
@@ -465,61 +471,8 @@ io #=> #<Down::ChunkedIO ...>
465
471
  It's implemented in much of the same way as `Down::Http`, so be sure to check
466
472
  its docs for ways to pass additional options.
467
473
 
468
- ### Down::Wget (experimental)
469
-
470
- The `Down::Wget` backend implements downloads using the `wget` command line
471
- utility.
472
-
473
- ```rb
474
- gem "down", "~> 5.0"
475
- gem "posix-spawn" # omit if on JRuby
476
- gem "http_parser.rb"
477
- ```
478
- ```rb
479
- require "down/wget"
480
-
481
- tempfile = Down::Wget.download("http://nature.com/forest.jpg")
482
- tempfile #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20150925-55456-z7vxqz.jpg>
483
-
484
- io = Down::Wget.open("http://nature.com/forest.jpg")
485
- io #=> #<Down::ChunkedIO ...>
486
- ```
487
-
488
- One major advantage of `wget` is that it automatically resumes downloads that
489
- were interrupted due to network failures, which is very useful when you're
490
- downloading large files.
491
-
492
- However, the Wget backend should still be considered experimental, as it wasn't
493
- easy to implement a CLI wrapper that streams output, so it's possible that I've
494
- made mistakes. Let me know how it's working out for you 😉.
495
-
496
- #### Additional arguments
497
-
498
- You can pass additional arguments to the underlying `wget` commmand via symbols:
499
-
500
- ```rb
501
- Down::Wget.download("http://nature.com/forest.jpg", :no_proxy, connect_timeout: 3)
502
- Down::Wget.open("http://nature.com/forest.jpg", user: "janko", password: "secret")
503
- ```
504
-
505
- You can also initialize the backend with default arguments:
506
-
507
- ```rb
508
- wget = Down::Wget.new(:no_proxy, connect_timeout: 3)
509
-
510
- wget.download("http://nature.com/forest.jpg")
511
- wget.open("http://nature.com/forest.jpg")
512
- ```
513
-
514
474
  ## Development
515
475
 
516
- Tests require that a [httpbin] server is running locally, which you can do via Docker:
517
-
518
- ```sh
519
- $ docker pull kennethreitz/httpbin
520
- $ docker run -p 80:80 kennethreitz/httpbin
521
- ```
522
-
523
476
  Then you can run tests:
524
477
 
525
478
  ```
@@ -535,4 +488,3 @@ $ bundle exec rake test
535
488
  [http.rb]: https://github.com/httprb/http
536
489
  [HTTPX]: https://github.com/HoneyryderChuck/httpx
537
490
  [Addressable::URI]: https://github.com/sporkmonger/addressable
538
- [httpbin]: https://github.com/postmanlabs/httpbin
data/down.gemspec CHANGED
@@ -4,9 +4,9 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "down"
5
5
  spec.version = Down::VERSION
6
6
 
7
- spec.required_ruby_version = ">= 2.3"
7
+ spec.required_ruby_version = ">= 3.2"
8
8
 
9
- spec.summary = "Robust streaming downloads using Net::HTTP, HTTP.rb or wget."
9
+ spec.summary = "Robust streaming downloads using Net::HTTP, http.rb or HTTPX."
10
10
  spec.homepage = "https://github.com/janko/down"
11
11
  spec.authors = ["Janko Marohnić"]
12
12
  spec.email = ["janko.marohnic@gmail.com"]
@@ -16,18 +16,13 @@ Gem::Specification.new do |spec|
16
16
  spec.require_path = "lib"
17
17
 
18
18
  spec.add_dependency "addressable", "~> 2.8"
19
+ spec.add_dependency "base64", "~> 0.3"
19
20
 
20
- spec.add_development_dependency "minitest", "~> 5.8"
21
+ spec.add_development_dependency "minitest", "~> 6.0"
21
22
  spec.add_development_dependency "mocha", "~> 1.5"
22
23
  spec.add_development_dependency "rake"
23
- spec.add_development_dependency "httpx", "~> 0", "< 2.0.0"
24
- # http 5.0 drop support of ruby 2.3 and 2.4. We still support those versions.
25
- if RUBY_VERSION >= "2.5"
26
- spec.add_development_dependency "http", "~> 5.0"
27
- else
28
- spec.add_development_dependency "http", "~> 4.3"
29
- end
30
- spec.add_development_dependency "posix-spawn" unless RUBY_ENGINE == "jruby"
31
- spec.add_development_dependency "http_parser.rb" unless RUBY_ENGINE == "jruby"
32
- spec.add_development_dependency "warning" if RUBY_VERSION >= "2.4"
24
+ spec.add_development_dependency "httpx", "~> 1.0", "< 1.4.4"
25
+ spec.add_development_dependency "http", "~> 6.0"
26
+ spec.add_development_dependency "warning"
27
+ spec.add_development_dependency "csv"
33
28
  end
@@ -42,6 +42,11 @@ module Down
42
42
  retrieve_chunk # fetch first chunk so that we know whether the file is empty
43
43
  end
44
44
 
45
+ # For compatibility with multipart-post gem.
46
+ def length
47
+ @size
48
+ end
49
+
45
50
  # Yields elements of the underlying enumerator.
46
51
  def each_chunk
47
52
  fail IOError, "closed stream" if closed?
@@ -105,7 +110,7 @@ module Down
105
110
  separator = separator_or_limit
106
111
  end
107
112
 
108
- return read(limit) if separator.nil?
113
+ return read(limit)&.force_encoding(@encoding) if separator.nil?
109
114
 
110
115
  separator = "\n\n" if separator.empty?
111
116
 
@@ -124,6 +129,8 @@ module Down
124
129
  data.clear # deallocate data
125
130
 
126
131
  if extra
132
+ @position -= extra.bytesize
133
+
127
134
  if cache
128
135
  cache.pos -= extra.bytesize
129
136
  else
@@ -238,6 +245,7 @@ module Down
238
245
  return false if cache && !cache.eof?
239
246
  @buffer.nil? && chunks_depleted?
240
247
  end
248
+ alias eof eof?
241
249
 
242
250
  # Implements IO#rewind semantics. Rewinds the Down::ChunkedIO by rewinding
243
251
  # the cache and setting the position to the beginning of the file. Raises
data/lib/down/http.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- gem "http", ">= 2.1.0", "< 6"
3
+ gem "http", ">= 2.1.0", "< 7"
4
4
 
5
5
  require "http"
6
+ require "addressable/uri"
6
7
 
7
8
  require "down/backend"
8
9
 
@@ -12,36 +13,39 @@ module Down
12
13
  # Provides streaming downloads implemented with HTTP.rb.
13
14
  class Http < Backend
14
15
  # Initializes the backend with common defaults.
15
- def initialize(**options, &block)
16
- @method = options.delete(:method) || :get
16
+ def initialize(method: :get, **options, &block)
17
+ @method = method
17
18
  @client = HTTP
18
19
  .headers("User-Agent" => "Down/#{Down::VERSION}")
19
20
  .follow(max_hops: 2)
20
21
  .timeout(connect: 30, write: 30, read: 30)
21
22
 
22
- @client = HTTP::Client.new(@client.default_options.merge(options)) if options.any?
23
+ if options.any?
24
+ client_class = defined?(HTTP::Session) ? HTTP::Session : HTTP::Client
25
+ @client = client_class.new(@client.default_options.merge(options))
26
+ end
23
27
  @client = block.call(@client) if block
24
28
  end
25
29
 
26
30
  # Downlods the remote file to disk. Accepts HTTP.rb options via a hash or a
27
31
  # block, and some additional options as well.
28
- def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, **options, &block)
29
- response = request(url, **options, &block)
32
+ def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, tempfile_name: nil, **, &)
33
+ response = request(url, **, &)
30
34
 
31
- content_length_proc.call(response.content_length) if content_length_proc && response.content_length
35
+ content_length_proc&.call(response.content_length) if response.content_length
32
36
 
33
37
  if max_size && response.content_length && response.content_length > max_size
34
38
  raise Down::TooLarge, "file is too large (#{response.content_length/1024/1024}MB, max is #{max_size/1024/1024}MB)"
35
39
  end
36
40
 
37
41
  extname = extension ? ".#{extension}" : File.extname(response.uri.path)
38
- tempfile = Tempfile.new(["down-http", extname], binmode: true)
42
+ tempfile = Tempfile.new([tempfile_name || "down-http", extname], binmode: true)
39
43
 
40
44
  stream_body(response) do |chunk|
41
45
  tempfile.write(chunk)
42
46
  chunk.clear # deallocate string
43
47
 
44
- progress_proc.call(tempfile.size) if progress_proc
48
+ progress_proc&.call(tempfile.size)
45
49
 
46
50
  if max_size && tempfile.size > max_size
47
51
  raise Down::TooLarge, "file is too large (#{tempfile.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
@@ -56,15 +60,15 @@ module Down
56
60
 
57
61
  download_result(tempfile, destination)
58
62
  rescue
59
- tempfile.close! if tempfile
63
+ tempfile&.close!
60
64
  raise
61
65
  end
62
66
 
63
67
  # Starts retrieving the remote file and returns an IO-like object which
64
68
  # downloads the response body on-demand. Accepts HTTP.rb options via a hash
65
69
  # or a block.
66
- def open(url, rewindable: true, **options, &block)
67
- response = request(url, **options, &block)
70
+ def open(url, rewindable: true, **, &)
71
+ response = request(url, **, &)
68
72
 
69
73
  Down::ChunkedIO.new(
70
74
  chunks: enum_for(:stream_body, response),
@@ -81,27 +85,27 @@ module Down
81
85
 
82
86
  private
83
87
 
84
- def request(url, method: @method, **options, &block)
85
- response = send_request(method, url, **options, &block)
88
+ def request(url, method: @method, **, &)
89
+ response = send_request(method, url, **, &)
86
90
  response_error!(response) unless response.status.success?
87
91
  response
88
92
  end
89
93
 
90
- def send_request(method, url, **options, &block)
94
+ def send_request(method, url, **, &block)
91
95
  uri = HTTP::URI.parse(url)
92
96
 
93
97
  client = @client
94
98
  client = client.basic_auth(user: uri.user, pass: uri.password) if uri.user || uri.password
95
99
  client = block.call(client) if block
96
100
 
97
- client.request(method, url, options)
101
+ client.request(method, url, **)
98
102
  rescue => exception
99
103
  request_error!(exception)
100
104
  end
101
105
 
102
106
  # Yields chunks of the response body to the block.
103
- def stream_body(response, &block)
104
- response.body.each(&block)
107
+ def stream_body(response, &)
108
+ response.body.each(&)
105
109
  rescue => exception
106
110
  request_error!(exception)
107
111
  ensure
@@ -123,7 +127,7 @@ module Down
123
127
  # Re-raise HTTP.rb exceptions as Down::Error exceptions.
124
128
  def request_error!(exception)
125
129
  case exception
126
- when HTTP::Request::UnsupportedSchemeError, Addressable::URI::InvalidURIError
130
+ when HTTP::Request::UnsupportedSchemeError, Addressable::URI::InvalidURIError, *invalid_url_errors
127
131
  raise Down::InvalidUrl, exception.message
128
132
  when HTTP::ConnectionError
129
133
  raise Down::ConnectionError, exception.message
@@ -138,6 +142,10 @@ module Down
138
142
  end
139
143
  end
140
144
 
145
+ def invalid_url_errors
146
+ defined?(HTTP::URI::InvalidError) ? [HTTP::URI::InvalidError] : []
147
+ end
148
+
141
149
  # Defines some additional attributes for the returned Tempfile.
142
150
  module DownloadedFile
143
151
  attr_accessor :url, :headers
data/lib/down/httpx.rb CHANGED
@@ -6,19 +6,16 @@ require "httpx"
6
6
 
7
7
  require "down/backend"
8
8
 
9
-
10
9
  module Down
11
10
  # Provides streaming downloads implemented with HTTPX.
12
11
  class Httpx < Backend
13
- # Initializes the backend
14
-
15
12
  USER_AGENT = "Down/#{Down::VERSION}"
16
13
 
17
14
  BASIC_AUTH = HTTPX::VERSION >= "1.0.0" ? :basic_auth : :basic_authentication
18
15
 
19
- def initialize(**options, &block)
20
- @method = options.delete(:method) || :get
21
- headers = options.delete(:headers) || {}
16
+ # Initializes the backend with common defaults.
17
+ def initialize(method: :get, headers: {}, **options, &block)
18
+ @method = method
22
19
  @client = HTTPX
23
20
  .plugin(:follow_redirects, max_redirects: 2)
24
21
  .plugin(BASIC_AUTH)
@@ -32,20 +29,15 @@ module Down
32
29
  @client = block.call(@client) if block
33
30
  end
34
31
 
35
-
36
- # Downlods the remote file to disk. Accepts HTTPX options via a hash or a
32
+ # Downloads the remote file to disk. Accepts HTTPX options via a hash or a
37
33
  # block, and some additional options as well.
38
- def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, **options, &block)
39
- client = @client
40
-
41
- response = request(client, url, **options, &block)
42
-
43
- content_length = nil
34
+ def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, extension: nil, tempfile_name: nil, **, &)
35
+ response = request(url, **, &)
44
36
 
45
37
  if response.headers.key?("content-length")
46
38
  content_length = response.headers["content-length"].to_i
47
39
 
48
- content_length_proc.call(content_length) if content_length_proc
40
+ content_length_proc&.call(content_length)
49
41
 
50
42
  if max_size && content_length > max_size
51
43
  response.close
@@ -54,13 +46,13 @@ module Down
54
46
  end
55
47
 
56
48
  extname = extension ? ".#{extension}" : File.extname(response.uri.path)
57
- tempfile = Tempfile.new(["down-http", extname], binmode: true)
49
+ tempfile = Tempfile.new([tempfile_name || "down-http", extname], binmode: true)
58
50
 
59
51
  stream_body(response) do |chunk|
60
52
  tempfile.write(chunk)
61
53
  chunk.clear # deallocate string
62
54
 
63
- progress_proc.call(tempfile.size) if progress_proc
55
+ progress_proc&.call(tempfile.size)
64
56
 
65
57
  if max_size && tempfile.size > max_size
66
58
  raise Down::TooLarge, "file is too large (#{tempfile.size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
@@ -77,20 +69,19 @@ module Down
77
69
 
78
70
  download_result(tempfile, destination)
79
71
  rescue
80
- tempfile.close! if tempfile
72
+ tempfile&.close!
81
73
  raise
82
74
  end
83
75
 
84
76
  # Starts retrieving the remote file and returns an IO-like object which
85
- # downloads the response body on-demand. Accepts HTTP.rb options via a hash
77
+ # downloads the response body on-demand. Accepts HTTPX options via a hash
86
78
  # or a block.
87
- def open(url, rewindable: true, **options, &block)
88
- response = request(@client, url, stream: true, **options, &block)
89
- size = response.headers["content-length"]
90
- size = size.to_i if size
79
+ def open(url, rewindable: true, **, &)
80
+ response = request(url, stream: true, **, &)
81
+
91
82
  Down::ChunkedIO.new(
92
83
  chunks: enum_for(:stream_body, response),
93
- size: size,
84
+ size: response.headers["content-length"]&.to_i,
94
85
  encoding: response.body.encoding,
95
86
  rewindable: rewindable,
96
87
  data: {
@@ -105,15 +96,23 @@ module Down
105
96
 
106
97
  # Yields chunks of the response body to the block.
107
98
  def stream_body(response, &block)
108
- response.each(&block)
99
+ response.each do |chunk|
100
+ next if (300..399).include?(response.status)
101
+
102
+ block.call(chunk)
103
+ end
104
+
105
+ if (300..399).include?(response.status)
106
+ raise Down::TooManyRedirects, "too many redirects"
107
+ end
109
108
  rescue => exception
110
109
  request_error!(exception)
111
110
  end
112
111
 
113
- def request(client, url, method: @method, **options, &block)
114
- response = send_request(client, method, url, **options, &block)
112
+ def request(url, method: @method, **, &)
113
+ response = send_request(method, url, **, &)
115
114
  response.raise_for_status
116
- response_error!(response) unless (200..299).include?(response.status)
115
+ response_error!(response) unless (200..399).include?(response.status)
117
116
  response
118
117
  rescue HTTPX::HTTPError
119
118
  response_error!(response)
@@ -121,7 +120,7 @@ module Down
121
120
  request_error!(error)
122
121
  end
123
122
 
124
- def send_request(client, method, url, **options, &block)
123
+ def send_request(method, url, **, &block)
125
124
  uri = URI(url)
126
125
  client = @client
127
126
  if uri.user || uri.password
@@ -130,7 +129,7 @@ module Down
130
129
  end
131
130
  client = block.call(client) if block
132
131
 
133
- client.request(method.to_s.upcase, uri, stream: true, **options)
132
+ client.request(method.to_s.upcase, uri, stream: true, **)
134
133
  rescue => exception
135
134
  request_error!(exception)
136
135
  end
@@ -140,7 +139,6 @@ module Down
140
139
  args = [response.status.to_s, response]
141
140
 
142
141
  case response.status
143
- when 300..399 then raise Down::TooManyRedirects, "too many redirects"
144
142
  when 404 then raise Down::NotFound.new(*args)
145
143
  when 400..499 then raise Down::ClientError.new(*args)
146
144
  when 500..599 then raise Down::ServerError.new(*args)
data/lib/down/net_http.rb CHANGED
@@ -17,30 +17,33 @@ module Down
17
17
  addressable_uri.normalize.to_s
18
18
  end
19
19
 
20
+ DEFAULT_OPTIONS = {
21
+ headers: { "User-Agent" => "Down/#{Down::VERSION}" }.freeze,
22
+ max_redirects: 2,
23
+ open_timeout: 30,
24
+ read_timeout: 30,
25
+ uri_normalizer: URI_NORMALIZER,
26
+ }.freeze
27
+
20
28
  # Initializes the backend with common defaults.
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)
29
+ def initialize(...)
30
+ @options = merge_options(DEFAULT_OPTIONS, ...)
29
31
  end
30
32
 
31
33
  # Downloads a remote file to disk using open-uri. Accepts any open-uri
32
34
  # options, and a few more.
33
- def download(url, *args, **options)
34
- options = merge_options(@options, *args, **options)
35
+ def download(url, ...)
36
+ options = merge_options(@options, ...)
35
37
 
36
38
  max_size = options.delete(:max_size)
37
- max_redirects = options.delete(:max_redirects)
38
39
  progress_proc = options.delete(:progress_proc)
39
40
  content_length_proc = options.delete(:content_length_proc)
41
+ proxy = options.delete(:proxy)
40
42
  destination = options.delete(:destination)
41
43
  headers = options.delete(:headers)
42
44
  uri_normalizer = options.delete(:uri_normalizer)
43
45
  extension = options.delete(:extension)
46
+ tempfile_name = options.delete(:tempfile_name)
44
47
 
45
48
  # Use open-uri's :content_lenth_proc or :progress_proc to raise an
46
49
  # exception early if the file is too large.
@@ -52,20 +55,20 @@ module Down
52
55
  if size && max_size && size > max_size
53
56
  raise Down::TooLarge, "file is too large (#{size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
54
57
  end
55
- content_length_proc.call(size) if content_length_proc
58
+ content_length_proc&.call(size)
56
59
  },
57
60
  progress_proc: proc { |current_size|
58
61
  if max_size && current_size > max_size
59
62
  raise Down::TooLarge, "file is too large (#{current_size/1024/1024}MB, max is #{max_size/1024/1024}MB)"
60
63
  end
61
- progress_proc.call(current_size) if progress_proc
64
+ progress_proc&.call(current_size)
62
65
  },
63
66
  redirect: false,
64
67
  }
65
68
 
66
69
  # Handle basic authentication in the :proxy option.
67
- if options[:proxy]
68
- proxy = URI(options.delete(:proxy))
70
+ if proxy
71
+ proxy = URI(proxy)
69
72
  user = proxy.user
70
73
  password = proxy.password
71
74
 
@@ -79,10 +82,9 @@ module Down
79
82
  end
80
83
  end
81
84
 
82
- open_uri_options.merge!(options)
83
- open_uri_options.merge!(headers)
85
+ open_uri_options.merge!(options, headers)
84
86
 
85
- uri = ensure_uri(normalize_uri(url, uri_normalizer: uri_normalizer))
87
+ uri = ensure_uri(normalize_uri(url, uri_normalizer:))
86
88
 
87
89
  # Handle basic authentication in the remote URL.
88
90
  if uri.user || uri.password
@@ -91,11 +93,11 @@ module Down
91
93
  uri.password = nil
92
94
  end
93
95
 
94
- open_uri_file = open_uri(uri, open_uri_options, follows_remaining: max_redirects)
96
+ open_uri_file = open_uri(uri, **open_uri_options)
95
97
 
96
98
  # Handle the fact that open-uri returns StringIOs for small files.
97
99
  extname = extension ? ".#{extension}" : File.extname(open_uri_file.base_uri.path)
98
- tempfile = ensure_tempfile(open_uri_file, extname)
100
+ tempfile = ensure_tempfile(open_uri_file, extname, tempfile_name)
99
101
  OpenURI::Meta.init tempfile, open_uri_file # add back open-uri methods
100
102
  tempfile.extend Down::NetHttp::DownloadedFile
101
103
 
@@ -104,17 +106,16 @@ module Down
104
106
 
105
107
  # Starts retrieving the remote file using Net::HTTP and returns an IO-like
106
108
  # object which downloads the response body on-demand.
107
- def open(url, *args, **options)
108
- options = merge_options(@options, *args, **options)
109
+ def open(url, ...)
110
+ options = merge_options(@options, ...)
109
111
 
110
- max_redirects = options.delete(:max_redirects)
111
112
  uri_normalizer = options.delete(:uri_normalizer)
112
113
 
113
- uri = ensure_uri(normalize_uri(url, uri_normalizer: uri_normalizer))
114
+ uri = ensure_uri(normalize_uri(url, uri_normalizer:))
114
115
 
115
116
  # Create a Fiber that halts when response headers are received.
116
117
  request = Fiber.new do
117
- net_http_request(uri, options, follows_remaining: max_redirects) do |response|
118
+ net_http_request(uri, **options) do |response|
118
119
  Fiber.yield response
119
120
  end
120
121
  end
@@ -126,7 +127,7 @@ module Down
126
127
  # Build an IO-like object that will retrieve response body on-demand.
127
128
  Down::ChunkedIO.new(
128
129
  chunks: enum_for(:stream_body, response),
129
- size: response["Content-Length"] && response["Content-Length"].to_i,
130
+ size: response["Content-Length"]&.to_i,
130
131
  encoding: response.type_params["charset"],
131
132
  rewindable: options.fetch(:rewindable, true),
132
133
  on_close: -> { request.resume }, # close HTTP connnection
@@ -141,10 +142,10 @@ module Down
141
142
  private
142
143
 
143
144
  # Calls open-uri's URI::HTTP#open method. Additionally handles redirects.
144
- def open_uri(uri, options, follows_remaining:)
145
+ def open_uri(uri, max_redirects:, auth_on_redirect: false, **options)
145
146
  uri.open(options)
146
147
  rescue OpenURI::HTTPRedirect => exception
147
- raise Down::TooManyRedirects, "too many redirects" if follows_remaining == 0
148
+ raise Down::TooManyRedirects, "too many redirects" if max_redirects == 0
148
149
 
149
150
  # fail if redirect URI scheme is not http or https
150
151
  begin
@@ -152,9 +153,13 @@ module Down
152
153
  rescue Down::InvalidUrl
153
154
  response = rebuild_response_from_open_uri_exception(exception)
154
155
 
155
- raise ResponseError.new("Invalid Redirect URI: #{exception.uri}", response: response)
156
+ raise ResponseError.new("Invalid Redirect URI: #{exception.uri}", response:)
156
157
  end
157
158
 
159
+ # do not leak credentials on redirect
160
+ options.delete("Authorization") unless auth_on_redirect
161
+ options.delete(:http_basic_authentication) unless auth_on_redirect
162
+
158
163
  # forward cookies on the redirect
159
164
  if !exception.io.meta["set-cookie"].to_s.empty?
160
165
  options["Cookie"] ||= ''
@@ -164,14 +169,14 @@ module Down
164
169
  options["Cookie"] = (old_cookies | new_cookies).join(';')
165
170
  end
166
171
 
167
- follows_remaining -= 1
172
+ max_redirects -= 1
168
173
  retry
169
174
  rescue OpenURI::HTTPError => exception
170
175
  response = rebuild_response_from_open_uri_exception(exception)
171
176
 
172
177
  # open-uri attempts to parse the redirect URI, so we re-raise that exception
173
178
  if exception.message.include?("(Invalid Location URI)")
174
- raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response: response)
179
+ raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response:)
175
180
  end
176
181
 
177
182
  response_error!(response)
@@ -182,8 +187,8 @@ module Down
182
187
  # Converts the given IO into a Tempfile if it isn't one already (open-uri
183
188
  # returns a StringIO when there is less than 10KB of content), and gives
184
189
  # it the specified file extension.
185
- def ensure_tempfile(io, extension)
186
- tempfile = Tempfile.new(["down-net_http", extension], binmode: true)
190
+ def ensure_tempfile(io, extension, tempfile_name = nil)
191
+ tempfile = Tempfile.new([tempfile_name || "down-net_http", extension], binmode: true)
187
192
 
188
193
  if io.is_a?(Tempfile)
189
194
  # Windows requires file descriptors to be closed before files are moved
@@ -200,8 +205,8 @@ module Down
200
205
  end
201
206
 
202
207
  # Makes a Net::HTTP request and follows redirects.
203
- def net_http_request(uri, options, follows_remaining:, &block)
204
- http, request = create_net_http(uri, options)
208
+ def net_http_request(uri, max_redirects:, auth_on_redirect: false, **options, &)
209
+ http, request = create_net_http(uri, **options)
205
210
 
206
211
  begin
207
212
  response = http.start do
@@ -222,28 +227,38 @@ module Down
222
227
  if response.is_a?(Net::HTTPNotModified)
223
228
  raise Down::NotModified
224
229
  elsif response.is_a?(Net::HTTPRedirection)
225
- raise Down::TooManyRedirects if follows_remaining == 0
230
+ raise Down::TooManyRedirects if max_redirects == 0
226
231
 
227
232
  # fail if redirect URI is not a valid http or https URL
228
233
  begin
229
234
  location = ensure_uri(response["Location"], allow_relative: true)
230
235
  rescue Down::InvalidUrl
231
- raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response: response)
236
+ raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response:)
232
237
  end
233
238
 
239
+ # do not leak credentials on redirect
240
+ options[:headers].delete("Authorization") unless auth_on_redirect
241
+
234
242
  # handle relative redirects
235
- location = uri + location if location.relative?
243
+ if location.relative?
244
+ location = uri + location
245
+ uri.user = nil unless auth_on_redirect
246
+ uri.password = nil unless auth_on_redirect
247
+ elsif auth_on_redirect && (uri.user || uri.password)
248
+ # for absolute redirects, carry over URI credentials since the new location won't have them
249
+ options[:http_basic_authentication] ||= [uri.user, uri.password]
250
+ end
236
251
 
237
- net_http_request(location, options, follows_remaining: follows_remaining - 1, &block)
252
+ net_http_request(location, **options, max_redirects: max_redirects - 1, auth_on_redirect:, &)
238
253
  end
239
254
  end
240
255
 
241
256
  # Build a Net::HTTP object for making a request.
242
- def create_net_http(uri, options)
257
+ def create_net_http(uri, proxy: nil, ssl_verify_mode: nil, ssl_ca_cert: nil, read_timeout: nil, open_timeout: nil, headers: {}, http_basic_authentication: nil, **)
243
258
  http_class = Net::HTTP
244
259
 
245
- if options[:proxy]
246
- proxy = URI(options[:proxy])
260
+ if proxy
261
+ proxy = URI(proxy)
247
262
  http_class = Net::HTTP::Proxy(proxy.hostname, proxy.port, proxy.user, proxy.password)
248
263
  end
249
264
 
@@ -252,10 +267,10 @@ module Down
252
267
  # Handle SSL parameters (taken from the open-uri implementation).
253
268
  if uri.is_a?(URI::HTTPS)
254
269
  http.use_ssl = true
255
- http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
270
+ http.verify_mode = ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER
256
271
  store = OpenSSL::X509::Store.new
257
- if options[:ssl_ca_cert]
258
- Array(options[:ssl_ca_cert]).each do |cert|
272
+ if ssl_ca_cert
273
+ Array(ssl_ca_cert).each do |cert|
259
274
  File.directory?(cert) ? store.add_path(cert) : store.add_file(cert)
260
275
  end
261
276
  else
@@ -264,23 +279,20 @@ module Down
264
279
  http.cert_store = store
265
280
  end
266
281
 
267
- http.read_timeout = options[:read_timeout] if options.key?(:read_timeout)
268
- http.open_timeout = options[:open_timeout] if options.key?(:open_timeout)
269
-
270
- headers = options[:headers].to_h
271
- headers["Accept-Encoding"] = "" # Net::HTTP's inflater causes FiberErrors
282
+ http.read_timeout = read_timeout
283
+ http.open_timeout = open_timeout
272
284
 
273
- get = Net::HTTP::Get.new(uri, headers)
285
+ get = Net::HTTP::Get.new(uri, headers.to_h)
274
286
 
275
- user, password = options[:http_basic_authentication] || [uri.user, uri.password]
287
+ user, password = http_basic_authentication || [uri.user, uri.password]
276
288
  get.basic_auth(user, password) if user || password
277
289
 
278
290
  [http, get]
279
291
  end
280
292
 
281
293
  # Yields chunks of the response body to the block.
282
- def stream_body(response, &block)
283
- response.read_body(&block)
294
+ def stream_body(response, &)
295
+ response.read_body(&)
284
296
  rescue => exception
285
297
  request_error!(exception)
286
298
  end
@@ -370,6 +382,8 @@ module Down
370
382
  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", ... }, ...)`)
371
383
  new_options[:headers] = new_options.select { |key, value| key.is_a?(String) }
372
384
  new_options.reject! { |key, value| key.is_a?(String) }
385
+ else
386
+ new_options[:headers] ||= {}
373
387
  end
374
388
 
375
389
  options.merge(new_options) do |key, value1, value2|
data/lib/down/utils.rb CHANGED
@@ -1,4 +1,8 @@
1
- require "cgi"
1
+ if RUBY_VERSION >= "4.0"
2
+ require "cgi/escape"
3
+ else
4
+ require "cgi/util"
5
+ end
2
6
 
3
7
  module Down
4
8
  module Utils
data/lib/down/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Down
4
- VERSION = "5.4.2"
4
+ VERSION = "5.6.1"
5
5
  end
data/lib/down/wget.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ warn "The Down::Wget backend has been deprecated and will be removed in the next major version. Please use the Net::HTTP/open-uri, http.rb or HTTPX backend instead."
4
+
3
5
  begin
4
6
  require "posix-spawn"
5
7
  rescue LoadError
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, **options, &block)
10
- backend.download(*args, **options, &block)
9
+ def download(...)
10
+ backend.download(...)
11
11
  end
12
12
 
13
- def open(*args, **options, &block)
14
- backend.open(*args, **options, &block)
13
+ def open(...)
14
+ backend.open(...)
15
15
  end
16
16
 
17
17
  # Allows setting a backend via a symbol or a downloader object.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: down
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.2
4
+ version: 5.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-04-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -24,20 +23,34 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '2.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: base64
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: minitest
29
42
  requirement: !ruby/object:Gem::Requirement
30
43
  requirements:
31
44
  - - "~>"
32
45
  - !ruby/object:Gem::Version
33
- version: '5.8'
46
+ version: '6.0'
34
47
  type: :development
35
48
  prerelease: false
36
49
  version_requirements: !ruby/object:Gem::Requirement
37
50
  requirements:
38
51
  - - "~>"
39
52
  - !ruby/object:Gem::Version
40
- version: '5.8'
53
+ version: '6.0'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: mocha
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -72,50 +85,36 @@ dependencies:
72
85
  requirements:
73
86
  - - "~>"
74
87
  - !ruby/object:Gem::Version
75
- version: '0'
88
+ version: '1.0'
76
89
  - - "<"
77
90
  - !ruby/object:Gem::Version
78
- version: 2.0.0
91
+ version: 1.4.4
79
92
  type: :development
80
93
  prerelease: false
81
94
  version_requirements: !ruby/object:Gem::Requirement
82
95
  requirements:
83
96
  - - "~>"
84
97
  - !ruby/object:Gem::Version
85
- version: '0'
98
+ version: '1.0'
86
99
  - - "<"
87
100
  - !ruby/object:Gem::Version
88
- version: 2.0.0
101
+ version: 1.4.4
89
102
  - !ruby/object:Gem::Dependency
90
103
  name: http
91
104
  requirement: !ruby/object:Gem::Requirement
92
105
  requirements:
93
106
  - - "~>"
94
107
  - !ruby/object:Gem::Version
95
- version: '5.0'
108
+ version: '6.0'
96
109
  type: :development
97
110
  prerelease: false
98
111
  version_requirements: !ruby/object:Gem::Requirement
99
112
  requirements:
100
113
  - - "~>"
101
114
  - !ruby/object:Gem::Version
102
- version: '5.0'
103
- - !ruby/object:Gem::Dependency
104
- name: posix-spawn
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
115
+ version: '6.0'
117
116
  - !ruby/object:Gem::Dependency
118
- name: http_parser.rb
117
+ name: warning
119
118
  requirement: !ruby/object:Gem::Requirement
120
119
  requirements:
121
120
  - - ">="
@@ -129,7 +128,7 @@ dependencies:
129
128
  - !ruby/object:Gem::Version
130
129
  version: '0'
131
130
  - !ruby/object:Gem::Dependency
132
- name: warning
131
+ name: csv
133
132
  requirement: !ruby/object:Gem::Requirement
134
133
  requirements:
135
134
  - - ">="
@@ -142,7 +141,6 @@ dependencies:
142
141
  - - ">="
143
142
  - !ruby/object:Gem::Version
144
143
  version: '0'
145
- description:
146
144
  email:
147
145
  - janko.marohnic@gmail.com
148
146
  executables: []
@@ -167,7 +165,6 @@ homepage: https://github.com/janko/down
167
165
  licenses:
168
166
  - MIT
169
167
  metadata: {}
170
- post_install_message:
171
168
  rdoc_options: []
172
169
  require_paths:
173
170
  - lib
@@ -175,15 +172,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
175
172
  requirements:
176
173
  - - ">="
177
174
  - !ruby/object:Gem::Version
178
- version: '2.3'
175
+ version: '3.2'
179
176
  required_rubygems_version: !ruby/object:Gem::Requirement
180
177
  requirements:
181
178
  - - ">="
182
179
  - !ruby/object:Gem::Version
183
180
  version: '0'
184
181
  requirements: []
185
- rubygems_version: 3.5.3
186
- signing_key:
182
+ rubygems_version: 4.0.13
187
183
  specification_version: 4
188
- summary: Robust streaming downloads using Net::HTTP, HTTP.rb or wget.
184
+ summary: Robust streaming downloads using Net::HTTP, http.rb or HTTPX.
189
185
  test_files: []