dassets 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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