add "copy_stream" config directive
[rainbows.git] / lib / rainbows / response.rb
blobfac2c0ef92e66d6b2c7226f4eb46c7f4ea167060
1 # -*- encoding: binary -*-
2 # :enddoc:
3 module Rainbows::Response
4   include Unicorn::HttpResponse
5   Close = "close"
6   KeepAlive = "keep-alive"
7   Content_Length = "Content-Length".freeze
8   Transfer_Encoding = "Transfer-Encoding".freeze
9   Rainbows.config!(self, :copy_stream)
11   # private file class for IO objects opened by Rainbows! itself (and not
12   # the app or middleware)
13   class F < File; end
15   # called after forking
16   def self.setup(klass)
17     Kgio.accept_class = Rainbows::Client
18     0 == Rainbows.server.keepalive_timeout and
19       Rainbows::HttpParser.keepalive_requests = 0
20   end
22   def write_headers(status, headers, alive)
23     @hp.headers? or return
24     status = CODES[status.to_i] || status
25     buf = "HTTP/1.1 #{status}\r\n" \
26           "Date: #{httpdate}\r\n" \
27           "Status: #{status}\r\n" \
28           "Connection: #{alive ? KeepAlive : Close}\r\n"
29     headers.each do |key, value|
30       next if %r{\A(?:Date\z|Connection\z)}i =~ key
31       if value =~ /\n/
32         # avoiding blank, key-only cookies with /\n+/
33         buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
34       else
35         buf << "#{key}: #{value}\r\n"
36       end
37     end
38     write(buf << CRLF)
39   end
41   def close_if_private(io)
42     io.close if F === io
43   end
45   def io_for_fd(fd)
46     Rainbows::FD_MAP.delete(fd) || F.for_fd(fd)
47   end
49   # to_io is not part of the Rack spec, but make an exception here
50   # since we can conserve path lookups and file descriptors.
51   # \Rainbows! will never get here without checking for the existence
52   # of body.to_path first.
53   def body_to_io(body)
54     if body.respond_to?(:to_io)
55       body.to_io
56     else
57       # try to take advantage of Rainbows::DevFdResponse, calling F.open
58       # is a last resort
59       path = body.to_path
60       %r{\A/dev/fd/(\d+)\z} =~ path ? io_for_fd($1.to_i) : F.open(path)
61     end
62   end
64   module Each
65     # generic body writer, used for most dynamically-generated responses
66     def write_body_each(body)
67       body.each { |chunk| write(chunk) }
68     end
70     # generic response writer, used for most dynamically-generated responses
71     # and also when copy_stream and/or IO#trysendfile is unavailable
72     def write_response(status, headers, body, alive)
73       write_headers(status, headers, alive)
74       write_body_each(body)
75       ensure
76         body.close if body.respond_to?(:close)
77     end
78   end
79   include Each
81   if IO.method_defined?(:trysendfile)
82     module Sendfile
83       def write_body_file(body, range)
84         io = body_to_io(body)
85         range ? sendfile(io, range[0], range[1]) : sendfile(io, 0)
86         ensure
87           close_if_private(io)
88       end
89     end
90     include Sendfile
91   end
93   if COPY_STREAM
94     unless IO.method_defined?(:trysendfile)
95       module CopyStream
96         def write_body_file(body, range)
97           range ? COPY_STREAM.copy_stream(body, self, range[1], range[0]) :
98                   COPY_STREAM.copy_stream(body, self, nil, 0)
99         end
100       end
101       include CopyStream
102     end
104     # write_body_stream is an alias for write_body_each if copy_stream
105     # isn't used or available.
106     def write_body_stream(body)
107       COPY_STREAM.copy_stream(io = body_to_io(body), self)
108       ensure
109         close_if_private(io)
110     end
111   else # ! COPY_STREAM
112     alias write_body_stream write_body_each
113   end  # ! COPY_STREAM
115   if IO.method_defined?(:trysendfile) || COPY_STREAM
116     HTTP_RANGE = 'HTTP_RANGE'
117     Content_Range = 'Content-Range'.freeze
119     # This does not support multipart responses (does anybody actually
120     # use those?)
121     def sendfile_range(status, headers)
122       200 == status.to_i &&
123       /\Abytes=(\d+-\d*|\d*-\d+)\z/ =~ @hp.env[HTTP_RANGE] or
124         return
125       a, b = $1.split(/-/)
127       # HeaderHash is quite expensive, and Rack::File currently
128       # uses a regular Ruby Hash with properly-cased headers the
129       # same way they're presented in rfc2616.
130       headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
131       clen = headers[Content_Length] or return
132       size = clen.to_i
134       if b.nil? # bytes=M-
135         offset = a.to_i
136         count = size - offset
137       elsif a.empty? # bytes=-N
138         offset = size - b.to_i
139         count = size - offset
140       else  # bytes=M-N
141         offset = a.to_i
142         count = b.to_i + 1 - offset
143       end
145       if 0 > count || offset >= size
146         headers[Content_Length] = "0"
147         headers[Content_Range] = "bytes */#{clen}"
148         return 416, headers, nil
149       else
150         count = size if count > size
151         headers[Content_Length] = count.to_s
152         headers[Content_Range] = "bytes #{offset}-#{offset+count-1}/#{clen}"
153         return 206, headers, [ offset, count ]
154       end
155     end
157     def write_response_path(status, headers, body, alive)
158       if File.file?(body.to_path)
159         if r = sendfile_range(status, headers)
160           status, headers, range = r
161           write_headers(status, headers, alive)
162           write_body_file(body, range) if range
163         else
164           write_headers(status, headers, alive)
165           write_body_file(body, nil)
166         end
167       else
168         write_headers(status, headers, alive)
169         write_body_stream(body)
170       end
171       ensure
172         body.close if body.respond_to?(:close)
173     end
175     module ToPath
176       def write_response(status, headers, body, alive)
177         if body.respond_to?(:to_path)
178           write_response_path(status, headers, body, alive)
179         else
180           super
181         end
182       end
183     end
184     include ToPath
185   end # COPY_STREAM || IO.method_defined?(:trysendfile)