Permalink
Browse files

Allow Rack::Static to assign HTTP Headers based on rules

  • Loading branch information...
1 parent d084a14 commit 308e57ed54d7923a17b384f9217b7b3bc00f4fbf @thomasklemm thomasklemm committed Aug 31, 2012
View
96 lib/rack/static.rb
@@ -28,19 +28,58 @@ module Rack
# use Rack::Static, :urls => [""], :root => 'public', :index =>
# 'index.html'
#
- # Set a fixed Cache-Control header for all served files:
+ # Set custom HTTP Headers for based on rules:
#
- # use Rack::Static, :root => 'public', :cache_control => 'public'
+ # use Rack::Static, :root => 'public',
+ # :header_rules => {
+ # rule => { header_field => content },
+ # another_rule => { header_field => content }
+ # }
#
- # Set custom HTTP Headers for all served files:
+ # These rules for generating HTTP Headers are shipped along:
+ # 1) Global
+ # :global => Matches every file
#
- # use Rack::Static, :root => 'public', :headers =>
- # {'Cache-Control' => 'public, max-age=31536000',
- # 'Access-Control-Allow-Origin' => '*'}
+ # Ex.:
+ # :header_rules => {
+ # :global => {'Cache-Control' => 'public, max-age=123'}
+ # }
#
- # Note: If both :headers => {'Cache-Control' => 'public, max-age=42'}
- # and :cache_control => 'public, max-age=38' are being provided
- # the :headers setting takes precedence
+ # 2) Folders
+ # '/folder' => Matches all files in a certain folder
+ # '/folder/subfolder' => ...
+ # Note: Provide the folder as a string,
+ # with or without the starting slash
+ #
+ # 3) File Extensions
+ # ['css', 'js'] => Will match all files ending in .css or .js
+ # %w(css js) => ...
+ # Note: Provide the file extensions in an array,
+ # use any ruby syntax you like to set that array up
+ #
+ # 4) Regular Expressions / Regexp
+ # %r{\.(?:css|js)\z} => will match all files ending in .css or .js
+ # /\.(?:eot|ttf|otf|woff|svg)\z/ => will match all files ending
+ # in the most common web font formats
+ #
+ # 5) Shortcuts
+ # There is currently only one shortcut defined.
+ # :fonts => will match all files ending in eot, ttf, otf, woff, svg
+ # using the Regexp stated right above
+ #
+ # Example use:
+ #
+ # use Rack::Static, :root => 'public',
+ # :header_rules => {
+ # # Cache all static files in http caches as well as on the client
+ # :global => { 'Cache-Control' => 'public, max-age=31536000' },
+ # # Provide Web Fonts with Access-Control-Headers
+ # :fonts => { 'Access-Control-Allow-Origin' => '*' }
+ # }
+ #
+ # Note: The rules will be applied in the order they are provided,
+ # thus more special rules further down below can override
+ # general global HTTP header settings
#
class Static
@@ -50,11 +89,12 @@ def initialize(app, options={})
@urls = options[:urls] || ["/favicon.ico"]
@index = options[:index]
root = options[:root] || Dir.pwd
- headers = options[:headers] || {}
- # Allow for legacy :cache_control option
- # while prioritizing :headers => {'Cache-Control' => ''} settings
- headers['Cache-Control'] ||= options[:cache_control] if options[:cache_control]
- @file_server = Rack::File.new(root, headers)
+ @headers = {}
+ @header_rules = options[:header_rules] || {}
+ # Allow for legacy :cache_control option while prioritizing global header_rules setting
+ @header_rules[:global] ||= {}
+ @header_rules[:global]['Cache-Control'] ||= options[:cache_control] if options[:cache_control]
+ @file_server = Rack::File.new(root, @headers)
end
def overwrite_file_path(path)
@@ -74,11 +114,39 @@ def call(env)
if can_serve(path)
env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
+ @path = env["PATH_INFO"]
+ set_headers
@file_server.call(env)
else
@app.call(env)
end
end
+ # Convert header rules to headers
+ def set_headers
+ @header_rules.each do |rule, headers|
+ case rule
+ when :global # Global
+ set_header(headers)
+ when :fonts # Fonts Shortcut
+ set_header(headers) if @path.match(%r{\.(?:ttf|otf|eot|woff|svg)\z})
+ when String # Folder
+ path = ::Rack::Utils.unescape(@path)
+ set_header(headers) if
+ (path.start_with?(rule) || path.start_with?('/' + rule))
+ when Array # Extension/Extensions
+ extensions = rule.join('|')
+ set_header(headers) if @path.match(%r{\.(#{extensions})\z})
+ when Regexp # Flexible Regexp
+ set_header(headers) if @path.match(rule)
+ else
+ end
+ end
+ end
+
+ def set_header(headers)
+ headers.each { |field, content| @headers[field] = content }
+ end
+
end
end
View
1 test/cgi/assets/folder/test.js
@@ -0,0 +1 @@
+### TestFile ###
View
1 test/cgi/assets/fonts/font.eot
@@ -0,0 +1 @@
+### TestFile ###
View
1 test/cgi/assets/images/image.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
1 test/cgi/assets/index.html
@@ -0,0 +1 @@
+### TestFile ###
View
1 test/cgi/assets/javascripts/app.js
@@ -0,0 +1 @@
+### TestFile ###
View
1 test/cgi/assets/stylesheets/app.css
@@ -0,0 +1 @@
+### TestFile ###
View
56 test/spec_static.rb
@@ -70,31 +70,65 @@ def static(app, *args)
res.body.should == "Hello World"
end
- it "supports serving fixed cache-control" do
+ it "supports serving fixed cache-control (legacy option)" do
opts = OPTIONS.merge(:cache_control => 'public')
request = Rack::MockRequest.new(static(DummyApp.new, opts))
res = request.get("/cgi/test")
res.should.be.ok
res.headers['Cache-Control'].should == 'public'
end
- it "supports serving custom http headers" do
- opts = OPTIONS.merge(:headers => {'Cache-Control' => 'public, max-age=42',
- 'Access-Control-Allow-Origin' => '*'})
+ it "allows :header_rules to take priority over fixed cache-control (legacy option)" do
+ opts = OPTIONS.merge(
+ :cache_control => 'public, max-age=38',
+ :header_rules => {
+ :global => {'Cache-Control' => 'public, max-age=42'}
+ })
+
request = Rack::MockRequest.new(static(DummyApp.new, opts))
res = request.get("/cgi/test")
res.should.be.ok
res.headers['Cache-Control'].should == 'public, max-age=42'
- res.headers['Access-Control-Allow-Origin'].should == '*'
end
- it "allows headers hash to take priority over fixed cache-control" do
- opts = OPTIONS.merge(:cache_control => 'public, max-age=38',
- :headers => {'Cache-Control' => 'public, max-age=42'})
+ it "support HTTP header rules" do
+ opts = OPTIONS.merge(:header_rules => {
+ :global => {'Cache-Control' => 'public, max-age=100'},
+ :fonts => {'Cache-Control' => 'public, max-age=200'},
+ %w(png jpg) => {'Cache-Control' => 'public, max-age=300'},
+ 'cgi/assets/folder/' => {'Cache-Control' => 'public, max-age=400'},
+ '/cgi/assets/javascripts' => {'Cache-Control' => 'public, max-age=500'},
+ /\.(css|erb)\z/ => {'Cache-Control' => 'public, max-age=600'},
+ })
request = Rack::MockRequest.new(static(DummyApp.new, opts))
- res = request.get("/cgi/test")
+
+ # Global Headers via :global shortcut
+ res = request.get('/cgi/assets/index.html')
res.should.be.ok
- res.headers['Cache-Control'].should == 'public, max-age=42'
- end
+ res.headers['Cache-Control'].should == 'public, max-age=100'
+
+ # Headers for web fonts via :fonts shortcut
+ res = request.get('/cgi/assets/fonts/font.eot')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=200'
+ # Headers for file extensions via array
+ res = request.get('/cgi/assets/images/image.png')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=300'
+
+ # Headers for files in folder via string
+ res = request.get('/cgi/assets/folder/test.js')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=400'
+
+ res = request.get('/cgi/assets/javascripts/app.js')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=500'
+
+ # Flexible Headers via Regexp
+ res = request.get('/cgi/assets/stylesheets/app.css')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=600'
+ end
end

0 comments on commit 308e57e

Please sign in to comment.