patron 1.0.0 → 1.1.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 +43 -0
- data/README.md +99 -1
- data/ext/patron/session_ext.c +697 -71
- data/lib/patron/request.rb +26 -5
- data/lib/patron/response.rb +2 -54
- data/lib/patron/response_decoding.rb +1 -18
- data/lib/patron/response_head.rb +109 -0
- data/lib/patron/response_stream.rb +217 -0
- data/lib/patron/session.rb +150 -19
- data/lib/patron/sse_client.rb +327 -0
- data/lib/patron/transfer_stats.rb +62 -0
- data/lib/patron/version.rb +1 -1
- data/lib/patron.rb +1 -0
- metadata +6 -2
data/lib/patron/request.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Patron
|
|
|
15
15
|
ssl_version http_version ssl_cert ssl_cert_type ssl_key ssl_key_password
|
|
16
16
|
ignore_content_length buffer_size download_byte_limit progress_callback
|
|
17
17
|
automatic_content_encoding force_ipv4 low_speed_time low_speed_limit
|
|
18
|
-
upload_data file_name multipart
|
|
18
|
+
upload_data file_name multipart read_timeout
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
# Any HTTP token works as a verb; the ones libcurl does not know natively are sent as custom requests
|
|
@@ -27,7 +27,8 @@ module Patron
|
|
|
27
27
|
:automatic_content_encoding, :force_ipv4, :download_byte_limit, :low_speed_time,
|
|
28
28
|
:low_speed_limit, :progress_callback, :ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_password
|
|
29
29
|
|
|
30
|
-
attr_reader :action, :headers, :timeout, :connect_timeout, :max_redirects, :auth_type, :buffer_size, :upload_data
|
|
30
|
+
attr_reader :action, :headers, :timeout, :connect_timeout, :max_redirects, :auth_type, :buffer_size, :upload_data,
|
|
31
|
+
:read_timeout
|
|
31
32
|
|
|
32
33
|
# Defaults to a GET with no timeouts and unlimited redirects. Attributes may be given as
|
|
33
34
|
# keywords; `action` and `multipart` are applied first since `upload_data=` depends on them.
|
|
@@ -63,10 +64,18 @@ module Patron
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
# Sets the request body. A Hash is form-encoded, unless the request is multipart, in which
|
|
66
|
-
# case libcurl composes the body from it.
|
|
67
|
-
# from the file it names
|
|
67
|
+
# case libcurl composes the body from it. An object that responds to `to_path` (a File, a
|
|
68
|
+
# Tempfile) is uploaded from the file it names. An object that responds to `readpartial` or
|
|
69
|
+
# `read` (a StringIO, an `IO.pipe` reader, a `Zlib::GzipReader`) is streamed: libcurl calls
|
|
70
|
+
# `readpartial(length, buffer)` until it raises EOFError, or, when the object has no
|
|
71
|
+
# `readpartial`, `read(length, buffer)` until it returns nil. `readpartial` sends whatever a
|
|
72
|
+
# pipe or a socket has so far, where `read` waits for the full length. The body length comes
|
|
73
|
+
# from `size` when it has one, then from a `Content-Length` header, and otherwise the body is
|
|
74
|
+
# sent with chunked transfer-encoding. When the body has to be sent again (a 307/308 redirect,
|
|
75
|
+
# say) the object is rewound with `seek` or `rewind`; one that cannot be rewound fails the
|
|
76
|
+
# request with a Patron::Error. Anything else is sent as its `to_s`.
|
|
68
77
|
#
|
|
69
|
-
# @param data [Hash, #
|
|
78
|
+
# @param data [Hash, #to_path, #read, #to_s, nil]
|
|
70
79
|
def upload_data=(data)
|
|
71
80
|
@upload_data = if data.is_a?(Hash) && !multipart
|
|
72
81
|
Util.build_query_string_from_hash(data, action == :post)
|
|
@@ -95,6 +104,18 @@ module Patron
|
|
|
95
104
|
@connect_timeout = validate_timeout(seconds)
|
|
96
105
|
end
|
|
97
106
|
|
|
107
|
+
# Sets how long to wait for the next piece of the response, counted from the previous one
|
|
108
|
+
# (or from the start of the request), as opposed to #timeout which bounds the whole transfer.
|
|
109
|
+
# Exceeding it raises Patron::TimeoutError.
|
|
110
|
+
#
|
|
111
|
+
# @param seconds [#to_f, nil] fractional seconds allowed, nil for no limit
|
|
112
|
+
# @raise [ArgumentError] when zero or negative
|
|
113
|
+
def read_timeout=(seconds)
|
|
114
|
+
return @read_timeout = nil if seconds.nil?
|
|
115
|
+
raise ArgumentError, "Read timeout must be nil or a positive number of seconds" unless seconds.respond_to?(:to_f) && seconds.to_f > 0
|
|
116
|
+
@read_timeout = seconds.to_f
|
|
117
|
+
end
|
|
118
|
+
|
|
98
119
|
# @param count [Integer] -1 for unlimited, 0 to not follow redirects
|
|
99
120
|
def max_redirects=(count)
|
|
100
121
|
raise ArgumentError, "Max redirects must be a positive integer, 0 or -1" if count.to_i < -1
|
data/lib/patron/response.rb
CHANGED
|
@@ -4,32 +4,13 @@ module Patron
|
|
|
4
4
|
|
|
5
5
|
# Represents the response from the HTTP server.
|
|
6
6
|
class Response
|
|
7
|
+
include ResponseHead
|
|
7
8
|
include ResponseDecoding
|
|
8
9
|
|
|
9
|
-
# @return [String] the original URL used to perform the request (contains the final URL after redirects)
|
|
10
|
-
attr_reader :url
|
|
11
|
-
|
|
12
|
-
# @return [Integer] the HTTP status code of the final response after all the redirects
|
|
13
|
-
attr_reader :status
|
|
14
|
-
|
|
15
|
-
# @return [String] the complete status line (code and message)
|
|
16
|
-
attr_reader :status_line
|
|
17
|
-
|
|
18
|
-
# @return [Integer] how many redirects were followed when fulfilling this request
|
|
19
|
-
attr_reader :redirect_count
|
|
20
|
-
|
|
21
10
|
# @return [String, nil] the response body as a String encoded as `Encoding::BINARY` or
|
|
22
11
|
# or `nil` if the response was written directly to a file
|
|
23
12
|
attr_reader :body
|
|
24
13
|
|
|
25
|
-
# @return [Hash] the response headers. If there were multiple headers received for the same value
|
|
26
|
-
# (like "Cookie"), the header values will be within an Array under the key for the header, in order.
|
|
27
|
-
attr_reader :headers
|
|
28
|
-
|
|
29
|
-
# @return [String] the recognized name of the charset for the response. The name is not checked
|
|
30
|
-
# to be a valid charset name, just stored. To check the charset for validity, use #body_decodable?
|
|
31
|
-
attr_reader :charset
|
|
32
|
-
|
|
33
14
|
# @return [Hash{Symbol => Float}, nil] curl's cumulative transfer marks in seconds since the request
|
|
34
15
|
# started: `:namelookup`, `:connect`, `:appconnect`, `:pretransfer`, `:starttransfer`, `:redirect`
|
|
35
16
|
# and `:total`. `nil` when the response was not built from a curl transfer (e.g. a stub).
|
|
@@ -54,20 +35,9 @@ module Patron
|
|
|
54
35
|
@body = body && (body.encoding == Encoding::BINARY ? body : body.b)
|
|
55
36
|
@timings = timings
|
|
56
37
|
|
|
57
|
-
|
|
58
|
-
@charset = charset_from_content_type || default_charset
|
|
38
|
+
parse_head(header_data, default_charset)
|
|
59
39
|
end
|
|
60
40
|
|
|
61
|
-
# Tells whether the HTTP response code is less than 400
|
|
62
|
-
#
|
|
63
|
-
# @return [Boolean]
|
|
64
|
-
def ok? = !error?
|
|
65
|
-
|
|
66
|
-
# Tells whether the HTTP response code is larger than 399
|
|
67
|
-
#
|
|
68
|
-
# @return [Boolean]
|
|
69
|
-
def error? = status >= 400
|
|
70
|
-
|
|
71
41
|
# Tells whether the response body can be decoded losslessly into the curren internal encoding
|
|
72
42
|
#
|
|
73
43
|
# @return [Boolean] true if the body is decodable, false if otherwise
|
|
@@ -120,27 +90,5 @@ module Patron
|
|
|
120
90
|
return decode_body(false) if frozen?
|
|
121
91
|
@inspectable_body ||= decode_body(false)
|
|
122
92
|
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
def parse_headers(header_data_for_multiple_responses)
|
|
127
|
-
@headers = {}
|
|
128
|
-
|
|
129
|
-
responses = Patron::HeaderParser.parse(header_data_for_multiple_responses)
|
|
130
|
-
last_response = responses[-1] # proxies and redirects put their own responses in front of the real one
|
|
131
|
-
|
|
132
|
-
@status_line = last_response.status_line
|
|
133
|
-
last_response.headers.each do |line|
|
|
134
|
-
hdr, val = line.split(":", 2)
|
|
135
|
-
val = val&.strip
|
|
136
|
-
|
|
137
|
-
if @headers.key?(hdr)
|
|
138
|
-
@headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of?(Array)
|
|
139
|
-
@headers[hdr] << val
|
|
140
|
-
else
|
|
141
|
-
@headers[hdr] = val
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
93
|
end
|
|
146
94
|
end
|
|
@@ -7,8 +7,6 @@ module Patron
|
|
|
7
7
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
-
CHARSET_CONTENT_TYPE_RE = /(?:charset|encoding)="?([a-z0-9-]+)"?/i
|
|
11
|
-
|
|
12
10
|
MISREPORTED_ENCODING_ERROR = <<~EOF
|
|
13
11
|
The server stated that the response has the charset matching %{declared}, but the actual
|
|
14
12
|
response body failed to decode as such (not flagged as `valid_encoding?')
|
|
@@ -61,16 +59,12 @@ module Patron
|
|
|
61
59
|
INTERNAL_CHARSET_MISMATCH_ERROR % { source_encoding: enc, target_encoding: internal }
|
|
62
60
|
end
|
|
63
61
|
|
|
64
|
-
def charset_from_content_type
|
|
65
|
-
@headers["Content-Type"].to_s[CHARSET_CONTENT_TYPE_RE, 1]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
62
|
def encoding_from_headers_or_binary
|
|
69
63
|
return Encoding::BINARY unless (charset_name = charset_from_content_type)
|
|
70
64
|
Encoding.find(charset_name)
|
|
71
65
|
rescue ArgumentError # invalid charset name
|
|
72
66
|
raise HeaderCharsetInvalid,
|
|
73
|
-
INVALID_CHARSET_NAME_ERROR % { content_type:
|
|
67
|
+
INVALID_CHARSET_NAME_ERROR % { content_type: content_type_header.inspect }
|
|
74
68
|
end
|
|
75
69
|
|
|
76
70
|
def internal_encoding
|
|
@@ -78,17 +72,6 @@ module Patron
|
|
|
78
72
|
# so the String is the better oracle for "what does this process speak"
|
|
79
73
|
Encoding.default_internal || ''.encoding
|
|
80
74
|
end
|
|
81
|
-
|
|
82
|
-
def decode_header_data(str)
|
|
83
|
-
# Headers are ISO-8859-1 by the book, but Content-Disposition routinely arrives as raw UTF-8
|
|
84
|
-
# and every browser but localized IE treats it as such, so 8859-1 -> UTF-8 -> binary it is
|
|
85
|
-
[Encoding::ISO8859_1, Encoding::UTF_8, Encoding::BINARY].each do |possible_enc|
|
|
86
|
-
return str.encode(possible_enc)
|
|
87
|
-
rescue ::Encoding::UndefinedConversionError
|
|
88
|
-
next
|
|
89
|
-
end
|
|
90
|
-
str
|
|
91
|
-
end
|
|
92
75
|
end
|
|
93
76
|
|
|
94
77
|
private_constant :ResponseDecoding
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Patron
|
|
4
|
+
|
|
5
|
+
# The facts known once the head of a response has arrived: status, headers, the effective
|
|
6
|
+
# URL. Shared by Response, ResponseStream and TransferStats, so that a header value has
|
|
7
|
+
# one shape wherever it is looked at.
|
|
8
|
+
module ResponseHead
|
|
9
|
+
CHARSET_CONTENT_TYPE_RE = /(?:charset|encoding)="?([a-z0-9-]+)"?/i
|
|
10
|
+
|
|
11
|
+
# @return [String] the original URL used to perform the request (contains the final URL after redirects)
|
|
12
|
+
attr_reader :url
|
|
13
|
+
|
|
14
|
+
# @return [Integer] the HTTP status code of the final response after all the redirects
|
|
15
|
+
attr_reader :status
|
|
16
|
+
|
|
17
|
+
# @return [String] the complete status line (code and message)
|
|
18
|
+
attr_reader :status_line
|
|
19
|
+
|
|
20
|
+
# @return [Integer] how many redirects were followed when fulfilling this request
|
|
21
|
+
attr_reader :redirect_count
|
|
22
|
+
|
|
23
|
+
# @return [Hash] the response headers. If there were multiple headers received for the same value
|
|
24
|
+
# (like "Cookie"), the header values will be within an Array under the key for the header, in order.
|
|
25
|
+
attr_reader :headers
|
|
26
|
+
|
|
27
|
+
# @return [String] the recognized name of the charset for the response. The name is not checked
|
|
28
|
+
# to be a valid charset name, just stored.
|
|
29
|
+
attr_reader :charset
|
|
30
|
+
|
|
31
|
+
# Tells whether the HTTP response code is less than 400
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def ok? = !error?
|
|
35
|
+
|
|
36
|
+
# Tells whether the HTTP response code is larger than 399
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def error? = status >= 400
|
|
40
|
+
|
|
41
|
+
# @return [Integer, nil] the Content-Length header as an Integer, nil when absent or not a number
|
|
42
|
+
def content_length
|
|
43
|
+
value = header_value("Content-Length")
|
|
44
|
+
value = value.first if value.is_a?(Array)
|
|
45
|
+
value.to_s.match?(/\A\d+\z/) ? Integer(value, 10) : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [String, nil] the media type from the Content-Type header, without its parameters
|
|
49
|
+
# and in lowercase, e.g. "text/event-stream"
|
|
50
|
+
def content_type
|
|
51
|
+
value = content_type_header
|
|
52
|
+
value = value.first if value.is_a?(Array)
|
|
53
|
+
media_type = value.to_s.split(";", 2).first.to_s.strip.downcase
|
|
54
|
+
media_type.empty? ? nil : media_type
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Builds a header Hash from "Name: value" lines, with repeated names collected into an Array
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param lines [Array<String>]
|
|
61
|
+
# @return [Hash]
|
|
62
|
+
def self.headers_from_lines(lines)
|
|
63
|
+
lines.each_with_object({}) do |line, headers|
|
|
64
|
+
hdr, val = line.split(":", 2)
|
|
65
|
+
val = val&.strip
|
|
66
|
+
|
|
67
|
+
if headers.key?(hdr)
|
|
68
|
+
headers[hdr] = [headers[hdr]] unless headers[hdr].kind_of?(Array)
|
|
69
|
+
headers[hdr] << val
|
|
70
|
+
else
|
|
71
|
+
headers[hdr] = val
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def parse_head(header_data, default_charset)
|
|
79
|
+
responses = Patron::HeaderParser.parse(decode_header_data(header_data))
|
|
80
|
+
last_response = responses[-1] # proxies and redirects put their own responses in front of the real one
|
|
81
|
+
|
|
82
|
+
@status_line = last_response&.status_line
|
|
83
|
+
@headers = ResponseHead.headers_from_lines(last_response ? last_response.headers : [])
|
|
84
|
+
@charset = charset_from_content_type || default_charset
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def charset_from_content_type
|
|
88
|
+
content_type_header.to_s[CHARSET_CONTENT_TYPE_RE, 1]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# HTTP/2 servers and Puma 8+ send header names lowercase, and `@headers` keeps them as received
|
|
92
|
+
def content_type_header = header_value("Content-Type")
|
|
93
|
+
|
|
94
|
+
def header_value(name)
|
|
95
|
+
@headers.find { |hdr, _| hdr.casecmp?(name) }&.last
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def decode_header_data(str)
|
|
99
|
+
# Headers are ISO-8859-1 by the book, but Content-Disposition routinely arrives as raw UTF-8
|
|
100
|
+
# and every browser but localized IE treats it as such, so 8859-1 -> UTF-8 -> binary it is
|
|
101
|
+
[Encoding::ISO8859_1, Encoding::UTF_8, Encoding::BINARY].each do |possible_enc|
|
|
102
|
+
return str.encode(possible_enc)
|
|
103
|
+
rescue ::Encoding::UndefinedConversionError
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
str
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Patron
|
|
4
|
+
|
|
5
|
+
# The response of Session#stream while its block runs: the complete head, and the verbs that
|
|
6
|
+
# receive the body. Every read drives the transfer itself, in the calling thread or fiber, until
|
|
7
|
+
# some bytes have arrived, so waiting for the network yields to a Fiber scheduler like any other
|
|
8
|
+
# IO, and an exception raised between two reads is plain Ruby.
|
|
9
|
+
#
|
|
10
|
+
# The reading methods follow IO: #read and #readpartial fill a String the caller owns, so that
|
|
11
|
+
# `IO.copy_stream(stream, destination)` runs without allocating per chunk. Once the block has
|
|
12
|
+
# ended, every reading method raises IOError.
|
|
13
|
+
#
|
|
14
|
+
# Under a Fiber scheduler use #each and #each_line with a block: an external Enumerator
|
|
15
|
+
# (`each.next`) runs the reads on a different fiber.
|
|
16
|
+
class ResponseStream
|
|
17
|
+
include ResponseHead
|
|
18
|
+
|
|
19
|
+
CHUNK_SIZE = 64 * 1024
|
|
20
|
+
private_constant :CHUNK_SIZE
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
def initialize(session, status:, url:, redirect_count:, header_data:, default_charset: nil)
|
|
24
|
+
@session = session
|
|
25
|
+
@status = status
|
|
26
|
+
@url = url
|
|
27
|
+
@redirect_count = redirect_count
|
|
28
|
+
@header_data = header_data
|
|
29
|
+
@default_charset = default_charset
|
|
30
|
+
@closed = false
|
|
31
|
+
@eof = false
|
|
32
|
+
@peeked = nil
|
|
33
|
+
parse_head(header_data, default_charset)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inspect
|
|
37
|
+
"#<Patron::ResponseStream @status_line='#{@status_line}'#{' (closed)' if @closed}>"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Reads like IO#read. With a length, receives until that many bytes have arrived or the body
|
|
41
|
+
# ends. Without one, receives the rest of the body.
|
|
42
|
+
#
|
|
43
|
+
# @param length [Integer, nil] how many bytes to read, nil for the rest of the body
|
|
44
|
+
# @param outbuf [String, nil] a String to fill in place instead of allocating one
|
|
45
|
+
# @return [String, nil] binary String; with a length, nil at the end of the body; without one, "" there
|
|
46
|
+
# @raise [IOError] when the stream is closed
|
|
47
|
+
def read(length = nil, outbuf = nil)
|
|
48
|
+
ensure_open!
|
|
49
|
+
if length.nil?
|
|
50
|
+
buf = outbuf || String.new(encoding: Encoding::BINARY)
|
|
51
|
+
kept = 0
|
|
52
|
+
while (n = receive(nil, buf, kept))
|
|
53
|
+
kept += n
|
|
54
|
+
end
|
|
55
|
+
return buf
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "negative length #{length} given" if length.negative?
|
|
59
|
+
buf = outbuf || String.new(capacity: length, encoding: Encoding::BINARY)
|
|
60
|
+
if length.zero?
|
|
61
|
+
buf.clear
|
|
62
|
+
return buf
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
kept = 0
|
|
66
|
+
while kept < length && (n = receive(length - kept, buf, kept))
|
|
67
|
+
kept += n
|
|
68
|
+
end
|
|
69
|
+
kept.zero? ? nil : buf
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reads like IO#readpartial: returns as soon as some bytes have arrived, at most `maxlen`.
|
|
73
|
+
#
|
|
74
|
+
# @param maxlen [Integer] most bytes to return
|
|
75
|
+
# @param outbuf [String, nil] a String to fill in place instead of allocating one
|
|
76
|
+
# @return [String] binary String
|
|
77
|
+
# @raise [EOFError] at the end of the body
|
|
78
|
+
# @raise [IOError] when the stream is closed
|
|
79
|
+
def readpartial(maxlen, outbuf = nil)
|
|
80
|
+
ensure_open!
|
|
81
|
+
raise ArgumentError, "negative length #{maxlen} given" if maxlen.negative?
|
|
82
|
+
buf = outbuf || String.new(capacity: maxlen, encoding: Encoding::BINARY)
|
|
83
|
+
raise EOFError, "end of file reached" unless receive(maxlen, buf, 0)
|
|
84
|
+
buf
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Yields the body in chunks of whatever size libcurl delivers them, each a new binary String,
|
|
88
|
+
# so collecting them is safe.
|
|
89
|
+
#
|
|
90
|
+
# @yieldparam chunk [String]
|
|
91
|
+
# @return [self, Enumerator] self, or an Enumerator without a block
|
|
92
|
+
# @raise [IOError] when the stream is closed
|
|
93
|
+
def each
|
|
94
|
+
return enum_for(:each) unless block_given?
|
|
95
|
+
ensure_open!
|
|
96
|
+
loop do
|
|
97
|
+
chunk = String.new(encoding: Encoding::BINARY)
|
|
98
|
+
break unless receive(CHUNK_SIZE, chunk, 0)
|
|
99
|
+
yield chunk
|
|
100
|
+
end
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Yields the body line by line, each line with its "\n" and binary. A last line without
|
|
105
|
+
# one is yielded when the body ends.
|
|
106
|
+
#
|
|
107
|
+
# @yieldparam line [String]
|
|
108
|
+
# @return [self, Enumerator] self, or an Enumerator without a block
|
|
109
|
+
# @raise [IOError] when the stream is closed
|
|
110
|
+
def each_line
|
|
111
|
+
return enum_for(:each_line) unless block_given?
|
|
112
|
+
carry = String.new(encoding: Encoding::BINARY)
|
|
113
|
+
each do |chunk|
|
|
114
|
+
carry << chunk
|
|
115
|
+
start = 0
|
|
116
|
+
while (newline = carry.byteindex("\n", start))
|
|
117
|
+
yield carry.byteslice(start, newline + 1 - start)
|
|
118
|
+
start = newline + 1
|
|
119
|
+
end
|
|
120
|
+
carry = carry.byteslice(start, carry.bytesize - start) if start > 0
|
|
121
|
+
end
|
|
122
|
+
yield carry unless carry.empty?
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Receives the rest of the body and throws it away, so that the connection can be reused.
|
|
127
|
+
# Bounded by the same read_timeout, download_byte_limit and interrupts as any read.
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer] how many bytes were discarded, 0 at the end of the body
|
|
130
|
+
# @raise [IOError] when the stream is closed
|
|
131
|
+
def drain!
|
|
132
|
+
ensure_open!
|
|
133
|
+
scratch = String.new(capacity: CHUNK_SIZE, encoding: Encoding::BINARY)
|
|
134
|
+
discarded = 0
|
|
135
|
+
while (n = receive(CHUNK_SIZE, scratch, 0))
|
|
136
|
+
discarded += n
|
|
137
|
+
end
|
|
138
|
+
discarded
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Tells whether the body has ended. Like IO#eof?, this waits for the next bytes when none
|
|
142
|
+
# have arrived yet.
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
# @raise [IOError] when the stream is closed
|
|
146
|
+
def eof?
|
|
147
|
+
ensure_open!
|
|
148
|
+
return true if @eof
|
|
149
|
+
return false if @peeked
|
|
150
|
+
|
|
151
|
+
peeked = String.new(encoding: Encoding::BINARY)
|
|
152
|
+
@peeked = peeked if receive(CHUNK_SIZE, peeked, 0)
|
|
153
|
+
@eof
|
|
154
|
+
end
|
|
155
|
+
alias eof eof?
|
|
156
|
+
|
|
157
|
+
# Ends the transfer. When the body has not been received to its end, its connection is
|
|
158
|
+
# closed rather than reused. Called by Session#stream when the block ends; idempotent.
|
|
159
|
+
#
|
|
160
|
+
# @return [nil]
|
|
161
|
+
def close
|
|
162
|
+
finish
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [Boolean] whether the stream has been closed
|
|
167
|
+
def closed? = @closed
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
attr_reader :header_data
|
|
172
|
+
|
|
173
|
+
def ensure_open!
|
|
174
|
+
raise IOError, "closed stream" if @closed
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Places up to `maxlen` bytes in `buf` after its first `kept` bytes, returns how many or nil at the end
|
|
178
|
+
def receive(maxlen, buf, kept)
|
|
179
|
+
return receive_peeked(maxlen, buf, kept) if @peeked
|
|
180
|
+
|
|
181
|
+
placed = @session.__send__(:transfer_read, maxlen, buf, kept)
|
|
182
|
+
@eof = true if placed.nil?
|
|
183
|
+
placed
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The bytes #eof? had to read ahead
|
|
187
|
+
def receive_peeked(maxlen, buf, kept)
|
|
188
|
+
piece = @peeked
|
|
189
|
+
if maxlen && piece.bytesize > maxlen
|
|
190
|
+
@peeked = piece.byteslice(maxlen, piece.bytesize - maxlen)
|
|
191
|
+
piece = piece.byteslice(0, maxlen)
|
|
192
|
+
else
|
|
193
|
+
@peeked = nil
|
|
194
|
+
end
|
|
195
|
+
buf.force_encoding(Encoding::BINARY)
|
|
196
|
+
buf.replace(buf.byteslice(0, kept)) if buf.bytesize > kept
|
|
197
|
+
buf << piece
|
|
198
|
+
piece.bytesize
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def finish
|
|
202
|
+
return if @closed
|
|
203
|
+
@closed = true
|
|
204
|
+
@tail = @session.__send__(:transfer_finish)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def receipt
|
|
208
|
+
downloaded_bytes, uploaded_bytes, timings, complete, trailer_data = @tail
|
|
209
|
+
TransferStats.new(
|
|
210
|
+
url: @url, status: @status, redirect_count: @redirect_count, header_data: @header_data,
|
|
211
|
+
default_charset: @default_charset, downloaded_bytes: downloaded_bytes.to_i,
|
|
212
|
+
uploaded_bytes: uploaded_bytes.to_i, timings: timings || {}, complete: !!complete,
|
|
213
|
+
trailer_data: trailer_data || ""
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|