From cf122e941e10c2812b7ba5e75c053a28950ddcb6 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 3 Jun 2010 15:23:01 -0700 Subject: add Rainbows::Sendfile middleware This lets most concurrency models understand and process X-Sendfile efficiently with IO.copy_stream under Ruby 1.9. EventMachine can take advantage of this middleware under both Ruby 1.8 and Ruby 1.9. --- lib/rainbows.rb | 1 + lib/rainbows/sendfile.rb | 67 +++++++++++++++++++++++++++++++++++++++++++++ t/t9001-sendfile-to-path.sh | 45 ++++++++++++++++++++++++++++++ t/t9001.ru | 11 ++++++++ 4 files changed, 124 insertions(+) create mode 100644 lib/rainbows/sendfile.rb create mode 100755 t/t9001-sendfile-to-path.sh create mode 100644 t/t9001.ru diff --git a/lib/rainbows.rb b/lib/rainbows.rb index dac68c2..e186549 100644 --- a/lib/rainbows.rb +++ b/lib/rainbows.rb @@ -32,6 +32,7 @@ module Rainbows require 'rainbows/http_response' require 'rainbows/base' require 'rainbows/tee_input' + autoload :Sendfile, 'rainbows/sendfile' autoload :AppPool, 'rainbows/app_pool' autoload :DevFdResponse, 'rainbows/dev_fd_response' autoload :MaxBody, 'rainbows/max_body' diff --git a/lib/rainbows/sendfile.rb b/lib/rainbows/sendfile.rb new file mode 100644 index 0000000..1fa832c --- /dev/null +++ b/lib/rainbows/sendfile.rb @@ -0,0 +1,67 @@ +# -*- encoding: binary -*- +module Rainbows + +# Convert X-Sendfile headers into Rack response bodies that respond +# to the +to_path+ method which allows certain concurrency models to +# serve efficiently using sendfile() or similar. With multithreaded +# models under Ruby 1.9, IO.copy_stream will be used. +# +# This middleware is recommended for EventMachine users regardless +# of Ruby version and 1.9 users with any Thread-based concurrency +# models. DO NOT use this middleware if you're proxying \Rainbows! +# with a server (e.g. Apache, Lighttpd) that understands X-Sendfile +# natively. +# +# This does NOT understand X-Accel-Redirect headers intended for +# nginx, that is much more complicated to configure and support +# as it is highly coupled with the corresponding nginx configuration. + +class Sendfile < Struct.new(:app) + + # :nodoc: + HH = Rack::Utils::HeaderHash + + # Body wrapper, this allows us to fall back gracefully to + # #each in case a given concurrency model does not optimize + # #to_path calls. + class Body < Struct.new(:to_io) + + def initialize(path, headers) + # Rainbows! will try #to_io if #to_path exists to avoid unnecessary + # open() calls. + self.to_io = File.open(path, 'rb') + + unless headers['Content-Length'] + stat = to_io.stat + headers['Content-Length'] = stat.size.to_s if stat.file? + end + end + + def to_path + to_io.path + end + + # fallback in case our #to_path doesn't get handled for whatever reason + def each(&block) + buf = '' + while to_io.read(0x4000, buf) + yield buf + end + end + + def close + to_io.close + end + end + + def call(env) + status, headers, body = app.call(env) + headers = HH.new(headers) + if path = headers.delete('X-Sendfile') + body = Body.new(path, headers) unless body.respond_to?(:to_path) + end + [ status, headers, body ] + end +end + +end diff --git a/t/t9001-sendfile-to-path.sh b/t/t9001-sendfile-to-path.sh new file mode 100755 index 0000000..aa75f22 --- /dev/null +++ b/t/t9001-sendfile-to-path.sh @@ -0,0 +1,45 @@ +#!/bin/sh +. ./test-lib.sh + +t_plan 7 "Sendfile middleware test for $model" + +t_begin "configure and start" && { + rtmpfiles curl_out curl_err + rainbows_setup + + # do not allow default middleware to be loaded since it may + # kill body#to_path + rainbows -E none -D t9001.ru -c $unicorn_config + rainbows_wait_start +} + +t_begin "hit with curl" && { + curl -sSfv http://$listen/ > $curl_out 2> $curl_err +} + +t_begin "kill server" && { + kill $rainbows_pid +} + +t_begin "file matches source" && { + cmp $curl_out random_blob +} + +t_begin "no errors in Rainbows! stderr" && { + check_stderr +} + +t_begin "X-Sendfile does not show up in headers" && { + dbgcat curl_err + if grep -i x-sendfile $curl_err + then + die "X-Sendfile did show up!" + fi +} + +t_begin "Content-Length is set correctly in headers" && { + expect=$(wc -c < random_blob) + grep "^< Content-Length: $expect" $curl_err +} + +t_done diff --git a/t/t9001.ru b/t/t9001.ru new file mode 100644 index 0000000..0a523b9 --- /dev/null +++ b/t/t9001.ru @@ -0,0 +1,11 @@ +use Rainbows::Sendfile +run lambda { |env| + path = "#{Dir.pwd}/random_blob" + [ 200, + { + 'X-Sendfile' => path, + 'Content-Type' => 'application/octet-stream' + }, + [] + ] +} -- cgit v1.2.3-24-ge0c7