down 4.5.0 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82abe7a56a0950e4743ebba457ceb8cadb113303d2942ab2a2cfd34bc065d06a
4
- data.tar.gz: bf7a15be5a3f4129f1ad6b85e7dba6757ac43f0877b3da4b7e18abc1703bb861
3
+ metadata.gz: 1976aa6977c5e4161c2f13e0259840edae016dea49087ce8ea3bf46918b0eaee
4
+ data.tar.gz: 24d875754c8174a2bf5030e9f35a14bda4d2f2a30b143e002eecdc128f8a8a6d
5
5
  SHA512:
6
- metadata.gz: ae99273f99ef4ac8a3a23512482f1e5fa2dbaea5697f197681ad99543b7f07b9665c0532ebdf3d87521abf1d45f34ac1a52c21e314c1408e70e5299ed44ca9b7
7
- data.tar.gz: 946eb4bda59b214c2aea5b885fd2d0ee9f85ac035cce635454a91e859d6d4dbf2e2a6de79ac6d9e631089aa12a7aa0fc7f2b96c73e6711890f3234eabe158489
6
+ metadata.gz: 6b4102338d79a8ecfb5c643aa387aec15b1af79cb4c0ef7eda29828c79e454b9037bf388b82e52fbefcfe3ab22792e1a06a46f7ed8356228c5c8f49c203972c3
7
+ data.tar.gz: 25c901d8e9f6d447ff2332c69e957a189baaa3960938ce1f3560ef11723ff209162c3c82b608dda7da2d0120469b8a59f8b9bf6577709f7e81f28f081544f852
@@ -1,3 +1,9 @@
1
+ ## 4.6.0 (2018-09-29)
2
+
3
+ * Ensure URLs are properly encoded in `NetHttp#download` and `#open` using Addressable (@linyaoli)
4
+
5
+ * Raise `ResponseError` with clear message when redirect URI was invalid in Down::NetHttp (@janko-m)
6
+
1
7
  ## 4.5.0 (2018-05-11)
2
8
 
3
9
  * Deprecate passing an `HTTP::Client` object to `Down::Http#initialize` (@janko-m)
data/README.md CHANGED
@@ -24,8 +24,8 @@ tempfile #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/2015092
24
24
 
25
25
  ### Metadata
26
26
 
27
- The returned Tempfile has `#content_type` and `#original_filename` attributes
28
- determined from the response headers:
27
+ The returned Tempfile has some additional attributes extracted from the
28
+ response data:
29
29
 
30
30
  ```rb
31
31
  tempfile.content_type #=> "text/plain"
@@ -345,9 +345,8 @@ Some features that give the HTTP.rb backend an advantage over `open-uri` +
345
345
 
346
346
  * Low memory usage (**10x less** than `open-uri`/`Net::HTTP`)
347
347
  * Correct URI parsing with [Addressable::URI]
348
- * Proper support for streaming downloads (`#download` and now reuse `#open`)
349
- * Proper support for SSL
350
- * Chaninable HTTP client builder API for setting default options
348
+ * Proper SSL support
349
+ * Chaninable builder API for setting default options
351
350
  * Support for persistent connections
352
351
 
353
352
  #### Additional options
@@ -359,11 +358,12 @@ Down::Http.download("http://example.org/image.jpg", headers: { "Foo" => "Bar" })
359
358
  Down::Http.open("http://example.org/image.jpg", follow: { max_hops: 0 })
360
359
  ```
361
360
 
362
- If you prefer to add options using the chainable API, you can pass a block:
361
+ However, it's recommended to configure request options using http.rb's
362
+ chainable API, as it's more convenient than passing raw options.
363
363
 
364
364
  ```rb
365
365
  Down::Http.open("http://example.org/image.jpg") do |client|
366
- client.timeout(connect: 3)
366
+ client.timeout(connect: 3, read: 3)
367
367
  end
