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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/README.md +113 -38
- data/down.gemspec +11 -5
- data/lib/down/backend.rb +11 -4
- data/lib/down/chunked_io.rb +75 -42
- data/lib/down/errors.rb +12 -9
- data/lib/down/http.rb +14 -14
- data/lib/down/httpx.rb +175 -0
- data/lib/down/net_http.rb +79 -38
- data/lib/down/version.rb +1 -1
- data/lib/down/wget.rb +5 -5
- data/lib/down.rb +4 -4
- metadata +34 -13
data/lib/down/http.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
gem "http", ">= 2.1.0", "<
|
|
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
|
|
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: {
|
|
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
|
|
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:
|
|
20
|
-
open_timeout:
|
|
21
|
-
read_timeout:
|
|
22
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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:
|
|
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"]
|
|
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
|
|
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 |
|
|
195
|
-
unless
|
|
196
|
-
yield
|
|
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
|
-
|
|
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::
|
|
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.
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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
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.
|