dassets 0.11.0 → 0.12.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.
data/README.md CHANGED
@@ -28,8 +28,8 @@ end
28
28
  ### Link To
29
29
 
30
30
  ```rb
31
- Dassets['css/site.css'].href # => "/css/site-123abc.css"
32
- Dassets['img/logos/main.jpg'].href # => "/img/logos/main-a1b2c3.jpg"
31
+ Dassets['css/site.css'].url # => "/css/site-123abc.css"
32
+ Dassets['img/logos/main.jpg'].url # => "/img/logos/main-a1b2c3.jpg"
33
33
  ```
34
34
 
35
35
  ### Serve
@@ -6,6 +6,7 @@ module Dassets; end
6
6
  class Dassets::Server
7
7
 
8
8
  class Response
9
+
9
10
  attr_reader :asset_file, :status, :headers, :body
10
11
 
11
12
  def initialize(env, asset_file)
@@ -13,18 +14,20 @@ class Dassets::Server
13
14
 
14
15
  mtime = @asset_file.mtime.to_s
15
16
  @status, @headers, @body = if env['HTTP_IF_MODIFIED_SINCE'] == mtime
16
- [ 304, Rack::Utils::HeaderHash.new, [] ]
17
+ [ 304, Rack::Utils::HeaderHash.new('Last-Modified' => mtime), [] ]
17
18
  elsif !@asset_file.exists?
18
19
  [ 404, Rack::Utils::HeaderHash.new, ["Not Found"] ]
19
20
  else
20
21
  @asset_file.digest!
