interval_response 0.1.6 → 0.1.7
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 +10 -0
- data/lib/interval_response.rb +1 -0
- data/lib/interval_response/abstract.rb +24 -62
- data/lib/interval_response/full.rb +14 -3
- data/lib/interval_response/multi.rb +8 -0
- data/lib/interval_response/rack_body_wrapper.rb +65 -0
- data/lib/interval_response/sequence.rb +33 -7
- data/lib/interval_response/single.rb +4 -0
- data/lib/interval_response/version.rb +1 -1
- data/spec/interval_response/sequence_spec.rb +22 -6
- data/spec/interval_response_spec.rb +35 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05cde032dcb1552e0814acf6904f300a840a9bca04399d932c4860f985adea1f
|
4
|
+
data.tar.gz: 8f04da33316e49efd6d15145ab19adec358f7035f7d5e07721b216e1f7ec9796
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c74910d5332103d7240e28f848d092d70299178735a9ba38dcd5e307e5d11859890201bc0ba6072b0c94fe96885994fb9195d4b83a90316b10986dfd29d69ebf
|
7
|
+
data.tar.gz: 6835c6fc24fe5874de7b7b5e653b75b41cf2ef3c78d7909fb4194bee6dd764baf4bb7e4634ef7fc5ccdf7c720bc292152ac6b9d046d36bdb65140ccbede9e391
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
# 0.1.7
|
2
|
+
|
3
|
+
* Move `RackResponseWrapper` into the main namespace
|
4
|
+
* Add `#satisfied_with_first_interval?` so that certain Range: requests can be served using a redirect
|
5
|
+
* Add `#multiple_ranges?` so that one can choose not to honor multipart Range requests
|
6
|
+
|
7
|
+
# 0.1.6
|
8
|
+
|
9
|
+
* Create a base response type (`Abstract`) which has the same interface as the rest of the responses
|
10
|
+
|
1
11
|
# 0.1.5
|
2
12
|
|
3
13
|
* Change the API of `IntervalResponse.new` to accept the Rack `env` hash directly, without having the caller extract the header values manually.
|
data/lib/interval_response.rb
CHANGED
@@ -5,6 +5,7 @@ module IntervalResponse
|
|
5
5
|
class Error < StandardError; end
|
6
6
|
|
7
7
|
require_relative "interval_response/version"
|
8
|
+
require_relative "interval_response/rack_body_wrapper"
|
8
9
|
require_relative "interval_response/sequence"
|
9
10
|
require_relative "interval_response/abstract"
|
10
11
|
require_relative "interval_response/empty"
|
@@ -1,70 +1,32 @@
|
|
1
1
|
# Base class for all response types, primarily for ease of documentation
|
2
2
|
class IntervalResponse::Abstract
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# at instantiation, filling up a pre-allocated String object
|
7
|
-
# with the bytes to be served out.
|
8
|
-
class RackBodyWrapper
|
9
|
-
# Default size of the chunk (String buffer) which is going to be
|
10
|
-
# yielded to the caller of the `each` method.
|
11
|
-
# Set toroughly one TCP kernel buffer
|
12
|
-
CHUNK_SIZE = 65 * 1024
|
13
|
-
|
14
|
-
def initialize(with_interval_response, chunk_size:)
|
15
|
-
@chunk_size = chunk_size
|
16
|
-
@interval_response = with_interval_response
|
17
|
-
end
|
18
|
-
|
19
|
-
def each
|
20
|
-
buf = String.new(capacity: @chunk_size)
|
21
|
-
@interval_response.each do |segment, range_in_segment|
|
22
|
-
case segment
|
23
|
-
when IntervalResponse::LazyFile
|
24
|
-
segment.with do |file_handle|
|
25
|
-
with_each_chunk(range_in_segment) do |offset, read_n|
|
26
|
-
file_handle.seek(offset, IO::SEEK_SET)
|
27
|
-
yield file_handle.read(read_n, buf)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
when String
|
31
|
-
with_each_chunk(range_in_segment) do |offset, read_n|
|
32
|
-
yield segment.slice(offset, read_n)
|
33
|
-
end
|
34
|
-
when IO, Tempfile
|
35
|
-
with_each_chunk(range_in_segment) do |offset, read_n|
|
36
|
-
segment.seek(offset, IO::SEEK_SET)
|
37
|
-
yield segment.read(read_n, buf)
|
38
|
-
end
|
39
|
-
else
|
40
|
-
raise TypeError, "RackBodyWrapper only supports IOs or Strings"
|
41
|
-
end
|
42
|
-
end
|
43
|
-
ensure
|
44
|
-
buf.clear
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def with_each_chunk(range_in_segment)
|
50
|
-
range_size = range_in_segment.end - range_in_segment.begin + 1
|
51
|
-
start_at_offset = range_in_segment.begin
|
52
|
-
n_whole_segments, remainder = range_size.divmod(@chunk_size)
|
53
|
-
|
54
|
-
n_whole_segments.times do |n|
|
55
|
-
unit_offset = start_at_offset + (n * @chunk_size)
|
56
|
-
yield unit_offset, @chunk_size
|
57
|
-
end
|
3
|
+
def to_rack_response_triplet(headers: nil, chunk_size: IntervalResponse::RackBodyWrapper::CHUNK_SIZE)
|
4
|
+
[status_code, headers.to_h.merge(self.headers), IntervalResponse::RackBodyWrapper.new(self, chunk_size: chunk_size)]
|
5
|
+
end
|
58
6
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
7
|
+
# Tells whether this response is responding with multiple ranges. If you want to simulate S3 for example,
|
8
|
+
# it might be relevant to deny a response from being served if it does respond with multiple ranges -
|
9
|
+
# IntervalResponse supports these responses just fine, but S3 doesn't.
|
10
|
+
def multiple_ranges?
|
11
|
+
false
|
64
12
|
end
|
65
13
|
|
66
|
-
|
67
|
-
|
14
|
+
# Tells whether this entire requested range can be satisfied with the first available segment within the given Sequence.
|
15
|
+
# If it is, then you can redirect to the URL of the first segment instead of streaming the response
|
16
|
+
# through - which can be cheaper for your application server. Note that you can redirect to the resource of the first
|
17
|
+
# interval only, because otherwise your `Range` header will no longer match. Suppose you have a stitched resource
|
18
|
+
# consisting of two segments:
|
19
|
+
#
|
20
|
+
# [bytes 0..456]
|
21
|
+
# [bytes 457..890]
|
22
|
+
#
|
23
|
+
# and your client requests `Range: bytes=0-33`. You can redirect the client to the location of the first interval,
|
24
|
+
# and the `Range:` header will be retransmitted to that location and will be satisfied. However, imagine you are requesting
|
25
|
+
# the `Range: bytes=510-512` - you _could_ redirect just to the second interval, but the `Range` header is not going to be
|
26
|
+
# adjusted by the client, and you are not going to receive the correct slice of the resource. That's why you can only
|
27
|
+
# redirect to the first interval only.
|
28
|
+
def satisfied_with_first_interval?
|
29
|
+
false
|
68
30
|
end
|
69
31
|
|
70
32
|
# @param interval_sequence[IntervalResponse::Sequence] the sequence the response is built for
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# Serves out a response that contains the entire resource
|
2
2
|
class IntervalResponse::Full < IntervalResponse::Abstract
|
3
|
+
def initialize(*)
|
4
|
+
super
|
5
|
+
@http_range_for_entire_resource = 0..(@interval_sequence.size - 1)
|
6
|
+
end
|
7
|
+
|
3
8
|
def each
|
4
|
-
|
5
|
-
full_range = 0..(@interval_sequence.size - 1)
|
6
|
-
@interval_sequence.each_in_range(full_range) do |segment, range_in_segment|
|
9
|
+
@interval_sequence.each_in_range(@http_range_for_entire_resource) do |segment, range_in_segment|
|
7
10
|
yield(segment, range_in_segment)
|
8
11
|
end
|
9
12
|
end
|
@@ -16,6 +19,14 @@ class IntervalResponse::Full < IntervalResponse::Abstract
|
|
16
19
|
@interval_sequence.size
|
17
20
|
end
|
18
21
|
|
22
|
+
def satisfied_with_first_interval?
|
23
|
+
@interval_sequence.first_interval_only?(@http_range_for_entire_resource)
|
24
|
+
end
|
25
|
+
|
26
|
+
def multiple_ranges?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
19
30
|
def headers
|
20
31
|
{
|
21
32
|
'Accept-Ranges' => 'bytes',
|
@@ -47,6 +47,14 @@ class IntervalResponse::Multi < IntervalResponse::Abstract
|
|
47
47
|
}
|
48
48
|
end
|
49
49
|
|
50
|
+
def satisfied_with_first_interval?
|
51
|
+
@interval_sequence.first_interval_only?(*@http_ranges)
|
52
|
+
end
|
53
|
+
|
54
|
+
def multiple_ranges?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
50
58
|
private
|
51
59
|
|
52
60
|
def compute_envelope_size
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# The Rack body wrapper is intended to be returned as the third element
|
2
|
+
# of the Rack response triplet. It supports the #each method
|
3
|
+
# and will call to the IntervalResponse object given to it
|
4
|
+
# at instantiation, filling up a pre-allocated String object
|
5
|
+
# with the bytes to be served out. The String object will then be repeatedly
|
6
|
+
# yielded to the Rack webserver with the response data. Since Ruby strings
|
7
|
+
# are mutable, the String object will be sized to a certain capacity and reused
|
8
|
+
# across calls to save allocations.
|
9
|
+
class IntervalResponse::RackBodyWrapper
|
10
|
+
# Default size of the chunk (String buffer) which is going to be
|
11
|
+
# yielded to the caller of the `each` method.
|
12
|
+
# Set toroughly one TCP kernel buffer
|
13
|
+
CHUNK_SIZE = 65 * 1024
|
14
|
+
|
15
|
+
def initialize(with_interval_response, chunk_size:)
|
16
|
+
@chunk_size = chunk_size
|
17
|
+
@interval_response = with_interval_response
|
18
|
+
end
|
19
|
+
|
20
|
+
def each
|
21
|
+
buf = String.new(capacity: @chunk_size)
|
22
|
+
@interval_response.each do |segment, range_in_segment|
|
23
|
+
case segment
|
24
|
+
when IntervalResponse::LazyFile
|
25
|
+
segment.with do |file_handle|
|
26
|
+
with_each_chunk(range_in_segment) do |offset, read_n|
|
27
|
+
file_handle.seek(offset, IO::SEEK_SET)
|
28
|
+
yield file_handle.read(read_n, buf)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
when String
|
32
|
+
with_each_chunk(range_in_segment) do |offset, read_n|
|
33
|
+
yield segment.slice(offset, read_n)
|
34
|
+
end
|
35
|
+
when IO, Tempfile
|
36
|
+
with_each_chunk(range_in_segment) do |offset, read_n|
|
37
|
+
segment.seek(offset, IO::SEEK_SET)
|
38
|
+
yield segment.read(read_n, buf)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
raise TypeError, "RackBodyWrapper only supports IOs or Strings"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
buf.clear
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def with_each_chunk(range_in_segment)
|
51
|
+
range_size = range_in_segment.end - range_in_segment.begin + 1
|
52
|
+
start_at_offset = range_in_segment.begin
|
53
|
+
n_whole_segments, remainder = range_size.divmod(@chunk_size)
|
54
|
+
|
55
|
+
n_whole_segments.times do |n|
|
56
|
+
unit_offset = start_at_offset + (n * @chunk_size)
|
57
|
+
yield unit_offset, @chunk_size
|
58
|
+
end
|
59
|
+
|
60
|
+
if remainder > 0
|
61
|
+
unit_offset = start_at_offset + (n_whole_segments * @chunk_size)
|
62
|
+
yield unit_offset, remainder
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -42,6 +42,8 @@ class IntervalResponse::Sequence
|
|
42
42
|
def add_segment(segment, size:, etag: size)
|
43
43
|
if size > 0
|
44
44
|
etag_quoted = '"%s"' % etag
|
45
|
+
# We save the index of the interval inside the Struct so that we can
|
46
|
+
# use `bsearch` later instead of requiring `bsearch_index` to be available
|
45
47
|
@intervals << Interval.new(segment, size, @size, @intervals.length, etag_quoted)
|
46
48
|
@size += size
|
47
49
|
end
|
@@ -58,31 +60,38 @@ class IntervalResponse::Sequence
|
|
58
60
|
# need to retrieve data from the inner Sequence which is one of the segments, the call will
|
59
61
|
# yield the segments from the inner Sequence, "drilling down" as deep as is appropriate.
|
60
62
|
#
|
63
|
+
# Three arguments will be yielded to the block - the segment (the "meat" of an interval, which
|
64
|
+
# is the object given when the interval was added to the Sequence), the range within the interval
|
65
|
+
# (which is always going to be an inclusive `Range` of integers) and a boolean flag indicating whether
|
66
|
+
# this interval is the very first interval in the requested subset of the sequence. This flag honors nesting
|
67
|
+
# (if you have arbitrarily nested interval Sequences and you request something from the first interval of
|
68
|
+
# several Sequences deep it will still indicate `true`).
|
69
|
+
#
|
61
70
|
# @param from_range_in_resource[Range] an inclusive Range that specifies the range within the segment map
|
62
|
-
# @yield segment[Object], range_in_segment[Range]
|
71
|
+
# @yield segment[Object], range_in_segment[Range], is_first_interval[Boolean]
|
63
72
|
def each_in_range(from_range_in_resource)
|
64
73
|
# Skip empty ranges
|
65
74
|
requested_range_size = (from_range_in_resource.end - from_range_in_resource.begin) + 1
|
66
75
|
return if requested_range_size < 1
|
67
76
|
|
68
|
-
#
|
77
|
+
# Then walk through included intervals. If the range misses
|
78
|
+
# our intervals completely included_intervals will be empty.
|
69
79
|
included_intervals = intervals_within_range(from_range_in_resource)
|
70
|
-
|
71
|
-
# And normal case - walk through included intervals
|
72
80
|
included_intervals.each do |interval|
|
73
81
|
int_start = interval.offset
|
74
82
|
int_end = interval.offset + interval.size - 1
|
75
83
|
req_start = from_range_in_resource.begin
|
76
84
|
req_end = from_range_in_resource.end
|
77
85
|
range_within_interval = (max(int_start, req_start) - int_start)..(min(int_end, req_end) - int_start)
|
86
|
+
is_first_interval = interval.position == 0
|
78
87
|
|
79
88
|
# Allow Sequences to be composed together
|
80
89
|
if interval.segment.respond_to?(:each_in_range)
|
81
|
-
interval.segment.each_in_range(range_within_interval) do |sub_segment, sub_range|
|
82
|
-
yield(sub_segment, sub_range)
|
90
|
+
interval.segment.each_in_range(range_within_interval) do |sub_segment, sub_range, is_first_nested_interval|
|
91
|
+
yield(sub_segment, sub_range, is_first_interval && is_first_nested_interval)
|
83
92
|
end
|
84
93
|
else
|
85
|
-
yield(interval.segment, range_within_interval)
|
94
|
+
yield(interval.segment, range_within_interval, is_first_interval)
|
86
95
|
end
|
87
96
|
end
|
88
97
|
end
|
@@ -132,6 +141,19 @@ class IntervalResponse::Sequence
|
|
132
141
|
'"%s"' % d.hexdigest
|
133
142
|
end
|
134
143
|
|
144
|
+
# Tells whether all of the given `ranges` will be satisfied from the first interval only. This can be used to
|
145
|
+
# redirect to the resource at that interval instead of proxying it through, since the `Range` header won't need to
|
146
|
+
# be adjusted
|
147
|
+
def first_interval_only?(*ranges)
|
148
|
+
ranges.map do |range|
|
149
|
+
each_in_range(range) do |_, _, is_first_interval|
|
150
|
+
return false unless is_first_interval
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
135
157
|
private
|
136
158
|
|
137
159
|
def max(a, b)
|
@@ -143,6 +165,10 @@ class IntervalResponse::Sequence
|
|
143
165
|
end
|
144
166
|
|
145
167
|
def interval_under(offset)
|
168
|
+
# For our purposes we would be better served by `bsearch_index`, but it is not available
|
169
|
+
# on older Ruby versions which we otherwise can splendidly support. Since when we retrieve
|
170
|
+
# the interval under offset we are going to need the index anyway, and since calling `Array#index`
|
171
|
+
# will incur another linear scan of the array, we save the index of the interval with the interval itself.
|
146
172
|
@intervals.bsearch do |interval|
|
147
173
|
# bsearch expects a 0 return value for "exact match".
|
148
174
|
# -1 tells it "look to my left" and 1 "look to my right",
|
@@ -17,19 +17,35 @@ RSpec.describe IntervalResponse::Sequence do
|
|
17
17
|
expect(seq.size).to eq(6 + 12 + 17)
|
18
18
|
expect { |b|
|
19
19
|
seq.each_in_range(0..0, &b)
|
20
|
-
}.to yield_with_args(a, 0..0)
|
20
|
+
}.to yield_with_args(a, 0..0, true)
|
21
21
|
|
22
22
|
expect { |b|
|
23
23
|
seq.each_in_range(0..7, &b)
|
24
|
-
}.to yield_successive_args([a, 0..5], [b, 0..1])
|
24
|
+
}.to yield_successive_args([a, 0..5, true], [b, 0..1, false])
|
25
25
|
|
26
26
|
expect { |b|
|
27
27
|
seq.each_in_range(7..27, &b)
|
28
|
-
}.to yield_successive_args([b, 1..11], [c, 0..9])
|
28
|
+
}.to yield_successive_args([b, 1..11, false], [c, 0..9, false])
|
29
29
|
|
30
30
|
expect { |b|
|
31
31
|
seq.each_in_range(0..(6 + 12 - 1), &b)
|
32
|
-
}.to yield_successive_args([a, 0..5], [b, 0..11])
|
32
|
+
}.to yield_successive_args([a, 0..5, true], [b, 0..11, false])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'indicates whether the first interval will satisfy a set of Ranges' do
|
36
|
+
seq = described_class.new
|
37
|
+
|
38
|
+
a = double(:a, size: 6)
|
39
|
+
b = double(:b, size: 12)
|
40
|
+
c = double(:c, size: 17)
|
41
|
+
seq << a << b << c
|
42
|
+
|
43
|
+
expect(seq).to be_first_interval_only(0..0)
|
44
|
+
expect(seq).to be_first_interval_only(0..0, 0..5)
|
45
|
+
expect(seq).not_to be_first_interval_only(0..6)
|
46
|
+
expect(seq).not_to be_first_interval_only(3..8)
|
47
|
+
expect(seq).not_to be_first_interval_only(15..16)
|
48
|
+
expect(seq).not_to be_first_interval_only(0..0, 15..16)
|
33
49
|
end
|
34
50
|
|
35
51
|
it 'generates the ETag for an empty sequence, and the etag contains data' do
|
@@ -91,7 +107,7 @@ RSpec.describe IntervalResponse::Sequence do
|
|
91
107
|
seq = described_class.new(a, b, c)
|
92
108
|
expect { |b|
|
93
109
|
seq.each_in_range(0..27, &b)
|
94
|
-
}.to yield_successive_args([a, 0..2], [b, 0..3], [c, 0..0])
|
110
|
+
}.to yield_successive_args([a, 0..2, true], [b, 0..3, false], [c, 0..0, false])
|
95
111
|
end
|
96
112
|
|
97
113
|
it 'is composable' do
|
@@ -103,7 +119,7 @@ RSpec.describe IntervalResponse::Sequence do
|
|
103
119
|
|
104
120
|
expect { |b|
|
105
121
|
seq.each_in_range(0..27, &b)
|
106
|
-
}.to yield_successive_args([a, 0..2], [b, 0..3], [c, 0..0])
|
122
|
+
}.to yield_successive_args([a, 0..2, true], [b, 0..3, false], [c, 0..0, false])
|
107
123
|
end
|
108
124
|
|
109
125
|
it 'has close to linear performance with large number of ranges and intervals' do
|
@@ -56,6 +56,9 @@ RSpec.describe IntervalResponse do
|
|
56
56
|
'ETag' => seq.etag,
|
57
57
|
)
|
58
58
|
expect(response.etag).to eq(seq.etag)
|
59
|
+
expect(response).not_to be_multiple_ranges
|
60
|
+
expect(response).not_to be_satisfied_with_first_interval
|
61
|
+
|
59
62
|
expect { |b|
|
60
63
|
response.each(&b)
|
61
64
|
}.to yield_successive_args([segment_a, 0..2], [segment_b, 0..3], [segment_c, 0..0])
|
@@ -72,6 +75,8 @@ RSpec.describe IntervalResponse do
|
|
72
75
|
'ETag' => seq.etag
|
73
76
|
)
|
74
77
|
expect(response.etag).to eq(seq.etag)
|
78
|
+
expect(response).not_to be_multiple_ranges
|
79
|
+
expect(response).not_to be_satisfied_with_first_interval
|
75
80
|
end
|
76
81
|
|
77
82
|
it 'returns a single HTTP range if the client asked for it and it can be satisfied' do
|
@@ -86,11 +91,34 @@ RSpec.describe IntervalResponse do
|
|
86
91
|
'ETag' => seq.etag,
|
87
92
|
)
|
88
93
|
expect(response.etag).to eq(seq.etag)
|
94
|
+
expect(response).not_to be_multiple_ranges
|
95
|
+
expect(response).not_to be_satisfied_with_first_interval
|
96
|
+
|
89
97
|
expect { |b|
|
90
98
|
response.each(&b)
|
91
99
|
}.to yield_successive_args([segment_a, 2..2], [segment_b, 0..1])
|
92
100
|
end
|
93
101
|
|
102
|
+
it 'returns a single HTTP range if the client asked for it and hints it can be satisfied from the first interval' do
|
103
|
+
response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=0-0")
|
104
|
+
expect(response.status_code).to eq(206)
|
105
|
+
expect(response.content_length).to eq(1)
|
106
|
+
expect(response.headers).to eq(
|
107
|
+
"Accept-Ranges" => "bytes",
|
108
|
+
"Content-Length" => "1",
|
109
|
+
"Content-Range" => "bytes 0-0/8",
|
110
|
+
"Content-Type" => "binary/octet-stream",
|
111
|
+
'ETag' => seq.etag,
|
112
|
+
)
|
113
|
+
expect(response.etag).to eq(seq.etag)
|
114
|
+
expect(response).not_to be_multiple_ranges
|
115
|
+
expect(response).to be_satisfied_with_first_interval
|
116
|
+
|
117
|
+
expect { |b|
|
118
|
+
response.each(&b)
|
119
|
+
}.to yield_successive_args([segment_a, 0..0])
|
120
|
+
end
|
121
|
+
|
94
122
|
it 'returns a single HTTP range if the client asked for it and it can be satisfied, ETag matches' do
|
95
123
|
response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=2-4", "HTTP_IF_RANGE" => seq.etag)
|
96
124
|
expect(response.status_code).to eq(206)
|
@@ -108,7 +136,7 @@ RSpec.describe IntervalResponse do
|
|
108
136
|
}.to yield_successive_args([segment_a, 2..2], [segment_b, 0..1])
|
109
137
|
end
|
110
138
|
|
111
|
-
it '
|
139
|
+
it 'responds with the entire resource if the Range is satisfiable but the If-Range specifies a different ETag than the sequence' do
|
112
140
|
response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=12901-", "HTTP_IF_RANGE" => '"different"')
|
113
141
|
expect(response.status_code).to eq(200)
|
114
142
|
expect(response.content_length).to eq(8)
|
@@ -119,6 +147,8 @@ RSpec.describe IntervalResponse do
|
|
119
147
|
'ETag' => seq.etag,
|
120
148
|
)
|
121
149
|
expect(response.etag).to eq(seq.etag)
|
150
|
+
expect(response).not_to be_multiple_ranges
|
151
|
+
expect(response).not_to be_satisfied_with_first_interval
|
122
152
|
end
|
123
153
|
|
124
154
|
it 'responds with the range that can be satisfied if asked for 2, of which one is unsatisfiable' do
|
@@ -133,6 +163,8 @@ RSpec.describe IntervalResponse do
|
|
133
163
|
'ETag' => seq.etag,
|
134
164
|
)
|
135
165
|
expect(response.etag).to eq(seq.etag)
|
166
|
+
expect(response).not_to be_multiple_ranges
|
167
|
+
expect(response).not_to be_satisfied_with_first_interval
|
136
168
|
|
137
169
|
expect { |b|
|
138
170
|
response.each(&b)
|
@@ -152,6 +184,8 @@ RSpec.describe IntervalResponse do
|
|
152
184
|
'ETag' => seq.etag,
|
153
185
|
)
|
154
186
|
expect(response.etag).to eq(seq.etag)
|
187
|
+
expect(response).to be_multiple_ranges
|
188
|
+
expect(response).to be_satisfied_with_first_interval
|
155
189
|
|
156
190
|
output = StringIO.new
|
157
191
|
response.each do |segment, range|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: interval_response
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -148,6 +148,7 @@ files:
|
|
148
148
|
- lib/interval_response/invalid.rb
|
149
149
|
- lib/interval_response/lazy_file.rb
|
150
150
|
- lib/interval_response/multi.rb
|
151
|
+
- lib/interval_response/rack_body_wrapper.rb
|
151
152
|
- lib/interval_response/sequence.rb
|
152
153
|
- lib/interval_response/single.rb
|
153
154
|
- lib/interval_response/version.rb
|
@@ -162,7 +163,7 @@ metadata:
|
|
162
163
|
homepage_uri: https://github.com/WeTransfer/interval_response
|
163
164
|
source_code_uri: https://github.com/WeTransfer/interval_response
|
164
165
|
changelog_uri: https://github.com/WeTransfer/interval_response
|
165
|
-
post_install_message:
|
166
|
+
post_install_message:
|
166
167
|
rdoc_options: []
|
167
168
|
require_paths:
|
168
169
|
- lib
|
@@ -178,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
179
|
version: '0'
|
179
180
|
requirements: []
|
180
181
|
rubygems_version: 3.0.3
|
181
|
-
signing_key:
|
182
|
+
signing_key:
|
182
183
|
specification_version: 4
|
183
184
|
summary: Assemble HTTP responses from spliced sequences of payloads
|
184
185
|
test_files: []
|