rack-zippy 0.1.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 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