dis 2.1.0 → 2.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b78f0ff03e09c43d839c8ba8e119c6548ae518469d17d6646767c987f36f6a15
4
- data.tar.gz: 3c9d88b627d1eadb871f926a2552e9761eb007442543db0aa65f88623e00f168
3
+ metadata.gz: 4aff9f12155e9d3335997a6b8c81c519e7b0447d9bf1079e07c717911b00b594
4
+ data.tar.gz: de04178e69d46d29b5ae581a4844a1c1c418eb14d2e81b4cd88ee72112f08107
5
5
  SHA512:
6
- metadata.gz: 7987a1138a986daa040447ff607a07b6bf077a0ea0450274dafbff3b977cca1fee9bdf2121847c9492ffe75fe12b7bb11fc8a056240d473a69129e205ca821af
7
- data.tar.gz: 888c70668ce77b47451359d5cfe953793488eb1238749eaa0f7151e6779ed105ff6a1941cd9b8e3d69419c24a349c022fc857cea443bb48b48f102f4711507bf
6
+ metadata.gz: 157a02d4c6e513436b0fd3c6a9d07f81ab3192de2e7899129a142ab028a11b65681c4e764ce985406dc8ae3231e2ad8e0ed1a4f6a23ce921c0d239f839638eec
7
+ data.tar.gz: f9f9ae7984bc88232c1442ea85759353c4d5987f1704b488b9f5605718fdf8b5b50bea7bceedfdc0d9734e439f5f12bc226f518005763d79fa07daa3309785e2
@@ -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: content_type || dis_metadata(record, :content_type),
48
- disposition: }.compact
86
+ type:, disposition: }.compact
49
87
  )
50
- self.status = status
51
- response.headers["Content-Length"] = file.size.to_s
52
- self.response_body = Dis::ResponseBody.new(file)
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)
@@ -58,11 +58,10 @@ module Dis
58
58
  #
59
59
  # @return [Integer]
60
60
  def content_length
61
- if raw? && raw.respond_to?(:length)
62
- raw.length
63
- else
64
- read.try(&:length).to_i
65
- end
61
+ return raw.bytesize if raw? && raw.respond_to?(:bytesize)
62
+ return raw.size if raw? && raw.respond_to?(:size)
63
+
64
+ read.try(&:bytesize).to_i
66
65
  end
67
66
 
68
67
  # Expires a data object from the storage if it's no longer
@@ -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
- def initialize(file)
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 entire contents as a binary string.
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
- @file.rewind
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.rewind
33
- while (chunk = @file.read(CHUNK_SIZE))
34
- yield chunk
35
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dis
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.1"
5
5
  end
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.1.0
4
+ version: 2.2.1
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