require 'sync' class MerbHandler < Mongrel::HttpHandler @@file_only_methods = ["GET","HEAD"] # take the name of a directory and use that as the doc root or public # directory of your site. This is set to the root of your merb app + '/public' # by default. See merb_daemon.rb if you want to change this. def initialize(dir, opts = {}) @files = Mongrel::DirHandler.new(dir,false) @guard = Sync.new end # process incoming http requests and do a number of things # 1. check for rails style cached pages. add .html to the # url and see if there is a static file in public that matches. # serve that file directly without invoking Merb and be done with it. # 2. Serve static asset and html files directly from public/ if # they exist and fall back to Merb otherwise # 3. If none of the above apply, we take apart the request url # and feed it into Merb::RouteMatcher to let it decide which # controller and method will serve the request. # 4. after the controller has done its thing, we check for the # X-SENDFILE header. if you set this header to the path to a file # in your controller then mongrel will serve the file directly # and your controller can go on processing other requests. def process(request, response) if response.socket.closed? return end # Rails style page caching. Check the public dir first for # .html pages and serve directly. Otherwise fall back to Merb # routing and request dispatching. path_info = request.params[Mongrel::Const::PATH_INFO] page_cached = path_info + ".html" get_or_head = @@file_only_methods.include? request.params[Mongrel::Const::REQUEST_METHOD] if get_or_head and @files.can_serve(path_info) # File exists as-is so serve it up @files.process(request,response) elsif get_or_head and @files.can_serve(page_cached) # Possible cached page, serve it up request.params[Mongrel::Const::PATH_INFO] = page_cached @files.process(request,response) else begin # This handles parsing the query string and post/file upload # params and is outside of the synchronize call so that # multiple file uploads can be done at once. controller, action = handle(request) p controller, action output = nil # synchronize here because this is where ActiveRecord or your db # calls will be run in your controller methods. @guard.synchronize(:EX) { output = if (controller && controller.kind_of?(Merb::Controller)) if action controller.send(action) else controller.to_s end else nil end } rescue Exception => e response.start(500) do |head,out| head["Content-Type"] = "text/html" out << exception(e) end return end sendfile, clength = nil response.status = controller.status # check for the X-SENDFILE header from your Merb::Controller # and serve the file directly instead of buffering. controller.headers.each do |k, v| if k =~ /^X-SENDFILE$/i sendfile = v elsif k =~ /^CONTENT-LENGTH$/i clength = v.to_i else [*v].each do |vi| response.header[k] = vi end end end if sendfile # send X-SENDFILE header to mongrel response.send_status(File.size(sendfile)) response.send_header response.send_file(sendfile) else # render response from successful controller response.send_status(output.length) response.send_header response.write(output) end end end # This is where we grab the incoming request PATH_INFO # and use that in the merb routematcher to determine # which controller and method to run. # returns a 2 element tuple of: # [controller_object, method] def handle(request) path = request.params[Mongrel::Const::PATH_INFO].sub(/\/+/, '/') path = path[0..-2] if (path[-1] == ?/) route = Merb::RouteMatcher.new.route_request(path) puts route.inspect if $DEBUG if route [ instantiate_controller(route[:controller], request.body, request.params, route.dup.delete_if{|k,v| [:controller, :action].include? k}), route[:action]] else ["
Error: no route matches!", nil] end end # take a controller class name string and reload or require # the right controller file then upcase the first letter and # trun it into a new object passing in the request and response. # this is where your Merb::Controller is instantiated. def instantiate_controller(controller_name, req, env, params) if !File.exist?(Merb::Server.config[:dist_root]+"/app/controllers/#{controller_name.snake_case}.rb") return Object.const_get(:Noroutefound).new(req, env, params) end begin controller_name.import return Object.const_get( controller_name.camel_case ).new(req, env, params) rescue Exception warn "Error getting instance of '#{controller_name.camel_case}': #{$!}" raise $! end end # format exception message for browser display def exception(e) "#{ e.message } - (#{ e.class })\n" <<
"#{(e.backtrace or []).join('
')}