down 2.5.1 → 3.0.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/README.md +190 -99
- data/down.gemspec +12 -12
- data/lib/down.rb +3 -215
- data/lib/down/chunked_io.rb +81 -46
- data/lib/down/errors.rb +16 -0
- data/lib/down/http.rb +150 -0
- data/lib/down/net_http.rb +221 -0
- data/lib/down/version.rb +1 -1
- metadata +16 -42
- data/lib/down/wget.rb +0 -20
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
require "open-uri"
|
|
2
|
+
require "net/http"
|
|
3
|
+
|
|
4
|
+
require "down/version"
|
|
5
|
+
require "down/chunked_io"
|
|
6
|
+
require "down/errors"
|
|
7
|
+
|
|
8
|
+
require "tempfile"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "cgi"
|
|
11
|
+
|
|
12
|
+
module Down
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def download(uri, options = {})
|
|
16
|
+
NetHttp.download(uri, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def open(uri, options = {})
|
|
20
|
+
NetHttp.open(uri, options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_to_tempfile(basename, io)
|
|
24
|
+
NetHttp.copy_to_tempfile(basename, io)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module NetHttp
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def download(uri, options = {})
|
|
31
|
+
max_size = options.delete(:max_size)
|
|
32
|
+
max_redirects = options.delete(:max_redirects) || 2
|
|
33
|
+
progress_proc = options.delete(:progress_proc)
|
|
34
|
+
content_length_proc = options.delete(:content_length_proc)
|
|
35
|
+
|
|
36
|
+
open_uri_options = {
|
|
37
|
+
"User-Agent" => "Down/#{VERSION}",
|
|
38
|
+
content_length_proc: proc { |size|
|
|
39
|
+
if size && max_size && size > max_size
|
|
40
|
+
raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
|
|
41
|
+
end
|
|
42
|
+
content_length_proc.call(size) if content_length_proc
|
|
43
|
+
},
|
|
44
|
+
progress_proc: proc { |current_size|
|
|
45
|
+
if max_size && current_size > max_size
|
|
46
|
+
raise Down::TooLarge, "file is too large (max is #{max_size/1024/1024}MB)"
|
|
47
|
+
end
|
|
48
|
+
progress_proc.call(current_size) if progress_proc
|
|
49
|
+
},
|
|
50
|
+
redirect: false,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if options[:proxy]
|
|
54
|
+
proxy = URI(options.delete(:proxy))
|
|
55
|
+
user = proxy.user
|
|
56
|
+
password = proxy.password
|
|
57
|
+
|
|
58
|
+
if user || password
|
|
59
|
+
proxy.user = nil
|
|
60
|
+
proxy.password = nil
|
|
61
|
+
|
|
62
|
+
open_uri_options[:proxy_http_basic_authentication] = [proxy.to_s, user, password]
|
|
63
|
+
else
|
|
64
|
+
open_uri_options[:proxy] = proxy.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
open_uri_options.update(options)
|
|
69
|
+
|
|
70
|
+
tries = max_redirects + 1
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
uri = URI(uri)
|
|
74
|
+
|
|
75
|
+
if uri.class != URI::HTTP && uri.class != URI::HTTPS
|
|
76
|
+
raise URI::InvalidURIError, "url is not http nor https"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if uri.user || uri.password
|
|
80
|
+
open_uri_options[:http_basic_authentication] ||= [uri.user, uri.password]
|
|
81
|
+
uri.user = nil
|
|
82
|
+
uri.password = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
downloaded_file = uri.open(open_uri_options)
|
|
86
|
+
rescue OpenURI::HTTPRedirect => redirect
|
|
87
|
+
if (tries -= 1) > 0
|
|
88
|
+
uri = redirect.uri
|
|
89
|
+
|
|
90
|
+
if !redirect.io.meta["set-cookie"].to_s.empty?
|
|
91
|
+
open_uri_options["Cookie"] = redirect.io.meta["set-cookie"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
retry
|
|
95
|
+
else
|
|
96
|
+
raise Down::NotFound, "too many redirects"
|
|
97
|
+
end
|
|
98
|
+
rescue OpenURI::HTTPError,
|
|
99
|
+
URI::InvalidURIError,
|
|
100
|
+
Errno::ECONNREFUSED,
|
|
101
|
+
SocketError,
|
|
102
|
+
Timeout::Error
|
|
103
|
+
raise Down::NotFound, "file not found"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# open-uri will return a StringIO instead of a Tempfile if the filesize is
|
|
107
|
+
# less than 10 KB, so if it happens we convert it back to Tempfile. We want
|
|
108
|
+
# to do this with a Tempfile as well, because open-uri doesn't preserve the
|
|
109
|
+
# file extension, so we want to run it against #copy_to_tempfile which
|
|
110
|
+
# does.
|
|
111
|
+
open_uri_file = downloaded_file
|
|
112
|
+
downloaded_file = copy_to_tempfile(uri.path, open_uri_file)
|
|
113
|
+
OpenURI::Meta.init downloaded_file, open_uri_file
|
|
114
|
+
|
|
115
|
+
downloaded_file.extend DownloadedFile
|
|
116
|
+
downloaded_file
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def open(uri, options = {})
|
|
120
|
+
uri = URI(uri)
|
|
121
|
+
http_class = Net::HTTP
|
|
122
|
+
|
|
123
|
+
if options[:proxy]
|
|
124
|
+
proxy = URI(options[:proxy])
|
|
125
|
+
http_class = Net::HTTP::Proxy(proxy.hostname, proxy.port, proxy.user, proxy.password)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
http = http_class.new(uri.host, uri.port)
|
|
129
|
+
|
|
130
|
+
# taken from open-uri implementation
|
|
131
|
+
if uri.is_a?(URI::HTTPS)
|
|
132
|
+
require "net/https"
|
|
133
|
+
http.use_ssl = true
|
|
134
|
+
http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
|
|
135
|
+
store = OpenSSL::X509::Store.new
|
|
136
|
+
if options[:ssl_ca_cert]
|
|
137
|
+
Array(options[:ssl_ca_cert]).each do |cert|
|
|
138
|
+
File.directory?(cert) ? store.add_path(cert) : store.add_file(cert)
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
store.set_default_paths
|
|
142
|
+
end
|
|
143
|
+
http.cert_store = store
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
request_headers = options.select { |key, value| key.is_a?(String) }
|
|
147
|
+
get = Net::HTTP::Get.new(uri.request_uri, request_headers)
|
|
148
|
+
get.basic_auth(uri.user, uri.password) if uri.user || uri.password
|
|
149
|
+
|
|
150
|
+
request = Fiber.new do
|
|
151
|
+
http.start do
|
|
152
|
+
http.request(get) do |response|
|
|
153
|
+
Fiber.yield response
|
|
154
|
+
response.instance_variable_set("@read", true)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
response = request.resume
|
|
160
|
+
|
|
161
|
+
raise Down::NotFound, "request returned status #{response.code} and body:\n#{response.body}" if response.code.to_i.between?(400, 599)
|
|
162
|
+
|
|
163
|
+
down_params = {
|
|
164
|
+
chunks: response.enum_for(:read_body),
|
|
165
|
+
size: response["Content-Length"] && response["Content-Length"].to_i,
|
|
166
|
+
on_close: -> { request.resume }, # close HTTP connnection
|
|
167
|
+
data: {
|
|
168
|
+
status: response.code.to_i,
|
|
169
|
+
headers: response.each_header.inject({}) { |headers, (downcased_name, value)|
|
|
170
|
+
name = downcased_name.split("-").map(&:capitalize).join("-")
|
|
171
|
+
headers.merge!(name => value)
|
|
172
|
+
},
|
|
173
|
+
response: response,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
down_params[:rewindable] = options[:rewindable] if options.key?(:rewindable)
|
|
177
|
+
down_params[:encoding] = response.type_params["charset"] if response.type_params["charset"]
|
|
178
|
+
|
|
179
|
+
Down::ChunkedIO.new(down_params)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def copy_to_tempfile(basename, io)
|
|
183
|
+
tempfile = Tempfile.new(["down", File.extname(basename)], binmode: true)
|
|
184
|
+
if io.is_a?(OpenURI::Meta) && io.is_a?(Tempfile)
|
|
185
|
+
io.close
|
|
186
|
+
tempfile.close
|
|
187
|
+
FileUtils.mv io.path, tempfile.path
|
|
188
|
+
else
|
|
189
|
+
IO.copy_stream(io, tempfile)
|
|
190
|
+
io.rewind
|
|
191
|
+
end
|
|
192
|
+
tempfile.open
|
|
193
|
+
tempfile
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
module DownloadedFile
|
|
197
|
+
def original_filename
|
|
198
|
+
filename_from_content_disposition || filename_from_uri
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def content_type
|
|
202
|
+
super unless meta["content-type"].to_s.empty?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
def filename_from_content_disposition
|
|
208
|
+
content_disposition = meta["content-disposition"].to_s
|
|
209
|
+
filename = content_disposition[/filename="([^"]*)"/, 1] || content_disposition[/filename=(.+)/, 1]
|
|
210
|
+
filename = CGI.unescape(filename.to_s.strip)
|
|
211
|
+
filename unless filename.empty?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def filename_from_uri
|
|
215
|
+
path = base_uri.path
|
|
216
|
+
filename = path.split("/").last
|
|
217
|
+
CGI.unescape(filename) if filename
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
data/lib/down/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: down
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janko Marohnić
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-05-
|
|
11
|
+
date: 2017-05-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: rake
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: minitest
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -39,47 +25,33 @@ dependencies:
|
|
|
39
25
|
- !ruby/object:Gem::Version
|
|
40
26
|
version: '5.8'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - "~>"
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '2.3'
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - "~>"
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '2.3'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: addressable
|
|
28
|
+
name: mocha
|
|
57
29
|
requirement: !ruby/object:Gem::Requirement
|
|
58
30
|
requirements:
|
|
59
|
-
- - "
|
|
31
|
+
- - ">="
|
|
60
32
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
33
|
+
version: '0'
|
|
62
34
|
type: :development
|
|
63
35
|
prerelease: false
|
|
64
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
37
|
requirements:
|
|
66
|
-
- - "
|
|
38
|
+
- - ">="
|
|
67
39
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
40
|
+
version: '0'
|
|
69
41
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
42
|
+
name: http
|
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
|
72
44
|
requirements:
|
|
73
|
-
- - "
|
|
45
|
+
- - "~>"
|
|
74
46
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
47
|
+
version: '2.1'
|
|
76
48
|
type: :development
|
|
77
49
|
prerelease: false
|
|
78
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
51
|
requirements:
|
|
80
|
-
- - "
|
|
52
|
+
- - "~>"
|
|
81
53
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
54
|
+
version: '2.1'
|
|
83
55
|
description:
|
|
84
56
|
email:
|
|
85
57
|
- janko.marohnic@gmail.com
|
|
@@ -92,8 +64,10 @@ files:
|
|
|
92
64
|
- down.gemspec
|
|
93
65
|
- lib/down.rb
|
|
94
66
|
- lib/down/chunked_io.rb
|
|
67
|
+
- lib/down/errors.rb
|
|
68
|
+
- lib/down/http.rb
|
|
69
|
+
- lib/down/net_http.rb
|
|
95
70
|
- lib/down/version.rb
|
|
96
|
-
- lib/down/wget.rb
|
|
97
71
|
homepage: https://github.com/janko-m/down
|
|
98
72
|
licenses:
|
|
99
73
|
- MIT
|
|
@@ -106,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
106
80
|
requirements:
|
|
107
81
|
- - ">="
|
|
108
82
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '
|
|
83
|
+
version: '2.1'
|
|
110
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
85
|
requirements:
|
|
112
86
|
- - ">="
|
data/lib/down/wget.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
require "down/version"
|
|
2
|
-
require "down/chunked_io"
|
|
3
|
-
|
|
4
|
-
require "open3"
|
|
5
|
-
|
|
6
|
-
module Down
|
|
7
|
-
class Error < StandardError; end
|
|
8
|
-
class TooLarge < Error; end
|
|
9
|
-
class NotFound < Error; end
|
|
10
|
-
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def download(url, options = {})
|
|
14
|
-
max_size = options.delete(:max_size)
|
|
15
|
-
max_redirects = options.delete(:max_redirects) || 2
|
|
16
|
-
progress_proc = options.delete(:progress_proc) || options.delete(:progress)
|
|
17
|
-
content_length_proc = options.delete(:content_length_proc)
|
|
18
|
-
timeout = options.delete(:timeout)
|
|
19
|
-
end
|
|
20
|
-
end
|