diff --git a/lib/mongrel.rb b/lib/mongrel.rb index 830aba13..228fd9f4 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -9,6 +9,15 @@ require 'mongrel/tcphack' require 'yaml' require 'time' +begin + require 'rubygems' + require 'sendfile' + $mongrel_has_sendfile = true + STDERR.puts "** You have sendfile installed, will use that to serve files." +rescue Object + $mongrel_has_sendfile = false +end + # Mongrel module containing all of the classes (include C extensions) for running # a Mongrel web server. It contains a minimalist HTTP server with just enough # functionality to service web application requests fast as possible. @@ -324,6 +333,25 @@ module Mongrel end end + # Appends the contents of +path+ to the response stream. The file is opened for binary + # reading and written in chunks to the socket. If the + # sendfile library is found, + # it is used to send the file, often with greater speed and less memory/cpu usage. + def send_file(path) + File.open(path, "rb") do |f| + if @socket.respond_to? :sendfile + @socket.sendfile(f) + else + while chunk = f.read(Const::CHUNK_SIZE) + @socket.write(chunk) + end + end + end + rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF + # ignore these since it means the client closed off early + STDERR.puts "Client closed socket requesting file #{req}: #$!" + end + def write(data) @socket.write(data) end diff --git a/lib/mongrel/camping.rb b/lib/mongrel/camping.rb index 26546c1a..fb4efb8e 100644 --- a/lib/mongrel/camping.rb +++ b/lib/mongrel/camping.rb @@ -34,21 +34,30 @@ module Mongrel def process(request, response) req = StringIO.new(request.body) controller = @klass.run(req, request.params) + sendfile = nil response.start(controller.status) do |head,out| controller.headers.each do |k, v| - [*v].each do |vi| - head[k] = vi + if k =~ /^X-SENDFILE$/i + sendfile = v + else + [*v].each do |vi| + head[k] = vi + end end end - if controller.body.respond_to? :read + response.send_header + + if sendfile + response.send_file(sendfile) + elsif controller.body.respond_to? :read while chunk = controller.body.read(16384) - out << chunk + @response.write(chunk) end if controller.body.respond_to? :close controller.body.close end else - out << controller.body + @response.write(controller.body) end end end diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb index 6496d90d..f9ad95f2 100644 --- a/lib/mongrel/handlers.rb +++ b/lib/mongrel/handlers.rb @@ -1,12 +1,3 @@ -require 'rubygems' -begin - require 'sendfile' - $mongrel_has_sendfile = true - STDERR.puts "** You have sendfile installed, will use that to serve files." -rescue Object - $mongrel_has_sendfile = false -end - module Mongrel # You implement your application handler with this. It's very light giving @@ -188,22 +179,10 @@ module Mongrel response.send_header if not header_only - begin - if $mongrel_has_sendfile - File.open(req, "rb") { |f| response.socket.sendfile(f) } - else - File.open(req, "rb") { |f| response.socket.write(f.read) } - end - rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF - # ignore these since it means the client closed off early - STDERR.puts "Client closed socket requesting file #{req}: #$!" - end - else - response.send_body # should send nothing + response.send_file(req) end end - # Process the request to either serve a file or a directory listing # if allowed (based on the listing_allowed paramter to the constructor). def process(request, response) diff --git a/test/test_response.rb b/test/test_response.rb index 36a29fb9..4659f32b 100644 --- a/test/test_response.rb +++ b/test/test_response.rb @@ -44,6 +44,25 @@ class ResponseTest < Test::Unit::TestCase assert io.length > 0, "output didn't have data" end + def test_response_file + contents = "PLAIN TEXT\r\nCONTENTS\r\n" + require 'tempfile' + tmpf = Tempfile.new("test_response_file") + tmpf.write(contents) + tmpf.rewind + io = StringIO.new + resp = HttpResponse.new(io) + resp.start(200) do |head,out| + head['Content-Type'] = 'text/plain' + resp.send_header + resp.send_file(tmpf.path) + end + io.rewind + tmpf.close + + assert io.length > 0, "output didn't have data" + assert io.read =~ /#{contents}\Z/, "output doesn't end with file payload" + end end