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. def initialize(dir, opts = {}) @files = Mongrel::DirHandler.new(dir,false) @guard = Mutex.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. # 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) start = Time.now if response.socket.closed? return end MERB_LOGGER.info("Request: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]}") # 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 MERB_LOGGER.info("Serving static file: #{path_info}") @files.process(request,response) elsif get_or_head and @files.can_serve(page_cached) # Possible cached page, serve it up MERB_LOGGER.info("Serving static file: #{page_cached}") 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 = nil controller, action = handle(request) MERB_LOGGER.info("Routing to controller: #{controller.class} action: #{action}\nParsing HTTP Input took: #{Time.now - start} seconds") # special case allows the progress action of a Files controller # to be handled without locking since no db access is required. # but we do need to synchronize whenever you use ActiveRecord # in your controllers. if (action == 'progress') && (Files === controller) puts 'skip mutex' controller.dispatch(action) else @guard.synchronize { controller.dispatch(action) } end rescue Exception => e response.start(500) do |head,out| head["Content-Type"] = "text/html" MERB_LOGGER.info(exception(e)) out << html_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 MERB_LOGGER.info("X-SENDFILE: #{sendfile}\nComplete Request took: #{Time.now - start} seconds") # send X-SENDFILE header to mongrel response.send_status(File.size(sendfile)) response.send_header response.send_file(sendfile) else MERB_LOGGER.info("Response status: #{response.status}\nComplete Request took: #{Time.now - start} seconds\n\n") # render response from successful controller response.send_status((controller.body||='').length) response.send_header response.write(controller.body) 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, action] 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) [ instantiate_controller(route[:controller], request.body, request.params, route), route[:action] ] end # take a controller class name string and reload or require # the right controller file then CamelCase it and turn 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 html_exception(e) "

Merb Error!

#{ e.message } - (#{ e.class })\n" << "#{(e.backtrace or []).join('
')}

" end def exception(e) "#{ e.message } - (#{ e.class })\n" << "#{(e.backtrace or []).join("\n")}" end end