rack-zippy 1.2.1 → 2.0.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/.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
|
[  ](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
|