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