From 342044a01d28a8f426493a56131c4f7358ad6f62 Mon Sep 17 00:00:00 2001 From: Thomas Reynolds Date: Tue, 1 May 2012 13:11:42 -0700 Subject: [PATCH] Split out Request handling into an Extension. Dup the Application (like Sinatra) when responding --- .../lib/middleman-core/application.rb | 310 +-------------- .../middleman-core/core_extensions/request.rb | 371 ++++++++++++++++++ .../lib/middleman-core/extensions.rb | 3 + 3 files changed, 381 insertions(+), 303 deletions(-) create mode 100644 middleman-core/lib/middleman-core/core_extensions/request.rb diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index a33b90fe..cd9749c4 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -1,7 +1,3 @@ -# Built on Rack -require "rack" -require "rack/file" - # Using Tilt for templating require "tilt" @@ -12,10 +8,6 @@ require "active_support/json" require "middleman-core/vendor/hooks-0.2.0/lib/hooks" require "middleman-core/sitemap" - -# Let's serve all HTML as UTF-8 -::Rack::Mime::MIME_TYPES['.html'] = 'text/html;charset=utf8' -::Rack::Mime::MIME_TYPES['.htm'] = 'text/html;charset=utf8' # Core Middleman Class module Middleman @@ -30,97 +22,6 @@ module Middleman define_hook :ready class << self - - # Reset Rack setup - # - # @private - def reset! - @app = nil - @prototype = nil - end - - # The shared Rack instance being build - # - # @private - # @return [Rack::Builder] - def app - @app ||= Rack::Builder.new - end - - # Get the static instance - # - # @private - # @return [Middleman::Application] - def inst(&block) - @inst ||= begin - mm = new(&block) - mm.run_hook :ready - mm - end - end - - # Set the shared instance - # - # @private - # @param [Middleman::Application] inst - # @return [void] - def inst=(inst) - @inst = inst - end - - # Return built Rack app - # - # @private - # @return [Rack::Builder] - def to_rack_app(&block) - inner_app = inst(&block) - - (@middleware || []).each do |m| - app.use(m[0], *m[1], &m[2]) - end - - app.map("/") { run inner_app } - - (@mappings || []).each do |m| - app.map(m[0], &m[1]) - end - - app - end - - # Prototype app. Used in config.ru - # - # @private - # @return [Rack::Builder] - def prototype - @prototype ||= to_rack_app - end - - # Call prototype, use in config.ru - # - # @private - def call(env) - prototype.call(env) - end - - # Use Rack middleware - # - # @param [Class] Middleware - # @return [void] - def use(middleware, *args, &block) - @middleware ||= [] - @middleware << [middleware, args, block] - end - - # Add Rack App mapped to specific path - # - # @param [String] Path to map - # @return [void] - def map(map, &block) - @mappings ||= [] - @mappings << [map, block] - end - # Mix-in helper methods. Accepts either a list of Modules # and/or a block to be evaluated # @return [void] @@ -149,7 +50,9 @@ module Middleman @inst.set(key, value, &block) if @inst end end - + + delegate :helpers, :to => :"self.class" + # Set attributes (global variables) # # @param [Symbol] Name of the attribue @@ -216,6 +119,9 @@ module Middleman # Activate custom features and extensions include Middleman::CoreExtensions::Extensions + + # Basic Rack Request Handling + register Middleman::CoreExtensions::Request # Handle exceptions register Middleman::CoreExtensions::ShowExceptions @@ -263,27 +169,6 @@ module Middleman Middleman::Extensions.register(:asset_host) { Middleman::Extensions::AssetHost } - # Backwards-compatibility with old request.path signature - attr :request - - # Accessor for current path - # @return [String] - def current_path - Thread.current[:current_path] - end - - # Set the current path - # - # @param [String] path The new current path - # @return [void] - def current_path=(path) - Thread.current[:current_path] = path - @request = ::Thor::CoreExt::HashWithIndifferentAccess.new({ - :path => path, - :params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {} - }) - end - # Initialize the Middleman project def initialize(&block) # Current path defaults to nil, used in views. @@ -315,75 +200,7 @@ module Middleman @_cache ||= ::Middleman::Util::Cache.new end delegate :cache, :to => :"self.class" - - # Rack env - def env - Thread.current[:env] - end - def env=(value) - Thread.current[:env] = value - end - - # Rack request - # @return [Rack::Request] - def req - Thread.current[:req] - end - def req=(value) - Thread.current[:req] = value - end - - # Rack response - # @return [Rack::Response] - def res - Thread.current[:res] - end - def res=(value) - Thread.current[:res] = value - end - # Rack Interface - # - # @private - # @param Rack environment - def call(env) - self.env = env - # Store environment, request and response for later - self.req = req = Rack::Request.new(env) - self.res = res = Rack::Response.new - - if env["PATH_INFO"] == "/__middleman__" - if env["REQUEST_METHOD"] == "POST" - if req.params.has_key?("change") - self.files.did_change(req.params["change"]) - elsif req.params.has_key?("delete") - self.files.did_delete(req.params["delete"]) - end - end - - res.status = 200 - return res.finish - end - - puts "== Request: #{env["PATH_INFO"]}" if logging? - - # Catch :halt exceptions and use that response if given - catch(:halt) do - process_request(env, req, res) - - res.status = 404 - res.finish - end - end - - # Halt the current request and return a response - # - # @private - # @param [String] Response value - def halt(response) - throw :halt, response - end - # Whether we're in development mode # @return [Boolean] If we're in dev mode def development?; environment == :development; end @@ -391,59 +208,6 @@ module Middleman # Whether we're in build mode # @return [Boolean] If we're in build mode def build?; environment == :build; end - - # Core response method. We process the request, check with the sitemap, - # and return the correct file, response or status message. - # - # @private - def process_request(env, req, res) - start_time = Time.now - - # Normalize the path and add index if we're looking at a directory - original_path = URI.decode(env["PATH_INFO"].dup) - if original_path.respond_to? :force_encoding - original_path.force_encoding('UTF-8') - end - request_path = full_path(original_path) - - # Run before callbacks - run_hook :before - - if original_path != request_path - # Get the resource object for this path - resource = sitemap.find_resource_by_destination_path(original_path) - end - - # Get the resource object for this full path - resource ||= sitemap.find_resource_by_destination_path(request_path) - - # Return 404 if not in sitemap - return not_found(res) unless resource && !resource.ignored? - - # If this path is a static file, send it immediately - return send_file(resource.source_file, env, res) unless resource.template? - - # Set the current path for use in helpers - self.current_path = request_path.dup - - # Set a HTTP content type based on the request's extensions - content_type(res, resource.mime_type) - - begin - # Write out the contents of the page - res.write resource.render - - # Valid content is a 200 status - res.status = 200 - rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e - res.write "Error: #{e.message}" - res.status = 500 - end - - # End the request - puts "== Finishing Request: #{self.current_path} (#{(Time.now - start_time).round(2)}s)" if logging? - halt res.finish - end # Backwards compatibilty with old Sinatra template interface # @@ -474,66 +238,6 @@ module Middleman end end - # Add a new mime-type for a specific extension - # - # @param [Symbol] type File extension - # @param [String] value Mime type - # @return [void] - def mime_type(type, value=nil) - return type if type.nil? || type.to_s.include?('/') - type = ".#{type}" unless type.to_s[0] == ?. - return ::Rack::Mime.mime_type(type, nil) unless value - ::Rack::Mime::MIME_TYPES[type] = value - end - - protected - - # Halt request and return 404 - def not_found(res) - res.status == 404 - res.write "

