interval_response 0.1.6 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|