Skip to content
Browse files

Replaced the Controller with a piece of Metal:

* removed controller
* removed routing
* added a Metal/Rack middleware to handle the requests instead
* updated README

Many thanks to Daniel Neighman (hassox) for his help converting this to Rack at RailsCamp in Melbourne!
  • Loading branch information...
1 parent dbdae08 commit fdd7343aef83916eb7cc10f539e5890e1b953f3b @justinfrench committed
View
9 README.rdoc
@@ -25,12 +25,12 @@ PluginAssets takes these "view paths" and builds the same thing for "public path
Given a request for "/stylesheets/foo/bah.css", the web server would first look for "bah.css" in "RAILS_ROOT/public/foo/bah.css". If that resource doesn't exist, the request would usually be passed through to Rails, which will try to route the request to a controller action and eventually respond with a 404 (file not found).
-Instead, this plugin sets up a controller and appropriate routing to handle the request, searching for the file in your plugins and gem plugins. In this case, it would search for "bah.css" in the following locations:
+Instead, this plugin intercepts the requests for these missing assets with a piece of middleware, searching through the installed plugins. In this case, it would search for "bah.css" in the following locations:
* RAILS_ROOT/vendor/plugins/plugin_123/public/stylesheets/foo/bah.css
* RAILS_ROOT/vendor/plugins/plugin_456/public/stylesheets/foo/bah.css
-As soon as a matching file is found, it's passed back to the browser with the correct mime type. If none of the plugins have such a file, the regular Rails 404 response for a missing template is invoked.
+As soon as a matching file is found, it's passed back to the browser with the correct mime type. If none of the plugins have such a file, the request is passed onto Rails, which will handle the 404 like it usually does.
It works for plugins and gem plugins. It works for the following URLs:
@@ -43,7 +43,7 @@ One day I'll make it configurable for other assets, but in the meantime it's eas
==Caveats
-PluginAssets leverages the view_paths array used by Rails plugins that supply their own views. Rails plugins and gems are only added to the view_paths array automatically if there's an app/views directory inside the plugin. If you're supplying public assets, chances are pretty good you're supplying views too, so that probably isn't a big deal.
+Publicious leverages the view_paths array used by Rails plugins that supply their own views. Rails plugins and gems are only added to the view_paths array automatically if there's an app/views directory inside the plugin. If you're supplying public assets, chances are pretty good you're supplying views too, so that probably isn't a big deal.
==Isn't it inefficient?
@@ -58,11 +58,10 @@ I wrote this on the train this morning as a proof of concept, so there's a long
* It should be a gem plugin, so that other gem plugins can list it as a dependency
* I haven't written any tests
* I haven't tackled caching yet (probably need to do both page caching and header response caching)
-* Thinking this could be Rack middleware or Rails Metal rather than a full Rails request stack.
I actually want to see this in Rails core, so help me make it awesome!
==Etc
-Publicious is Copyright (c) 2009 Justin French, released under the MIT license. Your feedback, forkings and contributions are greatly welcomed.
+Publicious is Copyright (c) 2009 Justin French, released under the MIT license. Your feedback, forkings and contributions are greatly welcomed. Many thanks to Daniel Neighman (hassox) for his help converting Publicious to Rack middleware rather than a regular Rails routing and controller stack.
View
44 app/controllers/publicious_controller.rb
@@ -1,44 +0,0 @@
-class PubliciousController < ApplicationController
-
- def show
- public_paths.each do |public_path|
- full_file_path = File.join(public_path, asset_directory, File.join(*params[:path]))
- if File.exist?(full_file_path)
- response.headers["Content-Type"] = content_type_for_response
- File.open(full_file_path, 'rb') { |file| render :text => file.read }
- break # break the public_paths loop when we find the first match
- end
- end
- # If the requested file isn't found in any of the paths, ActionView::MissingTemplate will
- # raise here, which results in a 404 in production.
- end
-
- protected
-
- # /stylesheets/foo/bah.css => "css" => "text/css"
- # /images/foo/bah.png => "png" => "image/png"
- # /javascripts/foo/bah.js => "js" => "text/javascript"
- def content_type_for_response
- Mime::Type.lookup_by_extension(params[:path].join("/").split(".").last).to_s
- end
-
- def public_paths
- @public_paths ||= find_public_paths_from_view_paths
- end
-
- # "/stylesheets/foo/bah.css" => "stylesheets"
- # "/images/foo.png" => "images"
- # "/javascripts/a/b/c/d/e.js" => "javascripts"
- def asset_directory
- request.request_uri.split("/").compact.reject(&:blank?).first
- end
-
- # Build an array of public paths (eg vendor/plugins/plugin_a/public) by looping through the
- # view_paths array, replacing app/views with public if the view path is in vendor.
- def find_public_paths_from_view_paths
- view_paths.map { |view_path|
- view_path.to_s.gsub("app/views", "public") if view_path.to_s =~ /vendor/
- }.compact
- end
-
-end
View
49 app/metal/publicious_metal.rb
@@ -0,0 +1,49 @@
+class PubliciousMetal
+
+ def self.call(env)
+ request = Rack::Request.new(env)
+ if request.path_info =~ %r{^\/(#{allowed_dirs.join("|")})}
+ file_name = nil
+ path = nil
+
+ public_paths.detect do |pub_path|
+ path = pub_path
+ fp = File.join(pub_path, request.path_info)
+ file_name = fp if File.file?(fp)
+ end
+
+ return respond_not_found! unless file_name
+
+ # Make sure pricks aren't ../../config/database.yml ing us
+ respond_not_found! unless file_name.gsub(%r[^#{path}], "") == request.path_info
+
+ Rack::Response.new(
+ File.open(file_name),
+ 200,'Content-Type' => content_type_for_file(file_name)
+ ).finish
+ else
+ respond_not_found!
+ end
+ end
+
+ def self.respond_not_found!
+ Rack::Response.new("Not Found", 404).finish
+ end
+
+ def self.allowed_dirs
+ %w(stylesheets javascripts images)
+ end
+
+ def self.content_type_for_file(name)
+ file_name = File.basename(name).split(".").last.to_s
+ Mime::Type.lookup_by_extension(file_name).to_s
+ end
+
+ def self.public_paths
+ ActionController::Base.view_paths.map do |vp|
+ full_path = File.expand_path(vp.to_s, RAILS_ROOT)
+ full_path.sub("app/views", "public") if full_path =~ /vendor/
+ end.compact
+ end
+
+end
View
7 config/routes.rb
@@ -1,7 +0,0 @@
-ActionController::Routing::Routes.draw do |map|
-
- map.stylesheet '/stylesheets/*path', :controller => "publicious", :action => "show"
- map.image '/images/*path', :controller => "publicious", :action => "show"
- map.javascript '/javascripts/*path', :controller => "publicious", :action => "show"
-
-end
View
3 rails/init.rb
@@ -0,0 +1,3 @@
+Mime::Type.register "image/jpeg", :jpg
+Mime::Type.register "image/gif", :gif
+Mime::Type.register "image/png", :png
View
54 test/multiple_match_test.rb
@@ -0,0 +1,54 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class MultipleMatchesTest < Test::Unit::TestCase
+
+ include TestHelper
+
+ def setup
+ super
+
+ setup_plugin :no_file_here
+ setup_plugin :my_plugin
+ setup_plugin :your_plugin
+
+ @path = 'foo/bah.css'
+
+ @filename_1 = "/tmp/vendor/my_plugin/public/stylesheets/#{@path}"
+ @filecontents_1 = "hello from my stylesheet"
+ FileUtils.mkdir(File.dirname(@filename_1))
+ File.open(@filename_1, 'w') do |file|
+ file << @filecontents_1
+ end
+
+ @filename_2 = "/tmp/vendor/your_plugin/public/stylesheets/#{@path}"
+ @filecontents_2 = "hello from your stylesheet"
+ FileUtils.mkdir(File.dirname(@filename_2))
+ File.open(@filename_2, 'w') do |file|
+ file << @filecontents_2
+ end
+
+ get "/stylesheets/#{@path}"
+ end
+
+ def test_should_respond_with_success
+ assert last_response.ok?
+ end
+
+ def test_view_paths_should_contain_three_items
+ assert_equal 4, ActionController::Base.view_paths.size
+ end
+
+ def test_public_paths_should_contain_two_items
+ assert_equal 3, app.public_paths.size # one less that view_paths
+ end
+
+ def test_public_paths_should_contain_the_plugins_public_dir
+ assert_equal "/tmp/vendor/my_plugin/public", app.public_paths.second
+ end
+
+ def test_should_respond_with_first_file_contents
+ assert_equal File.read(@filename_1), last_response.body
+ assert_equal @filecontents_1, last_response.body
+ end
+
+end
View
12 test/no_match_test.rb
@@ -0,0 +1,12 @@
+require File.join(File.dirname(__FILE__), "test_helper")
+
+class NoMatchTest < Test::Unit::TestCase
+
+ include TestHelper
+
+ def test_redirect_logged_in_users_to_dashboard
+ get "/stylesheets/not_here.css"
+ assert last_response.not_found?
+ end
+
+end
View
12 test/no_matches_test.rb
@@ -1,12 +0,0 @@
-require File.dirname(__FILE__) + '/test_helper'
-
-class NoMatchesTest < ActionController::TestCase
- tests PubliciousController
-
- test "when no assets are found" do
- assert_raises ActionView::MissingTemplate do
- get :show, :path => ['does', 'not', 'exist.css']
- end
- end
-
-end
View
52 test/publicious_base_test.rb
@@ -1,52 +0,0 @@
-require File.dirname(__FILE__) + '/test_helper'
-
-class PubliciousBaseTest < ActionController::TestCase
- tests PubliciousController
-
- def setup_plugin(*plugin_names)
- plugin_names.each do |plugin_name|
- plugin_name = plugin_name.to_s
-
- PubliciousController.view_paths << File.join(@vendor_dir, plugin_name, 'app', 'views')
-
- FileUtils.mkdir(File.join(@vendor_dir, plugin_name))
- FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public'))
- FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'stylesheets'))
- FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'images'))
- FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'javascripts'))
- end
- end
-
- def setup_vendor_dir
- unless @vendor_dir
- @vendor_dir = "/tmp/vendor"
- FileUtils.mkdir(@vendor_dir)
- end
- end
-
- def teardown_vendor_dir
- if @vendor_dir
- begin
- FileUtils.rm_r(@vendor_dir)
- rescue StandardError => e
- puts "Error while removing #{@vendor_dir}"
- end
- end
- end
-
- def setup_standard_view_paths
- PubliciousController.view_paths = ['/tmp/app/views']
- end
-
- def setup
- super
- setup_vendor_dir
- setup_standard_view_paths
- end
-
- def teardown
- super
- teardown_vendor_dir
- end
-
-end
View
39 test/single_match_test.rb
@@ -1,37 +1,44 @@
-require File.dirname(__FILE__) + '/test_helper'
-
-class SingleMatchTest < PubliciousBaseTest
+require File.join(File.dirname(__FILE__), "test_helper")
+class SingleMatchTest < Test::Unit::TestCase
+
+ include TestHelper
+
def setup
super
+
setup_plugin :my_plugin
- @path = ['foo', 'bah.css']
- @filename = "/tmp/vendor/my_plugin/public/stylesheets/#{@path.join('/')}"
+ @path = 'foo/bah.css'
+ @filename = "/tmp/vendor/my_plugin/public/stylesheets/#{@path}"
@filecontents = "hello from stylesheet"
FileUtils.mkdir(File.dirname(@filename))
File.open(@filename, 'w') do |file|
file << "hello from stylesheet"
end
- get :show, :path => @path
+ get "/stylesheets/#{@path}"
end
- test "view_paths should contain two items" do
- assert_equal 2, @controller.view_paths.size
+ def test_should_respond_with_success
+ assert last_response.ok?
end
- test "public_paths should contain one item (one less than view paths)" do
- assert_equal 1, @controller.send(:public_paths).size
+ def test_view_paths_should_contain_three_items
+ assert_equal 2, ActionController::Base.view_paths.size
end
- test "public_paths should contain the plugin's public dir" do
- assert_equal "/tmp/vendor/my_plugin/public", @controller.send(:public_paths).first
+ def test_public_paths_should_contain_two_items
+ assert_equal 1, app.public_paths.size # one less that view_paths
+ end
+
+ def test_public_paths_should_contain_the_plugins_public_dir
+ assert_equal "/tmp/vendor/my_plugin/public", app.public_paths.first
end
- test "should respond with the file contents" do
- assert_equal File.read(@filename), @controller.response.body
- assert_equal @filecontents, @controller.response.body
+ def test_should_respond_with_first_file_contents
+ assert_equal File.read(@filename), last_response.body
+ assert_equal @filecontents, last_response.body
end
-end
+end
View
68 test/test_helper.rb
@@ -1,15 +1,61 @@
-require 'test/unit'
require 'rubygems'
-require 'active_support'
-require 'activesupport'
-require 'action_controller'
-require 'action_controller/test_case'
-require 'action_controller/test_process'
+require 'test/unit'
+require "rack/test"
+require "action_controller"
+require File.join(File.dirname(__FILE__), "../app/metal/publicious_metal")
+RAILS_ROOT = "/tmp"
-class ApplicationController < ActionController::Base
+module TestHelper
+ include Rack::Test::Methods
+
+ def app
+ PubliciousMetal
+ end
+
+ def setup_vendor_dir
+ unless @vendor_dir
+ @vendor_dir = "/tmp/vendor"
+ FileUtils.mkdir(@vendor_dir)
+ end
+ end
+
+ def teardown_vendor_dir
+ if @vendor_dir
+ begin
+ FileUtils.rm_r(@vendor_dir)
+ rescue StandardError => e
+ puts "Error while removing #{@vendor_dir}"
+ end
+ end
+ end
+
+ def setup_standard_view_paths
+ ActionController::Base.view_paths = ['/tmp/app/views']
+ end
+
+ def setup
+ setup_vendor_dir
+ setup_standard_view_paths
+ end
+
+ def teardown
+ teardown_vendor_dir
+ setup_standard_view_paths
+ end
+
+ def setup_plugin(*plugin_names)
+ plugin_names.each do |plugin_name|
+ plugin_name = plugin_name.to_s
+
+ ActionController::Base.view_paths << File.join(@vendor_dir, plugin_name, 'app', 'views')
+
+ FileUtils.mkdir(File.join(@vendor_dir, plugin_name))
+ FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public'))
+ FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'stylesheets'))
+ FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'images'))
+ FileUtils.mkdir(File.join(@vendor_dir, plugin_name, 'public', 'javascripts'))
+ end
+ end
+
end
-
-require File.dirname(__FILE__) + '/../app/controllers/publicious_controller'
-require File.dirname(__FILE__) + '/../config/routes'
-

0 comments on commit fdd7343

Please sign in to comment.
Something went wrong with that request. Please try again.