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("\nRequest: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]} (#{Time.now.strftime("%Y-%m-%d %H:%M:%S")})") # 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") # We need a mutex here because ActiveRecord code can be run # in your controller actions. AR performs much better in single # threaded mode so we lock here for the shortest amount of time # possible. Route recognition and mime parsing has already occured # at this point because those processes are thread safe. This # gives us the best trade off for multi threaded performance # of thread safe, and a lock around calls to your controller actions. @guard.synchronize { controller.dispatch(action) } 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") file_status = File.stat(sendfile) response.status = 200 # Set the last modified times as well and etag for all files response.header[Mongrel::Const::LAST_MODIFIED] = file_status.mtime.httpdate # Calculated the same as apache, not sure how well the works on win32 response.header[Mongrel::Const::ETAG] = Mongrel::Const::ETAG_FORMAT % [file_status.mtime.to_i, file_status.size, file_status.ino] # send a status with out content length response.send_status(file_status.size) response.send_header response.send_file(sendfile) elsif controller.body.respond_to? :read response.send_status(clength) response.send_header while chunk = controller.body.read(16384) response.write(chunk) end if controller.body.respond_to? :close controller.body.close end 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") raise Merb::MissingControllerFile end begin controller_name.import return Object.const_get( controller_name.camel_case ).new(req, env, params) rescue RuntimeError warn "Error getting instance of '#{controller_name.camel_case}': #{$!}" raise $! end end # format exception message for browser display def html_exception(e) "
#{ e.message } - (#{ e.class })\n" <<
"#{(e.backtrace or []).join('
')}