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 CHANGED
@@ -20,4 +20,4 @@ tmp
20
20
  .ruby-version
21
21
  vendor
22
22
  .DS_Store
23
-
23
+ *.tmp
@@ -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
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :development do
7
- gem 'rake'
7
+ gem 'guard-test'
8
8
  gem 'rack-test'
9
+ gem 'rake'
9
10
  end
@@ -0,0 +1,7 @@
1
+ # More info at https://github.com/guard/guard#readme
2
+
3
+ guard :test do
4
+ watch(%r{^test/.+_test\.rb$})
5
+ watch('test/test_helper.rb') { 'test' }
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
7
+ end
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
 
@@ -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
- def initialize(app, asset_root=Rails.public_path)
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 serve?(path_info)
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
- file_path = path_to_file(path_info)
21
- gzipped_file_path = "#{file_path}.gz"
22
- gzipped_file_present = ::File.exists?(gzipped_file_path) && ::File.readable?(gzipped_file_path)
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
- if gzipped_file_present
25
- headers['Vary'] = 'Accept-Encoding'
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 has_static_extension?(path)
110
- path =~ STATIC_EXTENSION_REGEX
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 assert_legal_path(path_info)
114
- raise SecurityError.new('Illegal path requested') if path_info =~ ILLEGAL_PATH_REGEX
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