qeweney 0.3 → 0.4
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/CHANGELOG.md +4 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +65 -0
- data/lib/qeweney/request_info.rb +9 -0
- data/lib/qeweney/response.rb +84 -4
- data/lib/qeweney/version.rb +1 -1
- data/test/helper.rb +20 -2
- data/test/{test_request.rb → test_request_info.rb} +0 -0
- data/test/test_response.rb +115 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfb8f3a7995ccb0d35e79ecb1cbc9f2556ee4bc5e0bc7d61d3f1278395ea47df
|
4
|
+
data.tar.gz: 04cf0bbb7ebe8a701ab7a7b4e912e461ffc744ece02a1b75ff791eba22036da6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f2d77164b052af1cdae718a2c44ccb3c0313bcc73cc87b637b9a1ccabee6701fecd5b223020b4ca854e74d218fefc0101a6150a4e426a9b5c2f1e41a8d6a951
|
7
|
+
data.tar.gz: 0030e7024d6a42f0301ba515e49f1e8da52e3b65368b515d81b520396a187dc0adf8107b32b8f0d08eb0f62d0e3c6f2f9e34f617a69444211b01b4971d950ed2
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/TODO.md
CHANGED
@@ -32,3 +32,68 @@
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
```
|
35
|
+
|
36
|
+
## Caching
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
req.route do
|
40
|
+
req.on 'assets' do
|
41
|
+
# setting cache to true implies the following:
|
42
|
+
# - etag (calculated from file stat)
|
43
|
+
# - last-modified (from file stat)
|
44
|
+
# - vary: Accept-Encoding
|
45
|
+
|
46
|
+
# before responding, looks at the following headers
|
47
|
+
# if-modified-since: (date from client's cache)
|
48
|
+
# if-none-match: (etag from client's cache)
|
49
|
+
# cache-control: no-cache will prevent caching
|
50
|
+
req.serve_file(path, base_path: STATIC_PATH, cache: true)
|
51
|
+
|
52
|
+
# We can control this manually instead:
|
53
|
+
req.serve_file(path, base_path: STATIC_PATH, cache: {
|
54
|
+
etag: 'blahblah',
|
55
|
+
last_modified: Time.now - 365*86400,
|
56
|
+
vary: 'Accept-Encoding, User-Agent'
|
57
|
+
})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
So, the algorithm:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
def validate_client_cache(path)
|
66
|
+
return false if headers['cache-control'] == 'no-cache'
|
67
|
+
|
68
|
+
stat = File.stat(path)
|
69
|
+
etag = file_stat_to_etag(path, stat)
|
70
|
+
return false if headers['if-none-match'] != etag
|
71
|
+
|
72
|
+
modified = file_stat_to_modified_stamp(stat)
|
73
|
+
return false if headers['if-modified-since'] != modified
|
74
|
+
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
def file_stat_to_etag(path, stat)
|
79
|
+
"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
require 'time'
|
83
|
+
|
84
|
+
def file_stat_to_modified_stamp(stat)
|
85
|
+
stat.mtime.httpdate
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## response compression
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
req.route do
|
93
|
+
req.on 'assets' do
|
94
|
+
# gzip on the fly
|
95
|
+
req.serve_file(path, base_path: STATIC_PATH, gzip: :gzip)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
```
|
data/lib/qeweney/request_info.rb
CHANGED
@@ -61,6 +61,15 @@ module Qeweney
|
|
61
61
|
def forwarded_for
|
62
62
|
@headers['x-forwarded-for']
|
63
63
|
end
|
64
|
+
|
65
|
+
# TODO: should return encodings in client's order of preference (and take
|
66
|
+
# into account q weights)
|
67
|
+
def accept_encoding
|
68
|
+
encoding = @headers['accept-encoding']
|
69
|
+
return [] unless encoding
|
70
|
+
|
71
|
+
encoding.split(',').map { |i| i.strip }
|
72
|
+
end
|
64
73
|
end
|
65
74
|
|
66
75
|
module RequestInfoClassMethods
|
data/lib/qeweney/response.rb
CHANGED
@@ -1,21 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'time'
|
4
|
+
require 'zlib'
|
5
|
+
require 'stringio'
|
6
|
+
|
3
7
|
require_relative 'status'
|
4
8
|
|
5
9
|
module Qeweney
|
10
|
+
module StaticFileCaching
|
11
|
+
class << self
|
12
|
+
def file_stat_to_etag(stat)
|
13
|
+
"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def file_stat_to_last_modified(stat)
|
17
|
+
stat.mtime.httpdate
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
6
22
|
module ResponseMethods
|
7
23
|
def redirect(url, status = Status::FOUND)
|
8
24
|
respond(nil, ':status' => status, 'Location' => url)
|
9
25
|
end
|
10
26
|
|
11
27
|
def serve_file(path, opts)
|
12
|
-
|
13
|
-
|
28
|
+
full_path = file_full_path(path, opts)
|
29
|
+
stat = File.stat(full_path)
|
30
|
+
etag = StaticFileCaching.file_stat_to_etag(stat)
|
31
|
+
last_modified = StaticFileCaching.file_stat_to_last_modified(stat)
|
32
|
+
|
33
|
+
if validate_static_file_cache(etag, last_modified)
|
34
|
+
return respond(nil, {
|
35
|
+
':status' => Status::NOT_MODIFIED,
|
36
|
+
'etag' => etag
|
37
|
+
})
|
14
38
|
end
|
15
|
-
|
39
|
+
|
40
|
+
respond_with_static_file(full_path, etag, last_modified, opts)
|
41
|
+
rescue Errno::ENOENT => e
|
16
42
|
respond(nil, ':status' => Status::NOT_FOUND)
|
17
43
|
end
|
18
44
|
|
45
|
+
def respond_with_static_file(path, etag, last_modified, opts)
|
46
|
+
File.open(path, 'r') do |f|
|
47
|
+
opts = opts.merge(headers: {
|
48
|
+
'etag' => etag,
|
49
|
+
'last-modified' => last_modified,
|
50
|
+
})
|
51
|
+
|
52
|
+
# accept_encoding should return encodings in client's order of preference
|
53
|
+
accept_encoding.each do |encoding|
|
54
|
+
case encoding
|
55
|
+
when 'deflate'
|
56
|
+
return serve_io_deflate(f, opts)
|
57
|
+
when 'gzip'
|
58
|
+
return serve_io_gzip(f, opts)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
serve_io(f, opts)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_static_file_cache(etag, last_modified)
|
66
|
+
if (none_match = headers['if-none-match'])
|
67
|
+
return true if none_match == etag
|
68
|
+
end
|
69
|
+
if (modified_since = headers['if-modified-since'])
|
70
|
+
return true if modified_since == last_modified
|
71
|
+
end
|
72
|
+
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
19
76
|
def file_full_path(path, opts)
|
20
77
|
if (base_path = opts[:base_path])
|
21
78
|
File.join(opts[:base_path], path)
|
@@ -25,7 +82,30 @@ module Qeweney
|
|
25
82
|
end
|
26
83
|
|
27
84
|
def serve_io(io, opts)
|
28
|
-
respond(io.read)
|
85
|
+
respond(io.read, opts[:headers] || {})
|
86
|
+
end
|
87
|
+
|
88
|
+
def serve_io_deflate(io, opts)
|
89
|
+
deflate = Zlib::Deflate.new
|
90
|
+
headers = opts[:headers].merge(
|
91
|
+
'content-encoding' => 'deflate',
|
92
|
+
'vary' => 'Accept-Encoding'
|
93
|
+
)
|
94
|
+
|
95
|
+
respond(deflate.deflate(io.read, Zlib::FINISH), headers)
|
96
|
+
end
|
97
|
+
|
98
|
+
def serve_io_gzip(io, opts)
|
99
|
+
buf = StringIO.new
|
100
|
+
z = Zlib::GzipWriter.new(buf)
|
101
|
+
z << io.read
|
102
|
+
z.flush
|
103
|
+
z.close
|
104
|
+
headers = opts[:headers].merge(
|
105
|
+
'content-encoding' => 'gzip',
|
106
|
+
'vary' => 'Accept-Encoding'
|
107
|
+
)
|
108
|
+
respond(buf.string, headers)
|
29
109
|
end
|
30
110
|
end
|
31
111
|
end
|
data/lib/qeweney/version.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -11,8 +11,26 @@ require 'minitest/autorun'
|
|
11
11
|
require 'minitest/reporters'
|
12
12
|
|
13
13
|
module Qeweney
|
14
|
-
|
15
|
-
|
14
|
+
class MockAdapter
|
15
|
+
attr_reader :calls
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@calls = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(sym, *args)
|
22
|
+
calls << [sym, *args]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.mock(headers = {})
|
27
|
+
Request.new(headers, MockAdapter.new)
|
28
|
+
end
|
29
|
+
|
30
|
+
class Request
|
31
|
+
def response_calls
|
32
|
+
adapter.calls
|
33
|
+
end
|
16
34
|
end
|
17
35
|
end
|
18
36
|
|
File without changes
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class RedirectTest < MiniTest::Test
|
6
|
+
def test_redirect
|
7
|
+
r = Qeweney.mock
|
8
|
+
r.redirect('/foo')
|
9
|
+
|
10
|
+
assert_equal [
|
11
|
+
[:respond, nil, {":status"=>302, "Location"=>"/foo"}]
|
12
|
+
], r.response_calls
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_redirect_wirth_status
|
16
|
+
r = Qeweney.mock
|
17
|
+
r.redirect('/bar', Qeweney::Status::MOVED_PERMANENTLY)
|
18
|
+
|
19
|
+
assert_equal [
|
20
|
+
[:respond, nil, {":status"=>301, "Location"=>"/bar"}]
|
21
|
+
], r.response_calls
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class StaticFileResponeTest < MiniTest::Test
|
26
|
+
def setup
|
27
|
+
@path = File.join(__dir__, 'helper.rb')
|
28
|
+
@stat = File.stat(@path)
|
29
|
+
|
30
|
+
@etag = Qeweney::StaticFileCaching.file_stat_to_etag(@stat)
|
31
|
+
@last_modified = Qeweney::StaticFileCaching.file_stat_to_last_modified(@stat)
|
32
|
+
@content = IO.read(@path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_serve_file
|
36
|
+
r = Qeweney.mock
|
37
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
38
|
+
|
39
|
+
assert_equal [
|
40
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
41
|
+
], r.response_calls
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_serve_file_with_cache_headers
|
45
|
+
r = Qeweney.mock('if-none-match' => @etag)
|
46
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
47
|
+
assert_equal [
|
48
|
+
[:respond, nil, { 'etag' => @etag, ':status' => Qeweney::Status::NOT_MODIFIED }]
|
49
|
+
], r.response_calls
|
50
|
+
|
51
|
+
r = Qeweney.mock('if-modified-since' => @last_modified)
|
52
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
53
|
+
assert_equal [
|
54
|
+
[:respond, nil, { 'etag' => @etag, ':status' => Qeweney::Status::NOT_MODIFIED }]
|
55
|
+
], r.response_calls
|
56
|
+
|
57
|
+
r = Qeweney.mock('if-none-match' => 'foobar')
|
58
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
59
|
+
assert_equal [
|
60
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
61
|
+
], r.response_calls
|
62
|
+
|
63
|
+
r = Qeweney.mock('if-modified-since' => Time.now.httpdate)
|
64
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
65
|
+
assert_equal [
|
66
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
67
|
+
], r.response_calls
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_serve_file_deflate
|
71
|
+
r = Qeweney.mock('accept-encoding' => 'deflate')
|
72
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
73
|
+
|
74
|
+
deflate = Zlib::Deflate.new
|
75
|
+
deflated_content = deflate.deflate(@content, Zlib::FINISH)
|
76
|
+
|
77
|
+
assert_equal [
|
78
|
+
[:respond, deflated_content, {
|
79
|
+
'etag' => @etag,
|
80
|
+
'last-modified' => @last_modified,
|
81
|
+
'vary' => 'Accept-Encoding',
|
82
|
+
'content-encoding' => 'deflate'
|
83
|
+
}]
|
84
|
+
], r.response_calls
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_serve_file_gzip
|
88
|
+
r = Qeweney.mock('accept-encoding' => 'gzip')
|
89
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
90
|
+
|
91
|
+
buf = StringIO.new
|
92
|
+
z = Zlib::GzipWriter.new(buf)
|
93
|
+
z << @content
|
94
|
+
z.flush
|
95
|
+
z.close
|
96
|
+
gzipped_content = buf.string
|
97
|
+
|
98
|
+
assert_equal [
|
99
|
+
[:respond, gzipped_content, {
|
100
|
+
'etag' => @etag,
|
101
|
+
'last-modified' => @last_modified,
|
102
|
+
'vary' => 'Accept-Encoding',
|
103
|
+
'content-encoding' => 'gzip'
|
104
|
+
}]
|
105
|
+
], r.response_calls
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_serve_file_non_existent
|
109
|
+
r = Qeweney.mock
|
110
|
+
r.serve_file('foo.rb', base_path: __dir__)
|
111
|
+
assert_equal [
|
112
|
+
[:respond, nil, { ':status' => Qeweney::Status::NOT_FOUND }]
|
113
|
+
], r.response_calls
|
114
|
+
end
|
115
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qeweney
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.4'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -93,7 +93,8 @@ files:
|
|
93
93
|
- qeweney.gemspec
|
94
94
|
- test/helper.rb
|
95
95
|
- test/run.rb
|
96
|
-
- test/
|
96
|
+
- test/test_request_info.rb
|
97
|
+
- test/test_response.rb
|
97
98
|
homepage: http://github.com/digital-fabric/qeweney
|
98
99
|
licenses:
|
99
100
|
- MIT
|