rack-zippy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea/*
19
+ *.iml
20
+ .ruby-version
21
+ vendor
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 / 2013-08-21
2
+
3
+ Initial release
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-zippy.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rake'
8
+ gem 'rack-test'
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Eliot Sykes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # rack-zippy
2
+
3
+ rack-zippy is a Rack middleware for serving static gzipped assets precompiled by the Rails asset pipeline into the public/assets directory. Use it
4
+ on Heroku if you want to serve the precompiled gzipped assets to gzip-capable clients with sensible caching headers.
5
+
6
+ By default, Rails + Heroku will not serve *.gz assets even though they are generated at deploy time.
7
+
8
+ rack-zippy replaces the ActionDispatch::Static middleware used by Rails, which is not capable of serving the gzipped assets created by
9
+ the `rake assets:precompile` task. rack-zippy will serve non-gzipped assets where they are not available or not supported by the
10
+ requesting client.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'rack-zippy'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Add this line to config/application.rb:
23
+
24
+ config.middleware.swap(ActionDispatch::Static, Rack::Zippy::AssetServer)
25
+
26
+ ## Usage
27
+
28
+ Follow the installation instructions above and rack-zippy will serve any static assets, including gzipped assets, from your
29
+ application's public/ directory and will respond with sensible caching headers.
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Run tests (`rake test`)
36
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
39
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.pattern = 'test/**/*_test.rb'
6
+ test.verbose = true
7
+ end
data/lib/rack-zippy.rb ADDED
@@ -0,0 +1,108 @@
1
+ require "rack-zippy/version"
2
+
3
+ module Rack
4
+ module Zippy
5
+ class AssetServer
6
+
7
+ def initialize(app, asset_root='public')
8
+ @app = app
9
+ @asset_root = asset_root
10
+ end
11
+
12
+ def call(env)
13
+ path_info = env['PATH_INFO']
14
+ assert_legal_path path_info
15
+
16
+ if serve?(path_info)
17
+
18
+ file_path = "#{@asset_root}#{path_info}"
19
+
20
+ if ::File.exists?(file_path)
21
+ headers = {
22
+ 'Content-Type' => Rack::Mime.mime_type(::File.extname(path_info)),
23
+ 'Last-Modified' => 'Mon, 10 Jan 2005 10:00:00 GMT',
24
+ 'Cache-Control' => "public, max-age=#{max_age_in_secs(path_info)}"
25
+ }
26
+
27
+ gzipped_file_path = "#{file_path}.gz"
28
+ gzipped_file_present = ::File.exists?(gzipped_file_path)
29
+
30
+ if gzipped_file_present
31
+ headers['Vary'] = 'Accept-Encoding'
32
+
33
+ if client_accepts_gzip?(env)
34
+ file_path = gzipped_file_path
35
+ headers['Content-Encoding'] = 'gzip'
36
+ end
37
+ end
38
+
39
+ status = 200
40
+ headers['Content-Length'] = ::File.size(file_path).to_s
41
+ response_body = [::File.read(file_path)]
42
+ else
43
+ status = 404
44
+ headers = {}
45
+ response_body = ['Not Found']
46
+ end
47
+ return [status, headers, response_body]
48
+ end
49
+
50
+ @app.call(env)
51
+ end
52
+
53
+ private
54
+
55
+ SECONDS_IN = {
56
+ :day => 24*60*60,
57
+ :month => 31*(24*60*60),
58
+ :year => 365*(24*60*60)
59
+ }.freeze
60
+
61
+ STATIC_EXTENSION_REGEX = /\.(?:css|js|html|htm|txt|ico|png|jpg|jpeg|gif|pdf|svg|zip|gz|eps|psd|ai)\z/i
62
+
63
+ PRECOMPILED_ASSETS_SUBDIR_REGEX = /\A\/assets\//
64
+
65
+ ACCEPTS_GZIP_REGEX = /\bgzip\b/
66
+
67
+ ILLEGAL_PATH_REGEX = /(\.\.|\/\.)/
68
+
69
+ def serve?(path_info)
70
+ is_compilable_asset = (path_info =~ PRECOMPILED_ASSETS_SUBDIR_REGEX)
71
+ if is_compilable_asset
72
+ return should_assets_be_compiled_already?
73
+ end
74
+ return has_static_extension?(path_info)
75
+ end
76
+
77
+ def should_assets_be_compiled_already?
78
+ !::Rails.configuration.assets.compile
79
+ end
80
+
81
+ def max_age_in_secs(path_info)
82
+ case path_info
83
+ when PRECOMPILED_ASSETS_SUBDIR_REGEX
84
+ max_age = SECONDS_IN[:year]
85
+ when '/favicon.ico'
86
+ max_age = SECONDS_IN[:month]
87
+ else
88
+ max_age = SECONDS_IN[:day]
89
+ end
90
+
91
+ return max_age
92
+ end
93
+
94
+ def client_accepts_gzip?(rack_env)
95
+ rack_env['HTTP_ACCEPT_ENCODING'] =~ ACCEPTS_GZIP_REGEX
96
+ end
97
+
98
+ def has_static_extension?(path)
99
+ path =~ STATIC_EXTENSION_REGEX
100
+ end
101
+
102
+ def assert_legal_path(path_info)
103
+ raise SecurityError.new('Illegal path requested') if path_info =~ ILLEGAL_PATH_REGEX
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module Zippy
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack-zippy/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rack-zippy"
8
+ gem.version = Rack::Zippy::VERSION
9
+ gem.authors = ["Eliot Sykes"]
10
+ gem.email = ["e@jetbootlabs.com"]
11
+ gem.description = %q{Rack middleware for serving static gzipped assets generated by the Rails asset pipeline}
12
+ gem.summary = %q{Rack middleware for serving static gzipped assets generated by the Rails asset pipeline}
13
+ gem.homepage = "https://github.com/eliotsykes/rack-zippy"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,283 @@
1
+ require_relative 'test_helper'
2
+
3
+ class Rack::Zippy::AssetServerTest < Test::Unit::TestCase
4
+ include Rack::Test::Methods
5
+
6
+ def setup
7
+ ensure_correct_working_directory
8
+ ::Rails.configuration.assets.compile = false
9
+ end
10
+
11
+ def teardown
12
+ revert_to_original_working_directory
13
+ end
14
+
15
+ def app
16
+ response = 'Up above the streets and houses'
17
+ headers = {}
18
+ status = 200
19
+ rack_app = lambda { |env| [status, headers, response] }
20
+ Rack::Zippy::AssetServer.new(rack_app)
21
+ end
22
+
23
+ def test_does_not_serve_assets_subdir_request_when_assets_compile_enabled
24
+ ::Rails.configuration.assets.compile = true
25
+ get '/assets/application.css'
26
+ assert_response_ok
27
+ assert_equal 'Up above the streets and houses', last_response.body
28
+ end
29
+
30
+ def test_serve_returns_true_if_request_has_static_extension
31
+ assert app.send(:serve?, '/about.html')
32
+ end
33
+
34
+ def test_serve_returns_false_if_request_does_not_have_static_extension
35
+ assert !app.send(:serve?, '/about')
36
+ end
37
+
38
+ def test_serve_returns_true_for_assets_subdir_request_when_assets_compile_disabled
39
+ assert app.send(:serve?, '/assets/application.css')
40
+ end
41
+
42
+ def test_serve_returns_false_for_assets_subdir_request_when_assets_compile_enabled
43
+ ::Rails.configuration.assets.compile = true
44
+ assert !app.send(:serve?, '/assets/application.css')
45
+ end
46
+
47
+ def test_should_assets_be_compiled_already_returns_false_if_assets_compile_enabled
48
+ ::Rails.configuration.assets.compile = true
49
+ assert ::Rails.configuration.assets.compile
50
+ assert !app.send(:should_assets_be_compiled_already?)
51
+ end
52
+
53
+ def test_should_assets_be_compiled_already_returns_true_if_assets_compile_disabled
54
+ assert !::Rails.configuration.assets.compile
55
+ assert app.send(:should_assets_be_compiled_already?)
56
+ end
57
+
58
+ def test_responds_with_gzipped_css_to_gzip_capable_clients
59
+ params = {}
60
+ get '/assets/application.css', params, env_for_gzip_capable_client
61
+ assert_response_ok
62
+ assert_content_length 'public/assets/application.css.gz'
63
+ assert_content_type 'text/css'
64
+ assert_cache_max_age :year
65
+ assert_cache_friendly_last_modified
66
+ assert_equal 'gzip', last_response.headers['content-encoding']
67
+ assert_vary_accept_encoding_header
68
+ end
69
+
70
+ def test_responds_with_gzipped_js_to_gzip_capable_clients
71
+ params = {}
72
+ get '/assets/application.js', params, env_for_gzip_capable_client
73
+ assert_response_ok
74
+ assert_content_length 'public/assets/application.js.gz'
75
+ assert_content_type 'application/javascript'
76
+ assert_cache_max_age :year
77
+ assert_cache_friendly_last_modified
78
+ assert_equal 'gzip', last_response.headers['content-encoding']
79
+ assert_vary_accept_encoding_header
80
+ end
81
+
82
+ def test_responds_with_maximum_cache_headers_for_assets_subdir_requests
83
+ get '/assets/favicon.ico'
84
+ assert_response_ok
85
+ assert_cache_max_age :year
86
+ assert_cache_friendly_last_modified
87
+ end
88
+
89
+ def test_responds_with_month_long_cache_headers_for_root_favicon
90
+ get '/favicon.ico'
91
+ assert_response_ok
92
+ assert_cache_max_age :month
93
+ assert_cache_friendly_last_modified
94
+ end
95
+
96
+ def test_responds_with_day_long_cache_headers_for_robots_txt
97
+ get '/robots.txt'
98
+ assert_response_ok
99
+ assert_cache_max_age :day
100
+ assert_cache_friendly_last_modified
101
+ end
102
+
103
+ def test_responds_with_day_long_cache_headers_for_root_html_requests
104
+ get '/thanks.html'
105
+ assert_response_ok
106
+ assert_cache_max_age :day
107
+ assert_cache_friendly_last_modified
108
+ end
109
+
110
+ def test_max_cache_and_vary_accept_encoding_headers_present_for_css_requests_by_non_gzip_clients
111
+ get '/assets/application.css'
112
+ assert_response_ok
113
+ assert_content_length 'public/assets/application.css'
114
+ assert_content_type 'text/css'
115
+ assert_cache_max_age :year
116
+ assert_cache_friendly_last_modified
117
+ assert_nil last_response.headers['content-encoding']
118
+ assert_vary_accept_encoding_header
119
+ end
120
+
121
+ def test_max_cache_and_vary_accept_encoding_headers_present_for_js_requests_by_non_gzip_clients
122
+ get '/assets/application.js'
123
+ assert_response_ok
124
+ assert_content_type 'application/javascript'
125
+ assert_content_length 'public/assets/application.js'
126
+ assert_cache_max_age :year
127
+ assert_cache_friendly_last_modified
128
+ assert_nil last_response.headers['content-encoding']
129
+ assert_vary_accept_encoding_header
130
+ end
131
+
132
+ def test_vary_header_not_present_if_gzipped_asset_unavailable
133
+ get '/assets/rails.png'
134
+ assert_response_ok
135
+ assert_nil last_response.headers['vary']
136
+ end
137
+
138
+ def assert_raises_illegal_path_error(path)
139
+ e = assert_raises SecurityError do
140
+ get path
141
+ end
142
+ assert_equal 'Illegal path requested', e.message
143
+ end
144
+
145
+ def test_throws_exception_if_path_contains_hidden_dir
146
+ paths = ['.confidential/secret-plans.pdf', '/.you-aint-seen-me/index.html', '/nothing/.to/see/here.jpg']
147
+ paths.each do |path|
148
+ assert_raises_illegal_path_error path
149
+ end
150
+ end
151
+
152
+ def test_throws_exception_if_path_ends_with_hidden_file
153
+ hidden_files = ['.htaccess', '/.top-secret', '/assets/.shhh']
154
+ hidden_files.each do |path|
155
+ assert_raises_illegal_path_error path
156
+ end
157
+ end
158
+
159
+ def test_throws_exception_if_path_contains_consecutive_periods
160
+ assert_raises_illegal_path_error '/hello/../sensitive/file'
161
+ end
162
+
163
+ def test_serves_html
164
+ get '/thanks.html'
165
+ assert_response_ok
166
+ assert_content_type 'text/html'
167
+ assert_content_length 'public/thanks.html'
168
+ end
169
+
170
+ def test_serves_robots_txt
171
+ get '/robots.txt'
172
+ assert_response_ok
173
+ assert_content_type 'text/plain'
174
+ assert_content_length 'public/robots.txt'
175
+ end
176
+
177
+ def test_has_static_extension_handles_non_lowercase_chars
178
+ ['pNG', 'JPEG', 'HTML', 'HtM', 'GIF', 'Ico'].each do |extension|
179
+ assert app.send(:has_static_extension?, "/some-asset.#{extension}")
180
+ end
181
+ end
182
+
183
+ def test_has_static_extension_returns_false_for_asset_paths_without_period
184
+ ['/assets/somepng', '/indexhtml', '/assets/applicationcss'].each do |path|
185
+ assert !app.send(:has_static_extension?, path)
186
+ end
187
+ end
188
+
189
+ def test_passes_non_asset_requests_onto_app
190
+ get '/about'
191
+ assert_underlying_app_responded
192
+ end
193
+
194
+ def test_does_not_pass_not_found_asset_requests_onto_app
195
+ get '/index.html'
196
+ assert_not_found
197
+ end
198
+
199
+ def test_responds_with_favicon_in_assets_dir
200
+ get '/assets/favicon.ico'
201
+ assert_response_ok
202
+ assert_content_type 'image/vnd.microsoft.icon'
203
+ assert_content_length 'public/assets/favicon.ico'
204
+ end
205
+
206
+ def test_responds_with_favicon_at_root
207
+ get '/favicon.ico'
208
+ assert_response_ok
209
+ assert_content_type 'image/vnd.microsoft.icon'
210
+ assert_content_length 'public/favicon.ico'
211
+ end
212
+
213
+ def test_responds_404_not_found_for_non_existent_image
214
+ get '/assets/pot-of-gold.png'
215
+ assert_not_found
216
+ end
217
+
218
+ def test_responds_404_not_found_for_non_existent_css
219
+ get '/assets/unicorn.css'
220
+ assert_not_found
221
+ end
222
+
223
+ def test_responds_404_not_found_for_non_existent_js
224
+ get '/assets/dragon.js'
225
+ assert_not_found
226
+ end
227
+
228
+ private
229
+
230
+ DURATIONS_IN_SECS = {:year => 31536000, :month => 2678400, :day => 86400}.freeze
231
+
232
+ def env_for_gzip_capable_client
233
+ {'HTTP_ACCEPT_ENCODING' => 'deflate,gzip,sdch'}
234
+ end
235
+
236
+ def assert_vary_accept_encoding_header
237
+ assert_equal 'Accept-Encoding', last_response.headers['vary']
238
+ end
239
+
240
+ def assert_cache_max_age(duration)
241
+ assert_equal "public, max-age=#{DURATIONS_IN_SECS[duration]}", last_response.headers['cache-control']
242
+ end
243
+
244
+ # Browsers favour caching assets with older Last Modified dates IIRC
245
+ def assert_cache_friendly_last_modified
246
+ assert_equal 'Mon, 10 Jan 2005 10:00:00 GMT', last_response.headers['last-modified']
247
+ end
248
+
249
+ def assert_underlying_app_responded
250
+ assert_response_ok
251
+ assert_equal 'Up above the streets and houses', last_response.body
252
+ end
253
+
254
+ def assert_response_ok
255
+ assert_equal 200, last_response.status
256
+ end
257
+
258
+ def assert_content_type(expected_content_type)
259
+ assert_equal expected_content_type, last_response.headers['content-type']
260
+ end
261
+
262
+ def assert_content_length(path)
263
+ assert_equal ::File.size(path).to_s, last_response.headers['content-length']
264
+ end
265
+
266
+ def assert_not_found
267
+ assert_equal 404, last_response.status
268
+ assert_equal 'Not Found', last_response.body
269
+ end
270
+
271
+ def ensure_correct_working_directory
272
+ is_project_root_working_directory = File.exists?('rack-zippy.gemspec')
273
+ if is_project_root_working_directory
274
+ @original_dir = Dir.pwd
275
+ Dir.chdir 'test'
276
+ end
277
+ end
278
+
279
+ def revert_to_original_working_directory
280
+ Dir.chdir @original_dir if @original_dir
281
+ end
282
+
283
+ end
@@ -0,0 +1,3 @@
1
+ body {
2
+ font-family: "Comic Sans";
3
+ }
@@ -0,0 +1 @@
1
+ var zippyMessage = 'Hello World!';
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ # This is robots.txt
@@ -0,0 +1,6 @@
1
+ <html>
2
+ <head><title>Hello and thanks</title></head>
3
+ <body>
4
+ <h1>Thanks!</h1>
5
+ </body>
6
+ </html>
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ require 'test/unit'
7
+ require 'rack/test'
8
+
9
+ module Rails
10
+
11
+ @configuration = Struct.new(:assets).new
12
+ @configuration.assets = Struct.new(:compile).new
13
+
14
+ def self.configuration
15
+ @configuration
16
+ end
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-zippy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Eliot Sykes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-21 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Rack middleware for serving static gzipped assets generated by the Rails
15
+ asset pipeline
16
+ email:
17
+ - e@jetbootlabs.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - CHANGELOG.md
24
+ - Gemfile
25
+ - LICENSE
26
+ - README.md
27
+ - Rakefile
28
+ - lib/rack-zippy.rb
29
+ - lib/rack-zippy/version.rb
30
+ - rack-zippy.gemspec
31
+ - test/asset_server_test.rb
32
+ - test/public/assets/application.css
33
+ - test/public/assets/application.css.gz
34
+ - test/public/assets/application.js
35
+ - test/public/assets/application.js.gz
36
+ - test/public/assets/favicon.ico
37
+ - test/public/assets/rails.png
38
+ - test/public/favicon.ico
39
+ - test/public/robots.txt
40
+ - test/public/thanks.html
41
+ - test/test_helper.rb
42
+ homepage: https://github.com/eliotsykes/rack-zippy
43
+ licenses: []
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 1.8.23
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Rack middleware for serving static gzipped assets generated by the Rails
66
+ asset pipeline
67
+ test_files:
68
+ - test/asset_server_test.rb
69
+ - test/public/assets/application.css
70
+ - test/public/assets/application.css.gz
71
+ - test/public/assets/application.js
72
+ - test/public/assets/application.js.gz
73
+ - test/public/assets/favicon.ico
74
+ - test/public/assets/rails.png
75
+ - test/public/favicon.ico
76
+ - test/public/robots.txt
77
+ - test/public/thanks.html
78
+ - test/test_helper.rb