21
- [ 200,
22
+ body = Body.new(env, @asset_file)
23
+ [ body.partial? ? 206 : 200,
22
24
  Rack::Utils::HeaderHash.new.tap do |h|
23
- h["Content-Type"] = @asset_file.mime_type.to_s
24
- h["Content-Length"] = @asset_file.size.to_s
25
- h["Last-Modified"] = mtime
25
+ h['Last-Modified'] = mtime
26
+ h['Content-Type'] = @asset_file.mime_type.to_s
27
+ h['Content-Length'] = body.size.to_s
28
+ h['Content-Range'] = body.content_range if body.partial?
26
29
  end,
27
- env["REQUEST_METHOD"] == "HEAD" ? [] : [ @asset_file.content ]
30
+ env["REQUEST_METHOD"] == "HEAD" ? [] : body
28
31
  ]
29
32
  end
30
33
  end
@@ -33,6 +36,68 @@ class Dassets::Server
33
36
  [@status, @headers.to_hash, @body]
34
37
  end
35
38
 
39
+ class Body
40
+
41
+ # this class borrows from the body range handling in Rack::File and adapts
42
+ # it for use with Dasset's asset files and their generic string content.
43
+
44
+ CHUNK_SIZE = (8*1024).freeze # 8k
45
+
46
+ attr_reader :asset_file, :size, :content_range
47
+
48
+ def initialize(env, asset_file)
49
+ @asset_file = asset_file
50
+
51
+ content_size = @asset_file.size
52
+ ranges = Rack::Utils.byte_ranges(env, content_size)
53
+ if ranges.nil? || ranges.empty? || ranges.length > 1
54
+ # No ranges or multiple ranges are not supported
55
+ @range = 0..content_size-1
56
+ @content_range = nil
57
+ else
58
+ # single range
59
+ @range = ranges[0]
60
+ @content_range = "bytes #{@range.begin}-#{@range.end}/#{content_size}"
61
+ end
62
+
63
+ @size = self.range_end - self.range_begin + 1
64
+ end
65
+
66
+ def partial?
67
+ !@content_range.nil?
68
+ end
69
+
70
+ def range_begin; @range.begin; end
71
+ def range_end; @range.end; end
72
+
73
+ def each
74
+ StringIO.open(@asset_file.content, "rb") do |io|
75
+ io.seek(@range.begin)
76
+ remaining_len = self.size
77
+ while remaining_len > 0
78
+ part = io.read([CHUNK_SIZE, remaining_len].min)
79
+ break if part.nil?
80
+
81
+ remaining_len -= part.length
82
+ yield part
83
+ end
84
+ end
85
+ end
86
+
87
+ def inspect
88
+ "#<#{self.class}:#{'0x0%x' % (self.object_id << 1)} " \
89
+ "digest_path=#{self.asset_file.digest_path} " \
90
+ "range_begin=#{self.range_begin} range_end=#{self.range_end}>"
91
+ end
92
+
93
+ def ==(other_body)
94
+ self.asset_file == other_body.asset_file &&
95
+ self.range_begin == other_body.range_begin &&
96
+ self.range_end == other_body.range_end
97
+ end
98
+
99
+ end
100
+
36
101
  end
37
102
 
38
103
  end
@@ -1,3 +1,3 @@
1
1
  module Dassets
2
- VERSION = "0.11.0"
2
+ VERSION = "0.12.0"
3
3
  end
@@ -38,6 +38,45 @@ module Dassets
38
38
  assert_empty resp.body
39
39
  end
40
40
 
41
+ should "return a partial content response on valid partial content requests" do
42
+ content = Dassets['file1.txt'].content
43
+ size = Factory.integer(content.length)
44
+
45
+ # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
46
+ env = { 'HTTP_RANGE' => "bytes=0-#{size}" }
47
+
48
+ resp = get '/file1-daa05c683a4913b268653f7a7e36a5b4.txt', {}, env
49
+ assert_equal 206, resp.status
50
+ assert_equal content[0..size], resp.body
51
+ end
52
+
53
+ should "return a full response on no-range partial content requests" do
54
+ # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
55
+ env = { 'HTTP_RANGE' => 'bytes=' }
56
+
57
+ resp = get '/file1-daa05c683a4913b268653f7a7e36a5b4.txt', {}, env
58
+ assert_equal 200, resp.status
59
+ assert_equal Dassets['file1.txt'].content, resp.body
60
+ end
61
+
62
+ should "return a full response on multiple-range partial content requests" do
63
+ # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
64
+ env = { 'HTTP_RANGE' => 'bytes=0-1,2-3' }
65
+
66
+ resp = get '/file1-daa05c683a4913b268653f7a7e36a5b4.txt', {}, env
67
+ assert_equal 200, resp.status
68
+ assert_equal Dassets['file1.txt'].content, resp.body
69
+ end
70
+
71
+ should "return a full response on invalid-range partial content requests" do
72
+ # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
73
+ env = { 'HTTP_RANGE' => ['bytes=3-2', 'bytes=abc'].choice }
74
+
75
+ resp = get '/file1-daa05c683a4913b268653f7a7e36a5b4.txt', {}, env
76
+ assert_equal 200, resp.status
77
+ assert_equal Dassets['file1.txt'].content, resp.body
78
+ end
79
+
41
80
  end
42
81
 
43
82
  class DigestTests < SuccessTests
@@ -9,61 +9,220 @@ class Dassets::Server::Response
9
9
  class UnitTests < Assert::Context
10
10
  desc "Dassets::Server::Response"
11
11
  setup do
12
- @resp = file_response(Dassets::AssetFile.new(''))
12
+ @env = {}
13
+ @asset_file = Dassets['file1.txt']
14
+
15
+ @response = Dassets::Server::Response.new(@env, @asset_file)
13
16
  end
14
- subject{ @resp }
17
+ subject{ @response }
15
18
 
16
19
  should have_readers :asset_file, :status, :headers, :body
17
20
  should have_imeths :to_rack
18
21
 
19
22
  should "handle not modified files" do
20
- af = Dassets['file1.txt']
21
- resp = file_response(af, 'HTTP_IF_MODIFIED_SINCE' => af.mtime)
23
+ env = { 'HTTP_IF_MODIFIED_SINCE' => @asset_file.mtime }
24
+ resp = Dassets::Server::Response.new(env, @asset_file)
22
25
 
23
26
  assert_equal 304, resp.status
24
- assert_equal [], resp.body
25
- assert_equal Rack::Utils::HeaderHash.new, resp.headers
26
- assert_equal [ 304, {}, [] ], resp.to_rack
27
+ assert_equal [], resp.body
28
+
29
+ exp_headers = Rack::Utils::HeaderHash.new('Last-Modified' => @asset_file.mtime.to_s)
30
+ assert_equal exp_headers, resp.headers
31
+
32
+ assert_equal [304, exp_headers.to_hash, []], resp.to_rack
27
33
  end
28
34
 
29
35
  should "handle found files" do
30
- af = Dassets['file1.txt']
31
- resp = file_response(af)
36
+ resp = Dassets::Server::Response.new(@env, @asset_file)
37
+
38
+ assert_equal 200, resp.status
39
+
40
+ exp_body = Body.new(@env, @asset_file)
41
+ assert_equal exp_body, resp.body
42
+
32
43
  exp_headers = {
33
44
  'Content-Type' => 'text/plain',
34
- 'Content-Length' => Rack::Utils.bytesize(af.content).to_s,
35
- 'Last-Modified' => af.mtime.to_s
45
+ 'Content-Length' => Rack::Utils.bytesize(@asset_file.content).to_s,
46
+ 'Last-Modified' => @asset_file.mtime.to_s
36
47
  }
37
-
38
- assert_equal 200, resp.status
39
- assert_equal [ af.content ], resp.body
40
48
  assert_equal exp_headers, resp.headers
41
- assert_equal [ 200, exp_headers, [ af.content ] ], resp.to_rack
49
+
50
+ assert_equal [200, exp_headers, exp_body], resp.to_rack
42
51
  end
43
52
 
44
53
  should "have an empty body for found files with a HEAD request" do
45
- af = Dassets['file1.txt']
46
- resp = file_response(af, 'REQUEST_METHOD' => 'HEAD')
54
+ env = { 'REQUEST_METHOD' => 'HEAD' }
55
+ resp = Dassets::Server::Response.new(env, @asset_file)
47
56
 
48
57
  assert_equal 200, resp.status
49
58
  assert_equal [], resp.body
50
59
  end
51
60
 
52
61
  should "handle not found files" do
53
- af = Dassets['not-found-file.txt']
54
- resp = file_response(af)
62
+ af = Dassets['not-found-file.txt']
63
+ resp = Dassets::Server::Response.new(@env, af)
55
64
 
56
- assert_equal 404, resp.status
57
- assert_equal ['Not Found'], resp.body
65
+ assert_equal 404, resp.status
66
+ assert_equal ['Not Found'], resp.body
58
67
  assert_equal Rack::Utils::HeaderHash.new, resp.headers
59
- assert_equal [ 404, {}, ['Not Found'] ], resp.to_rack
68
+ assert_equal [404, {}, ['Not Found']], resp.to_rack
69
+ end
70
+
71
+ end
72
+
73
+ class PartialContentTests < UnitTests
74
+ desc "for a partial content request"
75
+ setup do
76
+ @body = Body.new(@env, @asset_file)
77
+ Assert.stub(Body, :new).with(@env, @asset_file){ @body }
78
+
79
+ content_range = Factory.string
80
+ Assert.stub(@body, :content_range){ content_range }
81
+ Assert.stub(@body, :partial?){ true }
82
+
83
+ @response = Dassets::Server::Response.new(@env, @asset_file)
84
+ end
85
+
86
+ should "be a partial content response" do
87
+ assert_equal 206, subject.status
88
+
89
+ assert_includes 'Content-Range', subject.headers
90
+ assert_equal @body.content_range, subject.headers['Content-Range']
91
+ end
92
+
93
+ end
94
+
95
+ class BodyTests < UnitTests
96
+ desc "Body"
97
+ setup do
98
+ @body = Body.new(@env, @asset_file)
60
99
  end
100
+ subject{ @body }
101
+
102
+ should have_readers :asset_file, :size, :content_range
103
+ should have_imeths :partial?, :range_begin, :range_end
104
+ should have_imeths :each
105
+
106
+ should "know its chunk size" do
107
+ assert_equal 8192, Body::CHUNK_SIZE
108
+ end
109
+
110
+ should "know its asset file" do
111
+ assert_equal @asset_file, subject.asset_file
112
+ end
113
+
114
+ should "know if it is equal to another body" do
115
+ same_af_same_range = Body.new(@env, @asset_file)
116
+ Assert.stub(same_af_same_range, :range_begin){ subject.range_begin }
117
+ Assert.stub(same_af_same_range, :range_end){ subject.range_end }
118
+ assert_equal same_af_same_range, subject
119
+
120
+ other_af_same_range = Body.new(@env, Dassets['file2.txt'])
121
+ Assert.stub(other_af_same_range, :range_begin){ subject.range_begin }
122
+ Assert.stub(other_af_same_range, :range_end){ subject.range_end }
123
+ assert_not_equal other_af_same_range, subject
124
+
125
+ same_af_other_range = Body.new(@env, @asset_file)
126
+
127
+ Assert.stub(same_af_other_range, :range_begin){ Factory.integer }
128
+ Assert.stub(same_af_other_range, :range_end){ subject.range_end }
129
+ assert_not_equal same_af_other_range, subject
130
+
131
+ Assert.stub(same_af_other_range, :range_begin){ subject.range_begin }
132
+ Assert.stub(same_af_other_range, :range_end){ Factory.integer }
133
+ assert_not_equal same_af_other_range, subject
134
+ end
135
+
136
+ end
137
+
138
+ class BodyIOTests < BodyTests
139
+ setup do
140
+ @min_num_chunks = 3
141
+ @num_chunks = @min_num_chunks + Factory.integer(3)
142
+
143
+ content = 'a' * (@num_chunks * Body::CHUNK_SIZE)
144
+ Assert.stub(@asset_file, :content){ content }
145
+ end
146
+
147
+ end
148
+
149
+ class NonPartialBodyTests < BodyIOTests
150
+ desc "for non/multi/invalid partial content requests"
151
+ setup do
152
+ range = [nil, 'bytes=', 'bytes=0-1,2-3', 'bytes=3-2', 'bytes=abc'].choice
153
+ env = range.nil? ? {} : { 'HTTP_RANGE' => range }
154
+ @body = Body.new(env, @asset_file)
155
+ end
156
+
157
+ should "not be partial" do
158
+ assert_false subject.partial?
159
+ end
160
+
161
+ should "be the full content size" do
162
+ assert_equal @asset_file.size, subject.size
163
+ end
164
+
165
+ should "have no content range" do
166
+ assert_nil subject.content_range
167
+ end
168
+
169
+ should "have the full content size as its range" do
170
+ assert_equal 0, subject.range_begin
171
+ assert_equal subject.size-1, subject.range_end
172
+ end
173
+
174
+ should "chunk the full content when iterated" do
175
+ chunks = []
176
+ subject.each{ |chunk| chunks << chunk }
177
+
178
+ assert_equal @num_chunks, chunks.size
179
+ assert_equal subject.class::CHUNK_SIZE, chunks.first.size
180
+ assert_equal @asset_file.content, chunks.join('')
181
+ end
182
+
183
+ end
184
+
185
+ class PartialBodyTests < BodyIOTests
186
+ desc "for a partial content request"
187
+ setup do
188
+ @start_chunk = Factory.boolean ? 0 : 1
189
+ @partial_begin = @start_chunk * Body::CHUNK_SIZE
190
+ @partial_chunks = @num_chunks - Factory.integer(@min_num_chunks)
191
+ @partial_size = @partial_chunks * Body::CHUNK_SIZE
192
+ @partial_end = @partial_begin + (@partial_size-1)
193
+
194
+ env = { 'HTTP_RANGE' => "bytes=#{@partial_begin}-#{@partial_end}" }
195
+ @body = Body.new(env, @asset_file)
196
+ end
197
+ subject{ @body }
198
+
199
+ should "be partial" do
200
+ assert_true subject.partial?
201
+ end
202
+
203
+ should "be the specified partial size" do
204
+ assert_equal @partial_size, subject.size
205
+ end
206
+
207
+ should "know its content range" do
208
+ exp = "bytes #{@partial_begin}-#{@partial_end}/#{@asset_file.size}"
209
+ assert_equal exp, subject.content_range
210
+ end
211
+
212
+ should "have the know its range" do
213
+ assert_equal @partial_begin, subject.range_begin
214
+ assert_equal @partial_end, subject.range_end
215
+ end
216
+
217
+ should "chunk the range when iterated" do
218
+ chunks = []
219
+ subject.each{ |chunk| chunks << chunk }
61
220
 
62
- protected
221
+ assert_equal @partial_chunks, chunks.size
222
+ assert_equal subject.class::CHUNK_SIZE, chunks.first.size
63
223
 
64
- def file_response(asset_file, env={})
65
- require 'dassets/server/response'
66
- Dassets::Server::Response.new(env, asset_file)
224
+ exp = @asset_file.content[@partial_begin..@partial_end]
225
+ assert_equal exp, chunks.join('')
67
226
  end
68
227
 
69
228
  end
metadata CHANGED
@@ -1,95 +1,110 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: dassets
3
- version: !ruby/object:Gem::Version
4
- version: 0.11.0
3
+ version: !ruby/object:Gem::Version
4
+ hash: 47
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 12
9
+ - 0
10
+ version: 0.12.0
5
11
  platform: ruby
6
- authors:
12
+ authors:
7
13
  - Kelly Redding
8
14
  - Collin Redding
9
15
  autorequire:
10
16
  bindir: bin
11
17
  cert_chain: []
12
- date: 2015-08-19 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: assert
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - "~>"
19
- - !ruby/object:Gem::Version
20
- version: '2.15'
18
+
19
+ date: 2015-11-24 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ hash: 29
28
+ segments:
29
+ - 2
30
+ - 15
31
+ version: "2.15"
21
32
  type: :development
33
+ name: assert
34
+ version_requirements: *id001
22
35
  prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - "~>"
26
- - !ruby/object:Gem::Version
27
- version: '2.15'
28
- - !ruby/object:Gem::Dependency
29
- name: assert-rack-test
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - "~>"
33
- - !ruby/object:Gem::Version
34
- version: '1.0'
36
+ - !ruby/object:Gem::Dependency
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 15
43
+ segments:
44
+ - 1
45
+ - 0
46
+ version: "1.0"
35
47
  type: :development
48
+ name: assert-rack-test
49
+ version_requirements: *id002
36
50
  prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - "~>"
40
- - !ruby/object:Gem::Version
41
- version: '1.0'
42
- - !ruby/object:Gem::Dependency
43
- name: sinatra
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '1.4'
51
+ - !ruby/object:Gem::Dependency
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ hash: 7
58
+ segments:
59
+ - 1
60
+ - 4
61
+ version: "1.4"
49
62
  type: :development
63
+ name: sinatra
64
+ version_requirements: *id003
50
65
  prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: '1.4'
56
- - !ruby/object:Gem::Dependency
57
- name: ns-options
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - "~>"
61
- - !ruby/object:Gem::Version
62
- version: '1.1'
66
+ - !ruby/object:Gem::Dependency
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ hash: 13
73
+ segments:
74
+ - 1
75
+ - 1
76
+ version: "1.1"
63
77
  type: :runtime
78
+ name: ns-options
79
+ version_requirements: *id004
64
80
  prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - "~>"
68
- - !ruby/object:Gem::Version
69
- version: '1.1'
70
- - !ruby/object:Gem::Dependency
71
- name: rack
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - "~>"
75
- - !ruby/object:Gem::Version
76
- version: '1.0'
81
+ - !ruby/object:Gem::Dependency
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ hash: 15
88
+ segments:
89
+ - 1
90
+ - 0
91
+ version: "1.0"
77
92
  type: :runtime
93
+ name: rack
94
+ version_requirements: *id005
78
95
  prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - "~>"
82
- - !ruby/object:Gem::Version
83
- version: '1.0'
84
96
  description: Digest and serve HTML asset files
85
- email:
97
+ email:
86
98
  - kelly@kellyredding.com
87
99
  - collin.redding@me.com
88
100
  executables: []
101
+
89
102
  extensions: []
103
+
90
104
  extra_rdoc_files: []
91
- files:
92
- - ".gitignore"
105
+
106
+ files:
107
+ - .gitignore
93
108
  - Gemfile
94
109
  - LICENSE.txt
95
110
  - README.md
@@ -142,30 +157,39 @@ files:
142
157
  - test/unit/source_tests.rb
143
158
  - tmp/.gitkeep
144
159
  homepage: http://github.com/redding/dassets
145
- licenses:
160
+ licenses:
146
161
  - MIT
147
- metadata: {}
148
162
  post_install_message:
149
163
  rdoc_options: []
150
- require_paths:
164
+
165
+ require_paths:
151
166
  - lib
152
- required_ruby_version: !ruby/object:Gem::Requirement
153
- requirements:
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
154
170
  - - ">="
155
- - !ruby/object:Gem::Version
156
- version: '0'
157
- required_rubygems_version: !ruby/object:Gem::Requirement
158
- requirements:
171
+ - !ruby/object:Gem::Version
172
+ hash: 3
173
+ segments:
174
+ - 0
175
+ version: "0"
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
159
179
  - - ">="
160
- - !ruby/object:Gem::Version
161
- version: '0'
180
+ - !ruby/object:Gem::Version
181
+ hash: 3
182
+ segments:
183
+ - 0
184
+ version: "0"
162
185
  requirements: []
186
+
163
187
  rubyforge_project:
164
- rubygems_version: 2.4.5
188
+ rubygems_version: 1.8.29
165
189
  signing_key:
166
- specification_version: 4
190
+ specification_version: 3
167
191
  summary: Digested asset files
168
- test_files:
192
+ test_files:
169
193
  - test/helper.rb
170
194
  - test/support/app.rb
171
195
  - test/support/app/assets/file1.txt
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 244a7ebdc3e3946a77738679d15461540e10fdf4
4
- data.tar.gz: f9139f0feab58b8ec02fdb7f65741e1066e13968
5
- SHA512:
6
- metadata.gz: d0986dc632093cc8960fb505c67bfec57e760e25c51a5023a307cb628ce45e4bc1939cf24afa756dd81d74a313e214f99671704d5d6ca900c9bfb8eba8a570f1
7
- data.tar.gz: af99765ab02de4783213f347106fe2fff5d863120b0a687f17c84e47a7a66d1e16a1539e00348447bea0ff3d307df5a114fc0bba8de92cdd7045eb9f1a978752