summary refs log tree commit homepage
path: root/lib/rainbows/sendfile.rb
blob: 767c0f914233047e042513f6215359f21a381b8f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- encoding: binary -*-
# This middleware handles X-\Sendfile headers generated by applications
# or middlewares down the stack.  It should be placed at the top
# (outermost layer) of the middleware stack to avoid having its
# +to_path+ method clobbered by another middleware.
#
# This converts X-\Sendfile responses to bodies which 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 the opposite of Rack::Sendfile as it
# reverses the effect of Rack:::Sendfile.  Unlike many Ruby
# web servers, some configurations of \Rainbows! are capable of
# serving static files efficiently.
#
# === Compatibility (via IO.copy_stream in Ruby 1.9):
# * ThreadSpawn
# * ThreadPool
# * WriterThreadPool
# * WriterThreadSpawn
#
# === Compatibility (Ruby 1.8 and 1.9)
# * EventMachine
# * NeverBlock (using EventMachine)
#
# DO NOT use this middleware if you're proxying to \Rainbows! with a
# server that understands X-\Sendfile (e.g. Apache, Lighttpd) natively.
#
# This does NOT understand X-Accel-Redirect headers intended for nginx.
# X-Accel-Redirect requires the application to be highly coupled with
# the corresponding nginx configuration, and is thus too complicated to
# be worth supporting.
#
# Example config.ru:
#
#    use Rainbows::Sendfile
#    run lambda { |env|
#      path = "#{Dir.pwd}/random_blob"
#      [ 200,
#        {
#          'X-Sendfile' => path,
#          'Content-Type' => 'application/octet-stream'
#        },
#        []
#      ]
#    }

class Rainbows::Sendfile < Struct.new(:app)

  # 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_path) # :nodoc: all
    CONTENT_LENGTH = 'Content-Length'.freeze

    def self.new(path, headers)
      unless headers[CONTENT_LENGTH]
        stat = File.stat(path)
        headers[CONTENT_LENGTH] = stat.size.to_s if stat.file?
      end
      super(path)
    end

    # fallback in case our +to_path+ doesn't get handled for whatever reason
    def each
      buf = ''
      File.open(to_path) do |fp|
        yield buf while fp.read(0x4000, buf)
      end
    end
  end

  # :stopdoc:
  X_SENDFILE = 'X-Sendfile'
  # :startdoc:

  def call(env) # :nodoc:
    status, headers, body = app.call(env)
    headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
    if path = headers.delete(X_SENDFILE)
      body = Body.new(path, headers) unless body.respond_to?(:to_path)
    end
    [ status, headers, body ]
  end
end