File Not Found

#{@request_path}

" - res.finish - end - - delegate :helpers, :use, :map, :to => :"self.class" - - # Immediately send static file - # - # @param [String] path File to send - def send_file(path, env, res) - extension = File.extname(path) - matched_mime = mime_type(extension) - matched_mime = "application/octet-stream" if matched_mime.nil? - content_type res, matched_mime - - file = ::Rack::File.new nil - file.path = path - response = file.serving(env) - response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension) - halt response - end - - # Set the content type for the current request - # - # @param [String] type Content type - # @param [Hash] params - # @return [void] - def content_type(res, type, params={}) - return res['Content-Type'] unless type - default = params.delete :default - mime_type = mime_type(type) || default - throw "Unknown media type: %p" % type if mime_type.nil? - mime_type = mime_type.dup - unless params.include? :charset - params[:charset] = params.delete('charset') || "utf-8" - end - params.delete :charset if mime_type.include? 'charset' - unless params.empty? - mime_type << (mime_type.include?(';') ? ', ' : ';') - mime_type << params.map { |kv| kv.join('=') }.join(', ') - end - res['Content-Type'] = mime_type - end end class << self @@ -572,4 +276,4 @@ module Middleman server end end -end +end \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/core_extensions/request.rb b/middleman-core/lib/middleman-core/core_extensions/request.rb new file mode 100644 index 00000000..228c0377 --- /dev/null +++ b/middleman-core/lib/middleman-core/core_extensions/request.rb @@ -0,0 +1,371 @@ +# Base helper to manipulate asset paths +module Middleman + module CoreExtensions + module Request + + # Extension registered + class << self + # @private + def registered(app) + # Built on Rack + require "rack" + require "rack/file" + + # Let's serve all HTML as UTF-8 + ::Rack::Mime::MIME_TYPES['.html'] = 'text/html;charset=utf8' + ::Rack::Mime::MIME_TYPES['.htm'] = 'text/html;charset=utf8' + + + app.extend ClassMethods + app.extend ServerMethods + + # Include instance methods + app.send :include, InstanceMethods + end + alias :included :registered + end + + module ClassMethods + # Reset Rack setup + # + # @private + def reset! + @app = nil + @prototype = nil + end + + # The shared Rack instance being build + # + # @private + # @return [Rack::Builder] + def app + @app ||= ::Rack::Builder.new + end + + # Get the static instance + # + # @private + # @return [Middleman::Application] + def inst(&block) + @inst ||= begin + mm = new(&block) + mm.run_hook :ready + mm + end + end + + # Set the shared instance + # + # @private + # @param [Middleman::Application] inst + # @return [void] + def inst=(inst) + @inst = inst + end + + # Return built Rack app + # + # @private + # @return [Rack::Builder] + def to_rack_app(&block) + inner_app = inst(&block) + + (@middleware || []).each do |m| + app.use(m[0], *m[1], &m[2]) + end + + app.map("/") { run inner_app } + + (@mappings || []).each do |m| + app.map(m[0], &m[1]) + end + + app + end + + # Prototype app. Used in config.ru + # + # @private + # @return [Rack::Builder] + def prototype + @prototype ||= to_rack_app + end + + # Call prototype, use in config.ru + # + # @private + def call(env) + prototype.call(env) + end + + # Use Rack middleware + # + # @param [Class] Middleware + # @return [void] + def use(middleware, *args, &block) + @middleware ||= [] + @middleware << [middleware, args, block] + end + + # Add Rack App mapped to specific path + # + # @param [String] Path to map + # @return [void] + def map(map, &block) + @mappings ||= [] + @mappings << [map, block] + end + end + + module ServerMethods + # Create a new Class which is based on Middleman::Application + # Used to create a safe sandbox into which extensions and + # configuration can be included later without impacting + # other classes and instances. + # + # @return [Class] + def server(&block) + @@servercounter ||= 0 + @@servercounter += 1 + const_set("MiddlemanApplication#{@@servercounter}", Class.new(Middleman::Application)) + end + + # Creates a new Rack::Server + # + # @param [Hash] options to pass to Rack::Server.new + # @return [Rack::Server] + def start_server(options={}) + opts = { + :Port => options[:port] || 4567, + :Host => options[:host] || "0.0.0.0", + :AccessLog => [] + } + + app_class = options[:app] ||= ::Middleman.server.inst + opts[:app] = app_class + + require "webrick" + opts[:Logger] = WEBrick::Log::new("/dev/null", 7) if !options[:logging] + opts[:server] = 'webrick' + + server = ::Rack::Server.new(opts) + server.start + server + end + end + # Methods to be mixed-in to Middleman::Application + module InstanceMethods + # Backwards-compatibility with old request.path signature + def request + Thread.current[:request] + end + + # Accessor for current path + # @return [String] + def current_path + Thread.current[:current_path] + end + + # Set the current path + # + # @param [String] path The new current path + # @return [void] + def current_path=(path) + Thread.current[:current_path] = path + Thread.current[:request] = ::Thor::CoreExt::HashWithIndifferentAccess.new({ + :path => path, + :params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {} + }) + end + + def use(*args, &block); self.class.use(*args); end + def map(*args, &block); self.class.map(*args, &block); end + + # Rack env + def env + Thread.current[:env] + end + def env=(value) + Thread.current[:env] = value + end + + # Rack request + # @return [Rack::Request] + def req + Thread.current[:req] + end + def req=(value) + Thread.current[:req] = value + end + + # Rack response + # @return [Rack::Response] + def res + Thread.current[:res] + end + def res=(value) + Thread.current[:res] = value + end + + def call(env) + # Keep `__middleman__` messaging to this thread + if env["PATH_INFO"] == "/__middleman__" + if env["REQUEST_METHOD"] == "POST" + req = ::Rack::Request.new(env) + if req.params.has_key?("change") + self.files.did_change(req.params["change"]) + elsif req.params.has_key?("delete") + self.files.did_delete(req.params["delete"]) + end + end + + res = ::Rack::Response.new + res.status = 200 + return res.finish + end + + dup.call!(env) + end + + # Rack Interface + # + # @private + # @param Rack environment + def call!(env) + self.env = env + # Store environment, request and response for later + self.req = req = ::Rack::Request.new(env) + self.res = res = ::Rack::Response.new + + puts "== Request: #{env["PATH_INFO"]}" if logging? + + # Catch :halt exceptions and use that response if given + catch(:halt) do + process_request(env, req, res) + + res.status = 404 + res.finish + end + end + + # Halt the current request and return a response + # + # @private + # @param [String] Response value + def halt(response) + throw :halt, response + end + + # Core response method. We process the request, check with the sitemap, + # and return the correct file, response or status message. + # + # @private + def process_request(env, req, res) + start_time = Time.now + + # Normalize the path and add index if we're looking at a directory + original_path = URI.decode(env["PATH_INFO"].dup) + if original_path.respond_to? :force_encoding + original_path.force_encoding('UTF-8') + end + request_path = full_path(original_path) + + # Run before callbacks + run_hook :before + + if original_path != request_path + # Get the resource object for this path + resource = sitemap.find_resource_by_destination_path(original_path) + end + + # Get the resource object for this full path + resource ||= sitemap.find_resource_by_destination_path(request_path) + + # Return 404 if not in sitemap + return not_found(res) unless resource && !resource.ignored? + + # If this path is a static file, send it immediately + return send_file(resource.source_file, env, res) unless resource.template? + + # Set the current path for use in helpers + self.current_path = request_path.dup + + # Set a HTTP content type based on the request's extensions + content_type(res, resource.mime_type) + + begin + # Write out the contents of the page + res.write resource.render + + # Valid content is a 200 status + res.status = 200 + rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e + res.write "Error: #{e.message}" + res.status = 500 + end + + # End the request + puts "== Finishing Request: #{self.current_path} (#{(Time.now - start_time).round(2)}s)" if logging? + halt res.finish + end + + # Add a new mime-type for a specific extension + # + # @param [Symbol] type File extension + # @param [String] value Mime type + # @return [void] + def mime_type(type, value=nil) + return type if type.nil? || type.to_s.include?('/') + type = ".#{type}" unless type.to_s[0] == ?. + return ::Rack::Mime.mime_type(type, nil) unless value + ::Rack::Mime::MIME_TYPES[type] = value + end + + # Halt request and return 404 + def not_found(res) + res.status == 404 + res.write "

