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 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