summary refs log tree commit
diff options
context:
space:
mode:
authorThomas Klemm <github@tklemm.eu>2012-08-31 21:24:55 +0200
committerJames Tucker <raggi@google.com>2013-01-04 09:30:59 -0500
commitfb5785294241a07deab95dea565267098fe186d2 (patch)
treefb7d9df71373cedcb169c8ddc64393f47dff6f91
parentceeccb110df7167bb0dfe3fa4e0a12f681211ce5 (diff)
downloadrack-fb5785294241a07deab95dea565267098fe186d2.tar.gz
Allow Rack::Static to assign HTTP Headers based on rules
-rw-r--r--lib/rack/static.rb96
-rw-r--r--test/cgi/assets/folder/test.js1
-rw-r--r--test/cgi/assets/fonts/font.eot1
-rw-r--r--test/cgi/assets/images/image.png1
-rw-r--r--test/cgi/assets/index.html1
-rw-r--r--test/cgi/assets/javascripts/app.js1
-rw-r--r--test/cgi/assets/stylesheets/app.css1
-rw-r--r--test/spec_static.rb56
8 files changed, 133 insertions, 25 deletions
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 28754360..d42e0364 100644
--- a/lib/rack/static.rb
+++ b/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 @@ module Rack
       @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 @@ module Rack
 
       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
diff --git a/test/cgi/assets/folder/test.js b/test/cgi/assets/folder/test.js
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/folder/test.js
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/fonts/font.eot b/test/cgi/assets/fonts/font.eot
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/fonts/font.eot
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/images/image.png b/test/cgi/assets/images/image.png
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/images/image.png
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/index.html b/test/cgi/assets/index.html
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/index.html
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/javascripts/app.js b/test/cgi/assets/javascripts/app.js
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/javascripts/app.js
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/stylesheets/app.css b/test/cgi/assets/stylesheets/app.css
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/stylesheets/app.css
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/spec_static.rb b/test/spec_static.rb
index 5e722e6a..07572309 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -70,7 +70,7 @@ describe Rack::Static do
     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")
@@ -78,23 +78,57 @@ describe Rack::Static do
     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