dis 2.1.0 → 2.2.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/lib/dis/controller.rb +57 -5
- data/lib/dis/response_body.rb +79 -9
- data/lib/dis/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4c3624aed32276988e39638489e694b4755991ec156d6e334013ff3e9f6dcae
|
|
4
|
+
data.tar.gz: 381f0c8bad7c0895f556d08040b2e83f514a8b52deca71db27740802048b5029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 911c9655763e26056cd0a93e32b37a546983798d24271be047d4c720a8a8fcbc5db0152dafabf147cf3cc788e60e6cf7e53222afc414345b0ac483a470967af0
|
|
7
|
+
data.tar.gz: efe469600850c64269abfbd2fecdf221cb70d8a1de9200022891440cc432d37bde55b0535f88fed462e8fa51c8b90fdd492f0079f7333d28be9b55599487f19a
|
data/lib/dis/controller.rb
CHANGED
|
@@ -29,6 +29,10 @@ module Dis
|
|
|
29
29
|
# +filename+ and +content_type+ default to the record's own
|
|
30
30
|
# metadata.
|
|
31
31
|
#
|
|
32
|
+
# Responds with +206 Partial Content+ to a range request, or +416
|
|
33
|
+
# Range Not Satisfiable+ if the range lies beyond the data. Several
|
|
34
|
+
# ranges are sent as +multipart/byteranges+.
|
|
35
|
+
#
|
|
32
36
|
# @param record [Dis::Model] the record to send data from
|
|
33
37
|
# @param filename [String, nil] suggested filename
|
|
34
38
|
# @param content_type [String, nil] the content type
|
|
@@ -42,14 +46,62 @@ module Dis
|
|
|
42
46
|
def send_dis_data(record, filename: nil, content_type: nil,
|
|
43
47
|
disposition: "attachment", status: 200)
|
|
44
48
|
file = record.open_data
|
|
49
|
+
ranges = dis_byte_ranges(file.size, status)
|
|
50
|
+
response.headers["Accept-Ranges"] = "bytes"
|
|
51
|
+
return dis_unsatisfiable_range(file) if ranges && ranges.empty?
|
|
52
|
+
|
|
53
|
+
type = content_type || dis_metadata(record, :content_type)
|
|
54
|
+
dis_send_file_headers(record, filename, type, disposition)
|
|
55
|
+
dis_send_body(
|
|
56
|
+
Dis::ResponseBody.new(file, ranges:, content_type: type), file.size,
|
|
57
|
+
status
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def dis_byte_ranges(size, status)
|
|
62
|
+
return unless status == 200
|
|
63
|
+
return unless dis_if_range_matches?
|
|
64
|
+
|
|
65
|
+
Rack::Utils.get_byte_ranges(request.get_header("HTTP_RANGE"), size)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dis_if_range_matches?
|
|
69
|
+
if_range = request.get_header("HTTP_IF_RANGE")
|
|
70
|
+
return true if if_range.blank?
|
|
71
|
+
|
|
72
|
+
[response.etag, response.headers["Last-Modified"]].any? do |validator|
|
|
73
|
+
validator.present? && if_range == validator
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dis_unsatisfiable_range(file)
|
|
78
|
+
response.headers["Content-Range"] = "bytes */#{file.size}"
|
|
79
|
+
file.close
|
|
80
|
+
head :range_not_satisfiable
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dis_send_file_headers(record, filename, type, disposition)
|
|
45
84
|
send_file_headers!(
|
|
46
85
|
{ filename: filename || dis_metadata(record, :filename),
|
|
47
|
-
type:
|
|
48
|
-
disposition: }.compact
|
|
86
|
+
type:, disposition: }.compact
|
|
49
87
|
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dis_send_body(body, size, status)
|
|
91
|
+
self.status = body.ranges.any? ? :partial_content : status
|
|
92
|
+
dis_partial_headers(body, size)
|
|
93
|
+
response.headers["Content-Length"] = body.length.to_s
|
|
94
|
+
self.response_body = body
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def dis_partial_headers(body, size)
|
|
98
|
+
if body.multipart?
|
|
99
|
+
self.content_type =
|
|
100
|
+
"multipart/byteranges; boundary=#{body.boundary}"
|
|
101
|
+
elsif body.range
|
|
102
|
+
response.headers["Content-Range"] =
|
|
103
|
+
"bytes #{body.range.begin}-#{body.range.end}/#{size}"
|
|
104
|
+
end
|
|
53
105
|
end
|
|
54
106
|
|
|
55
107
|
def dis_metadata(record, name)
|
data/lib/dis/response_body.rb
CHANGED
|
@@ -1,38 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module Dis
|
|
4
6
|
# = Dis Response Body
|
|
5
7
|
#
|
|
6
8
|
# Rack body that streams an open file and closes it once the
|
|
7
9
|
# response has been sent. The file may already be unlinked, so it is
|
|
8
10
|
# read through the open descriptor and never by path.
|
|
11
|
+
#
|
|
12
|
+
# Streams only the given ranges when any are given, as a
|
|
13
|
+
# +multipart/byteranges+ payload if there is more than one.
|
|
9
14
|
class ResponseBody
|
|
10
15
|
CHUNK_SIZE = 16_384
|
|
11
16
|
|
|
12
17
|
delegate :closed?, to: :@file
|
|
13
18
|
|
|
19
|
+
# @return [Array<Range>] the ranges being streamed
|
|
20
|
+
attr_reader :ranges
|
|
21
|
+
|
|
22
|
+
# @return [String, nil] the multipart boundary, when multipart
|
|
23
|
+
attr_reader :boundary
|
|
24
|
+
|
|
14
25
|
# @param file [File] an open, readable file
|
|
15
|
-
|
|
26
|
+
# @param ranges [Array<Range>, nil] byte ranges, or nil for all of it
|
|
27
|
+
# @param content_type [String, nil] content type of the parts
|
|
28
|
+
def initialize(file, ranges: nil, content_type: nil)
|
|
16
29
|
@file = file
|
|
30
|
+
@ranges = Array(ranges)
|
|
31
|
+
@content_type = content_type
|
|
32
|
+
@boundary = SecureRandom.hex(16) if multipart?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] whether more than one range is being streamed
|
|
36
|
+
def multipart?
|
|
37
|
+
ranges.length > 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Range, nil] the range being streamed, unless multipart
|
|
41
|
+
def range
|
|
42
|
+
ranges.first unless multipart?
|
|
17
43
|
end
|
|
18
44
|
|
|
19
|
-
# Returns the
|
|
45
|
+
# Returns the number of bytes that will be written.
|
|
46
|
+
#
|
|
47
|
+
# @return [Integer]
|
|
48
|
+
def length
|
|
49
|
+
return @file.size if ranges.empty?
|
|
50
|
+
return multipart_length if multipart?
|
|
51
|
+
|
|
52
|
+
range.size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the contents as a binary string.
|
|
20
56
|
#
|
|
21
57
|
# @return [String]
|
|
22
58
|
def body
|
|
23
|
-
|
|
24
|
-
@file.read
|
|
59
|
+
(+"").b.tap { |out| each { |chunk| out << chunk } }
|
|
25
60
|
end
|
|
26
61
|
|
|
27
62
|
# Yields the contents in chunks.
|
|
28
63
|
#
|
|
29
64
|
# @yieldparam chunk [String] a chunk of the contents
|
|
30
65
|
# @return [void]
|
|
31
|
-
def each
|
|
32
|
-
@file.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
def each(&)
|
|
67
|
+
return stream(0, @file.size, &) if ranges.empty?
|
|
68
|
+
return stream(range.begin, range.size, &) unless multipart?
|
|
69
|
+
|
|
70
|
+
each_part(&)
|
|
36
71
|
end
|
|
37
72
|
|
|
38
73
|
# Closes the underlying file.
|
|
@@ -41,5 +76,40 @@ module Dis
|
|
|
41
76
|
def close
|
|
42
77
|
@file.close unless @file.closed?
|
|
43
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def each_part(&)
|
|
83
|
+
ranges.each do |range|
|
|
84
|
+
yield heading(range)
|
|
85
|
+
stream(range.begin, range.size, &)
|
|
86
|
+
end
|
|
87
|
+
yield terminator
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stream(offset, remaining)
|
|
91
|
+
@file.seek(offset)
|
|
92
|
+
while remaining.positive? &&
|
|
93
|
+
(chunk = @file.read([CHUNK_SIZE, remaining].min))
|
|
94
|
+
remaining -= chunk.bytesize
|
|
95
|
+
yield chunk
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def multipart_length
|
|
100
|
+
ranges.sum { |range| heading(range).bytesize + range.size } +
|
|
101
|
+
terminator.bytesize
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def heading(range)
|
|
105
|
+
"\r\n--#{boundary}\r\n" \
|
|
106
|
+
"Content-Type: #{@content_type}\r\n" \
|
|
107
|
+
"Content-Range: bytes #{range.begin}-#{range.end}/#{@file.size}\r\n" \
|
|
108
|
+
"\r\n"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def terminator
|
|
112
|
+
"\r\n--#{boundary}--\r\n"
|
|
113
|
+
end
|
|
44
114
|
end
|
|
45
115
|
end
|
data/lib/dis/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inge Jørgensen
|
|
@@ -71,6 +71,20 @@ dependencies:
|
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rack
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: 2.2.4
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 2.2.4
|
|
74
88
|
- !ruby/object:Gem::Dependency
|
|
75
89
|
name: rails
|
|
76
90
|
requirement: !ruby/object:Gem::Requirement
|