rack-zippy 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -1
- data/Guardfile +7 -0
- data/README.md +56 -3
- data/lib/rack-zippy.rb +34 -82
- data/lib/rack-zippy/asset_compiler.rb +39 -0
- data/lib/rack-zippy/serveable_file.rb +138 -0
- data/lib/rack-zippy/version.rb +1 -1
- data/test/asset_server_test.rb +331 -353
- data/test/null_asset_compiler_test.rb +14 -0
- data/test/public/foo/bar.html +8 -0
- data/test/public/foo/index.html +11 -0
- data/test/public/index.html +8 -0
- data/test/public/thanks.html +1 -0
- data/test/rails_asset_compiler_test.rb +46 -0
- data/test/serveable_file_test.rb +468 -0
- data/test/test_helper.rb +40 -2
- metadata +17 -8
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 2.0.0 / 2014-12-07
|
2
|
+
- Rack Zippy now works cleanly with all Rack apps, including Rails apps ([#23](https://github.com/eliotsykes/rack-zippy/issues/23))
|
3
|
+
- Decomposed AssetServer into AssetServer, ServeableFile, and AssetCompiler classes
|
4
|
+
- Installation notes for non-Rails Rack apps added to README.md
|
5
|
+
- Smarter handling of directory requests to match behaviour of Rails Static middleware ([#15](https://github.com/eliotsykes/rack-zippy/issues/15))
|
6
|
+
- Requests for `/` and `/index` respond with `public/index.html` if present
|
7
|
+
- Requests for `/foo/` and `/foo` respond with first file present out of `public/foo.html`, `public/foo/index.html` (Same behaviour for subdirectories)
|
8
|
+
- Use File.join to build file path ([#34](https://github.com/eliotsykes/rack-zippy/issues/34))
|
9
|
+
- Respond with 404 Not Found instead of raising SecurityError for illegal paths ([#17](https://github.com/eliotsykes/rack-zippy/issues/17))
|
10
|
+
|
1
11
|
## 1.2.1 / 2014-07-09
|
2
12
|
- Use absolute (not relative) path for default asset_root ([#11](https://github.com/eliotsykes/rack-zippy/issues/11))
|
3
13
|
|
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -5,16 +5,21 @@ on Heroku if you want to serve the precompiled gzipped assets to gzip-capable cl
|
|
5
5
|
|
6
6
|
By default, Rails + Heroku will not serve *.gz assets even though they are generated at deploy time.
|
7
7
|
|
8
|
-
rack-zippy replaces the ActionDispatch::Static middleware used by Rails, which is not capable of serving the gzipped assets created by
|
8
|
+
rack-zippy replaces the `ActionDispatch::Static` middleware used by Rails, which is not capable of serving the gzipped assets created by
|
9
9
|
the `rake assets:precompile` task. rack-zippy will serve non-gzipped assets where they are not available or not supported by the
|
10
10
|
requesting client.
|
11
11
|
|
12
|
+
rack-zippy (since 2.0.0) has the same **convenient directory request handling** provided by `ActionDispatch::Static`, which means you can take advantage of this in any rack app:
|
13
|
+
|
14
|
+
- Requests for `/` and `/index` respond with `public/index.html` if present
|
15
|
+
- Requests for `/foo/` and `/foo` respond with first file present out of `public/foo.html`, `public/foo/index.html` (Same behaviour for subdirectories)
|
16
|
+
|
12
17
|
Watch the [Web Dev Break podcast on rack-zippy](http://www.webdevbreak.com/specials/rack-zippy "Faster, friendlier assets with rack-zippy") to see how you can check if your app
|
13
18
|
is currently serving uncompressed assets and how quick it is to setup rack-zippy:
|
14
19
|
|
15
20
|
[ ![Faster, friendlier assets with rack-zippy](/video-player.png "Faster, friendlier assets with rack-zippy") ](http://www.webdevbreak.com/specials/rack-zippy "Faster, friendlier assets with rack-zippy")
|
16
21
|
|
17
|
-
## Installation
|
22
|
+
## Installation in Rails app
|
18
23
|
|
19
24
|
Add this line to your application's Gemfile:
|
20
25
|
|
@@ -36,11 +41,32 @@ Create the file `config/initializers/rack_zippy.rb` and put this line in it:
|
|
36
41
|
|
37
42
|
Now run `rake middleware` at the command line and make sure that `Rack::Zippy::AssetServer` is near the top of the outputted list. ActionDispatch::Static should not be in the list. Nicely done, rack-zippy is now installed in your app.
|
38
43
|
|
44
|
+
## Installation in Rack app (that isn’t a Rails app)
|
45
|
+
|
46
|
+
Add this line to your application's Gemfile:
|
47
|
+
|
48
|
+
gem 'rack-zippy'
|
49
|
+
|
50
|
+
And then execute:
|
51
|
+
|
52
|
+
$ bundle
|
53
|
+
|
54
|
+
In `config.ru`:
|
55
|
+
|
56
|
+
require 'rack-zippy'
|
57
|
+
|
58
|
+
# Set asset_root to an absolute or relative path to the directory holding your asset files
|
59
|
+
# e.g. '/path/to/my/apps/static-assets' or 'public'
|
60
|
+
asset_root = '/path/to/my/apps/public'
|
61
|
+
use Rack::Zippy::AssetServer, asset_root
|
62
|
+
|
63
|
+
|
39
64
|
## Usage
|
40
65
|
|
41
66
|
Follow the installation instructions above and rack-zippy will serve any static assets, including gzipped assets, from your
|
42
67
|
application's public/ directory and will respond with sensible caching headers.
|
43
68
|
|
69
|
+
|
44
70
|
## Troubleshooting
|
45
71
|
|
46
72
|
##### 'assert_index': No such middleware to insert before: ActionDispatch::Static (RuntimeError)
|
@@ -49,6 +75,7 @@ Check your environment (in config/environments/) does not have `serve_static_ass
|
|
49
75
|
|
50
76
|
config.serve_static_assets = false # Oops! Should be set to true for rack-zippy
|
51
77
|
|
78
|
+
|
52
79
|
## Contributing
|
53
80
|
|
54
81
|
1. Fork it
|
@@ -58,6 +85,31 @@ Check your environment (in config/environments/) does not have `serve_static_ass
|
|
58
85
|
5. Push to the branch (`git push origin my-new-feature`)
|
59
86
|
6. Create new Pull Request
|
60
87
|
|
88
|
+
### Optional for contributors
|
89
|
+
To try a local branch of rack-zippy out as the gem dependency in a local app, configure bundler with a local gem
|
90
|
+
override as follows:
|
91
|
+
|
92
|
+
In `your-app/Gemfile`: edit the rack-zippy dependency to the following:
|
93
|
+
|
94
|
+
# The branch your-local-branch-name **must** exist otherwise bundler will shout obscenities at you
|
95
|
+
gem 'rack-zippy', :github => 'eliotsykes/rack-zippy', :branch => 'your-local-branch-name'
|
96
|
+
|
97
|
+
At the command line, inside `your-app`, configure bundler to set a local git repo to override the one we specified in the previous step for rack-zippy:
|
98
|
+
|
99
|
+
$> bundle config --local local.rack-zippy /path/to/your/local/rack-zippy
|
100
|
+
|
101
|
+
Now when you run your-app **with** `bundle exec`, the rack-zippy gem dependency will resolve to `/path/to/your/local/rack-zippy`.
|
102
|
+
|
103
|
+
Cleanup time! When you’re finished testing, delete the local override and set your Gemfile dependency back to the original:
|
104
|
+
|
105
|
+
# At the command line:
|
106
|
+
$> bundle config --delete local.rack-zippy
|
107
|
+
|
108
|
+
# In your-app/Gemfile change rack-zippy dependency to this (or similar):
|
109
|
+
gem 'rack-zippy', '~> 9.8.7' # Replace 9.8.7 with the rack-zippy release version you want to use.
|
110
|
+
|
111
|
+
|
112
|
+
|
61
113
|
## Contributors
|
62
114
|
|
63
115
|
- [Eliot Sykes](https://github.com/eliotsykes)
|
@@ -72,5 +124,6 @@ Check your environment (in config/environments/) does not have `serve_static_ass
|
|
72
124
|
3. Tests pass? (`rake test`)
|
73
125
|
4. Build the gem (`rake build`)
|
74
126
|
5. Release on rubygems.org (`rake release`)
|
75
|
-
6. Update version to the next pre-release version in lib/rack-zippy/version.rb, e.g. '1.0.1' becomes '1.0.2.pre'
|
127
|
+
6. Update version to the next pre-release version in lib/rack-zippy/version.rb, e.g. '1.0.1' becomes '1.0.2.pre'.
|
128
|
+
7. Commit and push the updated lib/rack-zippy/version.rb.
|
76
129
|
|
data/lib/rack-zippy.rb
CHANGED
@@ -1,39 +1,47 @@
|
|
1
1
|
require 'rack-zippy/version'
|
2
|
+
require 'rack-zippy/asset_compiler'
|
3
|
+
require 'rack-zippy/serveable_file'
|
2
4
|
|
3
5
|
module Rack
|
4
6
|
module Zippy
|
7
|
+
|
8
|
+
PRECOMPILED_ASSETS_SUBDIR_REGEX = /\A\/assets(?:\/|\z)/
|
9
|
+
|
5
10
|
class AssetServer
|
6
11
|
|
7
|
-
|
12
|
+
# Font extensions: woff, woff2, ttf, eot, otf
|
13
|
+
STATIC_EXTENSION_REGEX = /\.(?:css|js|html|htm|txt|ico|png|jpg|jpeg|gif|pdf|svg|zip|gz|eps|psd|ai|woff|woff2|ttf|eot|otf|swf)\z/i
|
14
|
+
|
15
|
+
HTTP_STATUS_CODE_OK = 200
|
16
|
+
|
17
|
+
def initialize(app, asset_root=nil)
|
18
|
+
if asset_root.nil?
|
19
|
+
if RailsAssetCompiler.rails_env?
|
20
|
+
asset_root = ::Rails.public_path
|
21
|
+
else
|
22
|
+
raise ArgumentError.new 'Please specify asset_root when initializing Rack::Zippy::AssetServer ' +
|
23
|
+
'(asset_root is the path to your public directory, often the one with favicon.ico in it)'
|
24
|
+
end
|
25
|
+
end
|
8
26
|
@app = app
|
9
27
|
@asset_root = asset_root
|
28
|
+
@asset_compiler = resolve_asset_compiler
|
10
29
|
end
|
11
30
|
|
12
31
|
def call(env)
|
13
32
|
path_info = env['PATH_INFO']
|
14
|
-
assert_legal_path path_info
|
15
33
|
|
16
|
-
if
|
17
|
-
headers = { 'Content-Type' => Rack::Mime.mime_type(::File.extname(path_info)) }
|
18
|
-
headers.merge! cache_headers(path_info)
|
34
|
+
return not_found_response if path_info =~ ILLEGAL_PATH_REGEX
|
19
35
|
|
20
|
-
|
21
|
-
|
22
|
-
|
36
|
+
serveable_file = ServeableFile.find_first(
|
37
|
+
:path_info => path_info,
|
38
|
+
:asset_root => @asset_root,
|
39
|
+
:asset_compiler => @asset_compiler,
|
40
|
+
:include_gzipped => client_accepts_gzip?(env)
|
41
|
+
)
|
23
42
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if client_accepts_gzip?(env)
|
28
|
-
file_path = gzipped_file_path
|
29
|
-
headers['Content-Encoding'] = 'gzip'
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
status = 200
|
34
|
-
headers['Content-Length'] = ::File.size(file_path).to_s
|
35
|
-
response_body = [::File.read(file_path)]
|
36
|
-
return [status, headers, response_body]
|
43
|
+
if serveable_file
|
44
|
+
return [HTTP_STATUS_CODE_OK, serveable_file.headers, serveable_file.response_body]
|
37
45
|
end
|
38
46
|
|
39
47
|
@app.call(env)
|
@@ -41,77 +49,21 @@ module Rack
|
|
41
49
|
|
42
50
|
private
|
43
51
|
|
44
|
-
SECONDS_IN = {
|
45
|
-
:day => 24*60*60,
|
46
|
-
:month => 31*(24*60*60),
|
47
|
-
:year => 365*(24*60*60)
|
48
|
-
}.freeze
|
49
|
-
|
50
|
-
# Font extensions: woff, woff2, ttf, eot, otf
|
51
|
-
STATIC_EXTENSION_REGEX = /\.(?:css|js|html|htm|txt|ico|png|jpg|jpeg|gif|pdf|svg|zip|gz|eps|psd|ai|woff|woff2|ttf|eot|otf|swf)\z/i
|
52
|
-
|
53
|
-
PRECOMPILED_ASSETS_SUBDIR_REGEX = /\A\/assets(?:\/|\z)/
|
54
|
-
|
55
52
|
ACCEPTS_GZIP_REGEX = /\bgzip\b/
|
56
53
|
|
57
54
|
ILLEGAL_PATH_REGEX = /(\.\.|\/\.)/
|
58
55
|
|
59
|
-
# Old last-modified headers encourage caching via browser heuristics. Use it for year-long cached assets.
|
60
|
-
CACHE_FRIENDLY_LAST_MODIFIED = 'Mon, 10 Jan 2005 10:00:00 GMT'
|
61
|
-
|
62
|
-
def cache_headers(path_info)
|
63
|
-
case path_info
|
64
|
-
when PRECOMPILED_ASSETS_SUBDIR_REGEX
|
65
|
-
lifetime = :year
|
66
|
-
last_modified = CACHE_FRIENDLY_LAST_MODIFIED
|
67
|
-
when '/favicon.ico'
|
68
|
-
lifetime = :month
|
69
|
-
last_modified = CACHE_FRIENDLY_LAST_MODIFIED
|
70
|
-
else
|
71
|
-
lifetime = :day
|
72
|
-
end
|
73
|
-
|
74
|
-
headers = { 'Cache-Control' => "public, max-age=#{SECONDS_IN[lifetime]}" }
|
75
|
-
headers['Last-Modified'] = last_modified if last_modified
|
76
|
-
|
77
|
-
return headers
|
78
|
-
end
|
79
|
-
|
80
|
-
def path_to_file(path_info)
|
81
|
-
"#{@asset_root}#{path_info}"
|
82
|
-
end
|
83
|
-
|
84
|
-
def serve?(path_info)
|
85
|
-
should_serve_from_filesystem = false
|
86
|
-
|
87
|
-
if has_static_extension?(path_info)
|
88
|
-
file_path = path_to_file(path_info)
|
89
|
-
is_serveable = ::File.file?(file_path) && ::File.readable?(file_path)
|
90
|
-
|
91
|
-
if is_serveable
|
92
|
-
is_outside_assets_dir = !(path_info =~ PRECOMPILED_ASSETS_SUBDIR_REGEX)
|
93
|
-
should_serve_from_filesystem = is_outside_assets_dir || block_asset_pipeline_from_generating_asset?
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
return should_serve_from_filesystem
|
98
|
-
end
|
99
|
-
|
100
|
-
def block_asset_pipeline_from_generating_asset?
|
101
|
-
# config.assets.compile is normally false in production, and true in dev+test envs.
|
102
|
-
!::Rails.configuration.assets.compile
|
103
|
-
end
|
104
|
-
|
105
56
|
def client_accepts_gzip?(rack_env)
|
106
57
|
rack_env['HTTP_ACCEPT_ENCODING'] =~ ACCEPTS_GZIP_REGEX
|
107
58
|
end
|
108
59
|
|
109
|
-
def
|
110
|
-
|
60
|
+
def resolve_asset_compiler
|
61
|
+
asset_compiler_class = RailsAssetCompiler.rails_env? ? RailsAssetCompiler : NullAssetCompiler
|
62
|
+
return asset_compiler_class.new
|
111
63
|
end
|
112
64
|
|
113
|
-
def
|
114
|
-
|
65
|
+
def not_found_response
|
66
|
+
[404, {}, ['Not Found']]
|
115
67
|
end
|
116
68
|
|
117
69
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Rack
|
2
|
+
module Zippy
|
3
|
+
|
4
|
+
class NullAssetCompiler
|
5
|
+
def compiles?(path_info)
|
6
|
+
return false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class RailsAssetCompiler
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
# config.assets.compile is normally false in production, and true in dev+test envs.
|
14
|
+
# compile == true => active pipeline
|
15
|
+
# compile == false => disabled pipeline
|
16
|
+
@active = ::Rails.configuration.assets.compile
|
17
|
+
end
|
18
|
+
|
19
|
+
def compiles?(path_info)
|
20
|
+
return active? && on_pipeline_path?(path_info)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def on_pipeline_path?(path_info)
|
26
|
+
path_info =~ PRECOMPILED_ASSETS_SUBDIR_REGEX
|
27
|
+
end
|
28
|
+
|
29
|
+
def active?
|
30
|
+
return @active
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.rails_env?
|
34
|
+
return defined?(::Rails.version)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Rack
|
2
|
+
module Zippy
|
3
|
+
class ServeableFile
|
4
|
+
|
5
|
+
attr_reader :path, :full_path_info
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
raise ArgumentError.new(':has_encoding_variants option must be given') unless options.has_key?(:has_encoding_variants)
|
9
|
+
|
10
|
+
@path = options[:path]
|
11
|
+
@full_path_info = options[:full_path_info]
|
12
|
+
@has_encoding_variants = options[:has_encoding_variants]
|
13
|
+
@is_gzipped = options[:is_gzipped]
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers
|
17
|
+
headers = { 'Content-Type' => Rack::Mime.mime_type(::File.extname(full_path_info)) }
|
18
|
+
headers.merge! cache_headers
|
19
|
+
|
20
|
+
headers['Vary'] = 'Accept-Encoding' if encoding_variants?
|
21
|
+
headers['Content-Encoding'] = 'gzip' if gzipped?
|
22
|
+
|
23
|
+
headers['Content-Length'] = ::File.size(path).to_s
|
24
|
+
return headers
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_headers
|
28
|
+
case full_path_info
|
29
|
+
when PRECOMPILED_ASSETS_SUBDIR_REGEX
|
30
|
+
lifetime = :year
|
31
|
+
last_modified = CACHE_FRIENDLY_LAST_MODIFIED
|
32
|
+
when '/favicon.ico'
|
33
|
+
lifetime = :month
|
34
|
+
last_modified = CACHE_FRIENDLY_LAST_MODIFIED
|
35
|
+
else
|
36
|
+
lifetime = :day
|
37
|
+
end
|
38
|
+
|
39
|
+
headers = { 'Cache-Control' => "public, max-age=#{SECONDS_IN[lifetime]}" }
|
40
|
+
headers['Last-Modified'] = last_modified if last_modified
|
41
|
+
|
42
|
+
return headers
|
43
|
+
end
|
44
|
+
|
45
|
+
def response_body
|
46
|
+
[::File.read(path)]
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.find_first(options)
|
50
|
+
asset_compiler = options[:asset_compiler]
|
51
|
+
path_info = options[:path_info].chomp('/')
|
52
|
+
|
53
|
+
return nil if asset_compiler.compiles?(path_info)
|
54
|
+
|
55
|
+
asset_root = options[:asset_root]
|
56
|
+
|
57
|
+
candidate_path_infos = []
|
58
|
+
if !path_info.empty?
|
59
|
+
candidate_path_infos << path_info
|
60
|
+
candidate_path_infos << "#{path_info}#{DEFAULT_STATIC_EXTENSION}"
|
61
|
+
end
|
62
|
+
candidate_path_infos << "#{path_info}/index#{DEFAULT_STATIC_EXTENSION}"
|
63
|
+
|
64
|
+
file_path = nil
|
65
|
+
|
66
|
+
full_path_info = candidate_path_infos.find do |candidate_path_info|
|
67
|
+
file_path = ::File.join(asset_root, candidate_path_info)
|
68
|
+
readable_file?(file_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
return nil if full_path_info.nil? || !has_static_extension?(full_path_info)
|
72
|
+
|
73
|
+
include_gzipped = options[:include_gzipped]
|
74
|
+
|
75
|
+
gzipped_file_path = "#{file_path}.gz"
|
76
|
+
gzipped_file_present = readable_file?(gzipped_file_path)
|
77
|
+
|
78
|
+
has_encoding_variants = gzipped_file_present
|
79
|
+
|
80
|
+
if include_gzipped && gzipped_file_present
|
81
|
+
return ServeableFile.new(
|
82
|
+
:path => gzipped_file_path,
|
83
|
+
:full_path_info => full_path_info,
|
84
|
+
:has_encoding_variants => has_encoding_variants,
|
85
|
+
:is_gzipped => true
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
return ServeableFile.new(
|
90
|
+
:path => file_path,
|
91
|
+
:full_path_info => full_path_info,
|
92
|
+
:has_encoding_variants => has_encoding_variants
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.has_static_extension?(path)
|
97
|
+
path =~ AssetServer::STATIC_EXTENSION_REGEX
|
98
|
+
end
|
99
|
+
|
100
|
+
def encoding_variants?
|
101
|
+
return @has_encoding_variants
|
102
|
+
end
|
103
|
+
|
104
|
+
def gzipped?
|
105
|
+
return @is_gzipped
|
106
|
+
end
|
107
|
+
|
108
|
+
def ==(other)
|
109
|
+
return false if other.nil?
|
110
|
+
return true if self.equal?(other)
|
111
|
+
return self.class == other.class &&
|
112
|
+
self.gzipped? == other.gzipped? &&
|
113
|
+
self.encoding_variants? == other.encoding_variants? &&
|
114
|
+
self.path == other.path &&
|
115
|
+
self.full_path_info == other.full_path_info
|
116
|
+
end
|
117
|
+
alias_method :eql?, :==
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Old last-modified headers encourage caching via browser heuristics. Use it for year-long cached assets.
|
122
|
+
CACHE_FRIENDLY_LAST_MODIFIED = 'Mon, 10 Jan 2005 10:00:00 GMT'
|
123
|
+
|
124
|
+
SECONDS_IN = {
|
125
|
+
:day => 24*60*60,
|
126
|
+
:month => 31*(24*60*60),
|
127
|
+
:year => 365*(24*60*60)
|
128
|
+
}.freeze
|
129
|
+
|
130
|
+
DEFAULT_STATIC_EXTENSION = '.html'.freeze
|
131
|
+
|
132
|
+
def self.readable_file?(file_path)
|
133
|
+
return ::File.file?(file_path) && ::File.readable?(file_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|