File Not Found

#{@request_path}

" + res.finish + end + + # Immediately send static file + # + # @param [String] path File to send + def send_file(path, env, res) + extension = File.extname(path) + matched_mime = mime_type(extension) + matched_mime = "application/octet-stream" if matched_mime.nil? + content_type res, matched_mime + + file = ::Rack::File.new nil + file.path = path + response = file.serving(env) + response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension) + halt response + end + + # Set the content type for the current request + # + # @param [String] type Content type + # @param [Hash] params + # @return [void] + def content_type(res, type, params={}) + return res['Content-Type'] unless type + default = params.delete :default + mime_type = mime_type(type) || default + throw "Unknown media type: %p" % type if mime_type.nil? + mime_type = mime_type.dup + unless params.include? :charset + params[:charset] = params.delete('charset') || "utf-8" + end + params.delete :charset if mime_type.include? 'charset' + unless params.empty? + mime_type << (mime_type.include?(';') ? ', ' : ';') + mime_type << params.map { |kv| kv.join('=') }.join(', ') + end + res['Content-Type'] = mime_type + end + end + end + end +end \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/extensions.rb b/middleman-core/lib/middleman-core/extensions.rb index 21a55127..550980b0 100644 --- a/middleman-core/lib/middleman-core/extensions.rb +++ b/middleman-core/lib/middleman-core/extensions.rb @@ -5,6 +5,9 @@ module Middleman end module CoreExtensions + # Rack Request + autoload :Request, "middleman-core/core_extensions/request" + # File Change Notifier autoload :FileWatcher, "middleman-core/core_extensions/file_watcher"