bin/httphere in httphere-1.0.1 vs bin/httphere in httphere-1.1.0

- old
+ new

@@ -1,9 +1,21 @@ #!/usr/bin/env ruby -$VERSION = '0.0.1' +$VERSION = File.read(File.dirname(__FILE__) + '/../VERSION').chomp $DEBUG = false +if $DEBUG + require 'benchmark' + require 'rubygems' + require 'ruby-debug' +end +def log(*msgs) + msgs.each do |msg| + $stdout << msg + end + $stdout.flush + true +end require 'socket' $options = {} ENV['XDG_DATA_DIRS'] = (ENV['XDG_DATA_DIRS'].to_s.split(/:/) << '/opt/local/share').join(':') @@ -22,10 +34,27 @@ $options[:address] = '0.0.0.0' opts.on( '-a ADDRESS', '--address ADDRESS', "Listen on ADDRESS ip. Defaults to [#{$options[:address]}]") do |address| $options[:address] = address end + $options[:cache_size] = nil + opts.on( '--cache-size SIZE', "Turn in-memory caching on and set the maximum cache size. Example: 500K, 10M, 1G") do |size| + $options[:cache_size] = case size + when /^\d+$/ + size.to_i + when /^\d+kb?$/i + size.to_i * 1024 # kilobytes + when /^\d+mb?$/i + size.to_i * 1024*1024 # megabytes + when /^\d+gb?$/i + size.to_i * 1024*1024*1000 # gigabytes + else + warn "Couldn't understand --cache-size #{size}!" + exit + end + end + $options[:https_domain] = $options[:address] end optparse.parse! @@ -391,11 +420,11 @@ HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i HttpRequestRE = /^(GET|POST|PUT|DELETE) (\/.*) HTTP\/([\d\.]+)[\r\n]?$/i BlankLineRE = /^[\n\r]+$/ def receive_data(data) - return unless (data and data.length > 0) + return unless (data and data.size > 0) @last_activity = Time.now case parser.state when :init @@ -446,11 +475,11 @@ parser.linebuffer << data end when :entity if parser.entity_size chars_yet_needed = parser.entity_size - parser.entity_pos - taking_this_many = [chars_yet_needed, data.length].sort.first + taking_this_many = [chars_yet_needed, data.size].sort.first parser.textbuffer << data[0...taking_this_many] leftover_data = data[taking_this_many..-1] parser.entity_pos += taking_this_many if parser.entity_pos >= parser.entity_size entity_data = parser.textbuffer.join @@ -538,61 +567,136 @@ autoload :Markdown, 'httphere/markdown' autoload :Textile, 'httphere/textile' end require 'UniversalDetector' require 'shared-mime-info' +class File + def size + File.size(path) + end +end + class FileServer < EventParsers::Http11Parser::Request + class << self + attr_accessor :cache + def cached?(filename) + # Serve from cache if it's in the cache and if the file hasn't changed. + if cache + # Delete from cache if the file's been modified since last cache. + cache.delete(filename) if cache.has_key?(filename) && File.mtime(filename) > cache[filename][0][:mtime] + cache.has_key?(filename) + end + end + def from_cache(filename) + cache[filename][0][:last_accessed_at] = Time.now + puts "From cache: #{filename} / #{cache[filename][0][:size]}" + [cache[filename][1], cache[filename][0][:content_type]] + end + def cache?(filename,response_body) + # Obviously, don't cache if it's bigger than the cache max size + cache && File.exists?(filename) && response_body.size < cache.max_size + end + def cache!(filename,content_type,response_body) + if cache?(filename,response_body) + puts "Caching #{filename} / #{response_body.size}" + cache[filename] = [ + { + :last_accessed_at => Time.now, + :mtime => File.mtime(filename), + :size => response_body.size, + :content_type => content_type + }, + response_body + ] + end + end + end + # This is where the routing is processed. def process # Get the filename desired filename, query = resource_uri.split('?',2) - filename = filename.sub(/^\//,'') + @filename = filename.sub(/^\//,'') # Default to any file named index.* - filename = Dir["index.*"].first if filename.to_s == '' && Dir["index.*"].length > 0 + @filename = Dir["index.*"].first if filename.to_s == '' && Dir["index.*"].length > 0 file_extension = (filename.match(/\.([^\.]+)$/) || [])[1] - if File.exists?(filename) && !File.directory?(filename) - content_type = MIME.check(filename).type - file_body = File.read(filename) + if File.exists?(@filename) && !File.directory?(@filename) - # If .markdown, render as Markdown - if file_extension == 'markdown' - file_body = Renderers::Markdown.render_content(file_body) - content_type = 'text/html' - end + # Read from cache if possible + file_handle = FileServer.cached?(@filename) ? :from_cache : :from_disk + if file_handle == :from_disk + if $DEBUG + Benchmark.bm do |x| + x.report("Getting MIME type") do + @content_type = MIME.check(@filename).type + end + end + else + @content_type = MIME.check(@filename).type + end - # If .textile, render as Textile - if file_extension == 'textile' - file_body = Renderers::Textile.render_content(file_body) - content_type = 'text/html' + file_handle = File.open(@filename) + + # If .markdown, render as Markdown + if file_extension == 'markdown' + file_handle = Renderers::Markdown.render_content(file_handle.read) + @content_type = 'text/html' + end + + # If .textile, render as Textile + if file_extension == 'textile' + file_handle = Renderers::Textile.render_content(file_handle.read) + @content_type = 'text/html' + end end # Send Response - respond!('200 Ok', content_type, file_body) + respond!('200 Ok', @content_type, file_handle) else respond!('404 Not Found', 'text/plain', "Could not find file: '#{resource_uri}'") end end def respond!(status, content_type, body) respond(status, content_type, body) connection.halt! end def respond(status, content_type, body) - # Convert to UTF-8 if possible - chardet = UniversalDetector::chardet(body) - if chardet['confidence'] > 0.7 - charset = chardet['encoding'] - body = Iconv.conv('utf-8', charset, body) - # else # no conversion + if body == :from_cache + body, content_type = FileServer.from_cache(@filename) + else + body = StringIO.new(body.to_s) if !body.is_a?(IO) && body.respond_to?(:to_s) + + # Convert to UTF-8 if possible + chardet = UniversalDetector::chardet(body.read(512)); body.rewind # Detect charset only from the first 512 bytes + if chardet['confidence'] > 0.7 && ['utf-8', 'ascii'].include?(chardet['encoding']) + if $DEBUG + Benchmark.bm do |x| + x.report("Converting from #{chardet['encoding']} to UTF-8") do + charset = chardet['encoding'] + body = StringIO.new(Iconv.conv('utf-8', charset, body.read)) + end + end + else + charset = chardet['encoding'] + body = StringIO.new(Iconv.conv('utf-8', charset, body.read)) + end + else # no conversion + puts "No charset conversion necessary." if $DEBUG + end + + # Write to cache if we should + FileServer.cache!(@filename, content_type, body) end - body_length = body.length + body_length = body.size + body.rewind + # Send the response! - connection.send_response! "HTTP/1.1 #{status}\r\nServer: HTTP-Here, version #{$VERSION}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body_length+2}\r\n\r\n#{body}\r\n" + connection.send_response! "HTTP/1.1 #{status}\r\nServer: HTTP-Here, version #{$VERSION}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body_length+2}\r\n\r\n#{body.read}\r\n" span = (Time.now - connection.time).to_f - content_type puts (status =~ /200/ ? "Served #{resource_uri} (#{content_type})" : "404 #{resource_uri}" ) + " at #{1 / span} requests/second" unless status =~ /404/ && resource_uri == '/favicon.ico' end @@ -629,9 +733,40 @@ # puts "Client #{@socket} disconnected." end end # EventMachineMini.ssl_config[:GenerateSSLCert] = true + +class Cache + def initialize(config={}) + @config = config + @cache = {} + end + def max_size + @config[:max_size] + end + def [](key) + @cache[key] + end + def []=(key,value) + @cache[key] = value + end + def has_key?(key) + @cache.has_key?(key) + end + def delete(key) + @cache.delete(key) + end + def bytes + @cache.values.inject(0) {|s,v| s+v[0][:size]} + end +end + +# Turn Caching on if asked for +if $options[:cache_size] + FileServer.cache = Cache.new(:max_size => $options[:cache_size]) + puts "Caching files in memory, using up to #{$options[:cache_size]} bytes." +end puts "HTTP Here v#{$VERSION} : listening on #{$options[:address]}:#{$options[:port]}..." begin $server = EventMachineMini.new( :listen => {"#{$options[:address]}:#{$options[:port]}" => Http11Server} ) rescue Errno::EACCES