368
368
  ```
369
369
 
@@ -15,6 +15,8 @@ Gem::Specification.new do |spec|
15
15
  spec.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "*.gemspec", "lib/**/*.rb"]
16
16
  spec.require_path = "lib"
17
17
 
18
+ spec.add_dependency "addressable", "~> 2.5"
19
+
18
20
  spec.add_development_dependency "minitest", "~> 5.8"
19
21
  spec.add_development_dependency "mocha", "~> 1.5"
20
22
  spec.add_development_dependency "rake"
@@ -14,6 +14,7 @@ module Down
14
14
  backend.open(*args, &block)
15
15
  end
16
16
 
17
+ # Allows setting a backend via a symbol or a downloader object.
17
18
  def backend(value = nil)
18
19
  if value.is_a?(Symbol)
19
20
  require "down/#{value}"
@@ -26,4 +27,5 @@ module Down
26
27
  end
27
28
  end
28
29
 
30
+ # Set Net::HTTP as the default backend
29
31
  Down.backend Down::NetHttp
@@ -19,14 +19,15 @@ module Down
19
19
 
20
20
  private
21
21
 
22
+ # If destination path is defined, move tempfile to the destination,
23
+ # otherwise return the tempfile unchanged.
22
24
  def download_result(tempfile, destination)
23
- if destination
24
- tempfile.close
25
- FileUtils.mv tempfile.path, destination
26
- nil
27
- else
28
- tempfile
29
- end
25
+ return tempfile unless destination
26
+
27
+ tempfile.close # required for Windows
28
+ FileUtils.mv tempfile.path, destination
29
+
30
+ nil
30
31
  end
31
32
  end
32
33
  end
@@ -7,11 +7,11 @@ require "http"
7
7
  require "down/backend"
8
8
 
9
9
  require "tempfile"
10
- require "cgi"
11
- require "base64"
12
10
 
13
11
  module Down
12
+ # Provides streaming downloads implemented with HTTP.rb.
14
13
  class Http < Backend
14
+ # Initializes the backend with common defaults.
15
15
  def initialize(options = {}, &block)
16
16
  if options.is_a?(HTTP::Client)
17
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."
@@ -28,6 +28,8 @@ module Down
28
28
  @client = block.call(@client) if block
29
29
  end
30
30
 
31
+ # Downlods the remote file to disk. Accepts HTTP.rb options via a hash or a
32
+ # block, and some additional options as well.
31
33
  def download(url, max_size: nil, progress_proc: nil, content_length_proc: nil, destination: nil, **options, &block)
32
34
  response = request(url, **options, &block)
33
35
 
@@ -63,6 +65,9 @@ module Down
63
65
  raise
64
66
  end
65
67
 
68
+ # Starts retrieving the remote file and returns an IO-like object which
69
+ # downloads the response body on-demand. Accepts HTTP.rb options via a hash
70
+ # or a block.
66
71
  def open(url, rewindable: true, **options, &block)
67
72
  response = request(url, **options, &block)
68
73
 
@@ -84,9 +89,10 @@ module Down
84
89
  end
85
90
 
86
91
  def send_request(method, url, **options, &block)
87
- url = process_url(url, options)
92
+ uri = HTTP::URI.parse(url)
88
93
 
89
94
  client = @client
95
+ client = client.basic_auth(user: uri.user, pass: uri.password) if uri.user || uri.password
90
96
  client = block.call(client) if block
91
97
 
92
98
  client.request(method, url, options)
@@ -94,6 +100,7 @@ module Down
94
100
  request_error!(exception)
95
101
  end
96
102
 
103
+ # Yields chunks of the response body to the block.
97
104
  def stream_body(response, &block)
98
105
  response.body.each(&block)
99
106
  rescue => exception
@@ -102,20 +109,7 @@ module Down
102
109
  response.connection.close unless @client.persistent?
103
110
  end
104
111
 
105
- def process_url(url, options)
106
- uri = HTTP::URI.parse(url)
107
-
108
- if uri.user || uri.password
109
- user, pass = uri.user, uri.password
110
- authorization = "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"
111
- options[:headers] ||= {}
112
- options[:headers].merge!("Authorization" => authorization)
113
- uri.user = uri.password = nil
114
- end
115
-
116
- uri.to_s
117
- end
118
-
112
+ # Raises non-sucessful response as a Down::ResponseError.
119
113
  def response_error!(response)
120
114
  args = [response.status.to_s, response: response]
121
115
 
@@ -126,6 +120,7 @@ module Down
126
120
  end
127
121
  end
128
122
 
123
+ # Re-raise HTTP.rb exceptions as Down::Error exceptions.
129
124
  def request_error!(exception)
130
125
  case exception
131
126
  when HTTP::Request::UnsupportedSchemeError, Addressable::URI::InvalidURIError
@@ -143,6 +138,7 @@ module Down
143
138
  end
144
139
  end
145
140
 
141
+ # Defines some additional attributes for the returned Tempfile.
146
142
  module DownloadedFile
147
143
  attr_accessor :url, :headers
148
144
 
@@ -2,15 +2,17 @@
2
2
 
3
3
  require "open-uri"
4
4
  require "net/https"
5
+ require "addressable/uri"
5
6
 
6
7
  require "down/backend"
7
8
 
8
9
  require "tempfile"
9
10
  require "fileutils"
10
- require "cgi"
11
11
 
12
12
  module Down
13
+ # Provides streaming downloads implemented with Net::HTTP and open-uri.
13
14
  class NetHttp < Backend
15
+ # Initializes the backend with common defaults.
14
16
  def initialize(options = {})
15
17
  @options = {
16
18
  "User-Agent" => "Down/#{Down::VERSION}",
@@ -20,6 +22,8 @@ module Down
20
22
  }.merge(options)
21
23
  end
22
24
 
25
+ # Downloads a remote file to disk using open-uri. Accepts any open-uri
26
+ # options, and a few more.
23
27
  def download(url, options = {})
24
28
  options = @options.merge(options)
25
29
 
@@ -29,6 +33,11 @@ module Down
29
33
  content_length_proc = options.delete(:content_length_proc)
30
34
  destination = options.delete(:destination)
31
35
 
36
+ # Use open-uri's :content_lenth_proc or :progress_proc to raise an
37
+ # exception early if the file is too large.
38
+ #
39
+ # Also disable following redirects, as we'll provide our own
40
+ # implementation that has the ability to limit the number of redirects.
32
41
  open_uri_options = {
33
42
  content_length_proc: proc { |size|
34
43
  if size && max_size && size > max_size
@@ -45,6 +54,7 @@ module Down
45
54
  redirect: false,
46
55
  }
47
56
 
57
+ # Handle basic authentication in the :proxy option.
48
58
  if options[:proxy]
49
59
  proxy = URI(options.delete(:proxy))
50
60
  user = proxy.user
@@ -62,8 +72,9 @@ module Down
62
72
 
63
73
  open_uri_options.merge!(options)
64
74
 
65
- uri = ensure_uri(url)
75
+ uri = ensure_uri(addressable_normalize(url))
66
76
 
77
+ # Handle basic authentication in the remote URL.
67
78
  if uri.user || uri.password
68
79
  open_uri_options[:http_basic_authentication] ||= [uri.user, uri.password]
69
80
  uri.user = nil
@@ -72,6 +83,7 @@ module Down
72
83
 
73
84
  open_uri_file = open_uri(uri, open_uri_options, follows_remaining: max_redirects)
74
85
 
86
+ # Handle the fact that open-uri returns StringIOs for small files.
75
87
  tempfile = ensure_tempfile(open_uri_file, File.extname(open_uri_file.base_uri.path))
76
88
  OpenURI::Meta.init tempfile, open_uri_file # add back open-uri methods
77
89
  tempfile.extend Down::NetHttp::DownloadedFile
@@ -79,11 +91,13 @@ module Down
79
91
  download_result(tempfile, destination)
80
92
  end
81
93
 
94
+ # Starts retrieving the remote file using Net::HTTP and returns an IO-like
95
+ # object which downloads the response body on-demand.
82
96
  def open(url, options = {})
97
+ uri = ensure_uri(addressable_normalize(url))
83
98
  options = @options.merge(options)
84
99
 
85
- uri = ensure_uri(url)
86
-
100
+ # Create a Fiber that halts when response headers are received.
87
101
  request = Fiber.new do
88
102
  net_http_request(uri, options) do |response|
89
103
  Fiber.yield response
@@ -94,6 +108,7 @@ module Down
94
108
 
95
109
  response_error!(response) unless response.is_a?(Net::HTTPSuccess)
96
110
 
111
+ # Build an IO-like object that will retrieve response body on-demand.
97
112
  Down::ChunkedIO.new(
98
113
  chunks: enum_for(:stream_body, response),
99
114
  size: response["Content-Length"] && response["Content-Length"].to_i,
@@ -113,13 +128,22 @@ module Down
113
128
 
114
129
  private
115
130
 
131
+ # Calls open-uri's URI::HTTP#open method. Additionally handles redirects.
116
132
  def open_uri(uri, options, follows_remaining: 0)
117
- downloaded_file = uri.open(options)
133
+ uri.open(options)
118
134
  rescue OpenURI::HTTPRedirect => exception
119
135
  raise Down::TooManyRedirects, "too many redirects" if follows_remaining == 0
120
136
 
121
- uri = exception.uri
137
+ # fail if redirect URI scheme is not http or https
138
+ begin
139
+ uri = ensure_uri(exception.uri)
140
+ rescue Down::InvalidUrl
141
+ response = rebuild_response_from_open_uri_exception(exception)
142
+
143
+ raise ResponseError.new("Invalid Redirect URI: #{exception.uri}", response: response)
144
+ end
122
145
 
146
+ # forward cookies on the redirect
123
147
  if !exception.io.meta["set-cookie"].to_s.empty?
124
148
  options["Cookie"] = exception.io.meta["set-cookie"]
125
149
  end
@@ -127,11 +151,11 @@ module Down
127
151
  follows_remaining -= 1
128
152
  retry
129
153
  rescue OpenURI::HTTPError => exception
130
- code, message = exception.io.status
131
- response_class = Net::HTTPResponse::CODE_TO_OBJ.fetch(code)
132
- response = response_class.new(nil, code, message)
133
- exception.io.metas.each do |name, values|
134
- values.each { |value| response.add_field(name, value) }
154
+ response = rebuild_response_from_open_uri_exception(exception)
155
+
156
+ # open-uri attempts to parse the redirect URI, so we re-raise that exception
157
+ if exception.message.include?("(Invalid Location URI)")
158
+ raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response: response)
135
159
  end
136
160
 
137
161
  response_error!(response)
@@ -159,6 +183,7 @@ module Down
159
183
  tempfile
160
184
  end
161
185
 
186
+ # Makes a Net::HTTP request and follows redirects.
162
187
  def net_http_request(uri, options, follows_remaining: options.fetch(:max_redirects, 2), &block)
163
188
  http, request = create_net_http(uri, options)
164
189
 
@@ -167,7 +192,10 @@ module Down
167
192
  http.request(request) do |response|
168
193
  unless response.is_a?(Net::HTTPRedirection)
169
194
  yield response
170
- response.instance_variable_set("@read", true) # mark response as read
195
+ # In certain cases the caller wants to download only one portion
196
+ # of the file and close the connection, so we tell Net::HTTP that
197
+ # it shouldn't continue retrieving it.
198
+ response.instance_variable_set("@read", true)
171
199
  end
172
200
  end
173
201
  end
@@ -178,13 +206,21 @@ module Down
178
206
  if response.is_a?(Net::HTTPRedirection)
179
207
  raise Down::TooManyRedirects if follows_remaining == 0
180
208
 
181
- location = URI.parse(response["Location"])
209
+ # fail if redirect URI is not a valid http or https URL
210
+ begin
211
+ location = ensure_uri(response["Location"], allow_relative: true)
212
+ rescue Down::InvalidUrl
213
+ raise ResponseError.new("Invalid Redirect URI: #{response["Location"]}", response: response)
214
+ end
215
+
216
+ # handle relative redirects
182
217
  location = uri + location if location.relative?
183
218
 
184
219
  net_http_request(location, options, follows_remaining: follows_remaining - 1, &block)
185
220
  end
186
221
  end
187
222
 
223
+ # Build a Net::HTTP object for making a request.
188
224
  def create_net_http(uri, options)
189
225
  http_class = Net::HTTP
190
226
 
@@ -195,7 +231,7 @@ module Down
195
231
 
196
232
  http = http_class.new(uri.host, uri.port)
197
233
 
198
- # taken from open-uri implementation
234
+ # Handle SSL parameters (taken from the open-uri implementation).
199
235
  if uri.is_a?(URI::HTTPS)
200
236
  http.use_ssl = true
201
237
  http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
@@ -214,7 +250,7 @@ module Down
214
250
  http.open_timeout = options[:open_timeout] if options.key?(:open_timeout)
215
251
 
216
252
  request_headers = options.select { |key, value| key.is_a?(String) }
217
- request_headers["Accept-Encoding"] = "" # otherwise FiberError can be raised
253
+ request_headers["Accept-Encoding"] = "" # Net::HTTP's inflater causes FiberErrors
218
254
 
219
255
  get = Net::HTTP::Get.new(uri.request_uri, request_headers)
220
256
  get.basic_auth(uri.user, uri.password) if uri.user || uri.password
@@ -222,20 +258,51 @@ module Down
222
258
  [http, get]
223
259
  end
224
260
 
261
+ # Yields chunks of the response body to the block.
225
262
  def stream_body(response, &block)
226
263
  response.read_body(&block)
227
264
  rescue => exception
228
265
  request_error!(exception)
229
266
  end
230
267
 
231
- def ensure_uri(url)
232
- uri = URI(url)
233
- raise Down::InvalidUrl, "URL scheme needs to be http or https" unless uri.is_a?(URI::HTTP)
268
+ # Checks that the url is a valid URI and that its scheme is http or https.
269
+ def ensure_uri(url, allow_relative: false)
270
+ begin
271
+ uri = URI(url)
272
+ rescue URI::InvalidURIError => exception
273
+ raise Down::InvalidUrl, exception.message
274
+ end
275
+
276
+ unless allow_relative && uri.relative?
277
+ raise Down::InvalidUrl, "URL scheme needs to be http or https: #{uri}" unless uri.is_a?(URI::HTTP)
278
+ end
279
+
234
280
  uri
235
- rescue URI::InvalidURIError => exception
236
- raise Down::InvalidUrl, exception.message
237
281
  end
238
282
 
283
+ # Makes sure that the URL is properly encoded.
284
+ def addressable_normalize(url)
285
+ addressable_uri = Addressable::URI.parse(url)
286
+ addressable_uri.normalize.to_s
287
+ end
288
+
289
+ # When open-uri raises an exception, it doesn't expose the response object.
290
+ # Fortunately, the exception object holds response data that can be used to
291
+ # rebuild the Net::HTTP response object.
292
+ def rebuild_response_from_open_uri_exception(exception)
293
+ code, message = exception.io.status
294
+
295
+ response_class = Net::HTTPResponse::CODE_TO_OBJ.fetch(code)
296
+ response = response_class.new(nil, code, message)
297
+
298
+ exception.io.metas.each do |name, values|
299
+ values.each { |value| response.add_field(name, value) }
300
+ end
301
+
302
+ response
303
+ end
304
+
305
+ # Raises non-sucessful response as a Down::ResponseError.
239
306
  def response_error!(response)
240
307
  code = response.code.to_i
241
308
  message = response.message.split(" ").map(&:capitalize).join(" ")
@@ -249,6 +316,7 @@ module Down
249
316
  end
250
317
  end
251
318
 
319
+ # Re-raise Net::HTTP exceptions as Down::Error exceptions.
252
320
  def request_error!(exception)
253
321
  case exception
254
322
  when Net::OpenTimeout
@@ -264,6 +332,8 @@ module Down
264
332
  end
265
333
  end
266
334
 
335
+ # Defines some additional attributes for the returned Tempfile (on top of what
336
+ # OpenURI::Meta already defines).
267
337
  module DownloadedFile
268
338
  def original_filename
269
339
  Utils.filename_from_content_disposition(meta["content-disposition"]) ||
@@ -4,6 +4,7 @@ module Down
4
4
  module Utils
5
5
  module_function
6
6
 
7
+ # Retrieves potential filename from the "Content-Disposition" header.
7
8
  def filename_from_content_disposition(content_disposition)
8
9
  content_disposition = content_disposition.to_s
9
10
 
@@ -13,6 +14,7 @@ module Down
13
14
  filename unless filename.empty?
14
15
  end
15
16
 
17
+ # Retrieves potential filename from the URL path.
16
18
  def filename_from_path(path)
17
19
  filename = path.split("/").last
18
20
  CGI.unescape(filename) if filename
@@ -1,5 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Down
4
- VERSION = "4.5.0"
4
+ VERSION = "4.6.0"
5
5
  end
@@ -11,10 +11,12 @@ require "down/backend"
11
11
 
12
12
  require "tempfile"
13
13
  require "uri"
14
- require "cgi"
15
14
 
16
15
  module Down
16
+ # Provides streaming downloads implemented with the wget command-line tool.
17
+ # The design is very similar to Down::Http.
17
18
  class Wget < Backend
19
+ # Initializes the backend with common defaults.
18
20
  def initialize(*arguments)
19
21
  @arguments = [
20
22
  user_agent: "Down/#{Down::VERSION}",
@@ -25,6 +27,8 @@ module Down
25
27
  ] + arguments
26
28
  end
27
29
 
30
+ # Downlods the remote file to disk. Accepts wget command-line options and
31
+ # some additional options as well.
28
32
  def download(url, *args, max_size: nil, content_length_proc: nil, progress_proc: nil, destination: nil, **options)
29
33
  io = open(url, *args, **options, rewindable: false)
30
34
 
@@ -63,10 +67,13 @@ module Down
63
67
  io.close if io
64
68
  end
65
69
 
70
+ # Starts retrieving the remote file and returns an IO-like object which
71
+ # downloads the response body on-demand. Accepts wget command-line options.
66
72
  def open(url, *args, rewindable: true, **options)
67
73
  arguments = generate_command(url, *args, **options)
68
74
 
69
75
  command = Down::Wget::Command.execute(arguments)
76
+ # Wrap the wget command output in an IO-like object.
70
77
  output = Down::ChunkedIO.new(
71
78
  chunks: command.enum_for(:output),
72
79
  on_close: command.method(:terminate),
@@ -78,6 +85,7 @@ module Down
78
85
  header_string << output.readpartial until header_string.include?("\r\n\r\n")
79
86
  header_string, first_chunk = header_string.split("\r\n\r\n", 2)
80
87
 
88
+ # Use an HTTP parser to parse out the response headers.
81
89
  parser = HTTP::Parser.new
82
90
  parser << header_string
83
91
 
@@ -92,6 +100,7 @@ module Down
92
100
  content_length = headers["Content-Length"].to_i if headers["Content-Length"]
93
101
  charset = headers["Content-Type"][/;\s*charset=([^;]+)/i, 1] if headers["Content-Type"]
94
102
 
103
+ # Create an Enumerator which will lazily retrieve chunks of response body.
95
104
  chunks = Enumerator.new do |yielder|
96
105
  yielder << first_chunk if first_chunk
97
106
  yielder << output.readpartial until output.eof?
@@ -109,6 +118,7 @@ module Down
109
118
 
110
119
  private
111
120
 
121
+ # Generates the wget command.
112
122
  def generate_command(url, *args, **options)
113
123
  command = %W[wget --no-verbose --save-headers -O -]
114
124
 
@@ -131,10 +141,12 @@ module Down
131
141
  command
132
142
  end
133
143
 
144
+ # Handles executing the wget command.
134
145
  class Command
135
146
  PIPE_BUFFER_SIZE = 64*1024
136
147
 
137
148
  def self.execute(arguments)
149
+ # posix-spawn gem has better performance, so we use it if it's available
138
150
  if defined?(POSIX::Spawn)
139
151
  pid, stdin_pipe, stdout_pipe, stderr_pipe = POSIX::Spawn.popen4(*arguments)
140
152
  status_reaper = Process.detach(pid)
@@ -156,6 +168,7 @@ module Down
156
168
  @stderr_pipe = stderr_pipe
157
169
  end
158
170
 
171
+ # Yields chunks of stdout. At the end handles the exit status.
159
172
  def output
160
173
  # Keep emptying the stderr buffer, to allow the subprocess to send more
161
174
  # than 64KB if it wants to.
@@ -165,8 +178,32 @@ module Down
165
178
 
166
179
  status = @status_reaper.value
167
180
  stderr = stderr_reader.value
181
+
168
182
  close
169
183
 
184
+ handle_status(status, stderr)
185
+ end
186
+
187
+ def terminate
188
+ begin
189
+ Process.kill("TERM", @status_reaper[:pid])
190
+ Process.waitpid(@status_reaper[:pid])
191
+ rescue Errno::ESRCH
192
+ # process has already terminated
193
+ end
194
+
195
+ close
196
+ end
197
+
198
+ def close
199
+ @stdout_pipe.close unless @stdout_pipe.closed?
200
+ @stderr_pipe.close unless @stderr_pipe.closed?
201
+ end
202
+
203
+ private
204
+
205
+ # Translates nonzero wget exit statuses into exceptions.
206
+ def handle_status(status, stderr)
170
207
  case status.exitstatus
171
208
  when 0 # No problems occurred
172
209
  # success
@@ -188,23 +225,9 @@ module Down
188
225
  raise Down::ResponseError, stderr
189
226
  end
190
227
  end
191
-
192
- def terminate
193
- begin
194
- Process.kill("TERM", @status_reaper[:pid])
195
- rescue Errno::ESRCH
196
- # process has already terminated
197
- end
198
-
199
- close
200
- end
201
-
202
- def close
203
- @stdout_pipe.close unless @stdout_pipe.closed?
204
- @stderr_pipe.close unless @stderr_pipe.closed?
205
- end
206
228
  end
207
229
 
230
+ # Adds additional attributes to the Tempfile returned in #download.
208
231
  module DownloadedFile
209
232
  attr_accessor :url, :headers
210
233
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: down
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.0
4
+ version: 4.6.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: 2018-05-11 00:00:00.000000000 Z
11
+ date: 2018-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: minitest
15
29
  requirement: !ruby/object:Gem::Requirement