Skip to content

Commit 7b87c78

Browse files
committed
make sure "optimized" File streaming gets used
- with Rails 3.x there seems to always be a body wrapping even while returning a File instance body, thus check for body_parts on the body object - besides this java file channel use avoids a nasty bug with 1.6.7 in --1.9 mode - while binary files are iterated using File.each the lines end up havind the default internal encoding which in case of rails is Encoding.default_internal = Encoding::UTF_8 and thus end up being incorrectly converted to_java_bytes (@see #107)
1 parent fd28abb commit 7b87c78

File tree

2 files changed

+55
-16
lines changed

2 files changed

+55
-16
lines changed

src/main/ruby/jruby/rack/response.rb

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ class Response
1111
include org.jruby.rack.RackResponse
1212
java_import java.nio.channels.Channels
1313

14-
@@object_polluted = begin
15-
# Fixnum should not have this method, and it
16-
# shouldn't be on Object
17-
Fixnum.method('to_channel').owner == Object
18-
rescue
19-
false
20-
end
21-
2214
def initialize(arr)
2315
@status, @headers, @body = *arr
2416
end
@@ -36,9 +28,9 @@ def chunked?
3628
end
3729

3830
def getBody
39-
b = ""
40-
@body.each {|part| b << part }
41-
b
31+
body = ""
32+
@body.each { |part| body << part }
33+
body
4234
ensure
4335
@body.close if @body.respond_to?(:close)
4436
end
@@ -91,12 +83,19 @@ def write_body(response)
9183
begin
9284
if @body.respond_to?(:call) && ! @body.respond_to?(:each)
9385
@body.call(outputstream)
94-
elsif @body.respond_to?(:to_channel) && !object_polluted_with_anyio?(@body, :to_channel)
86+
elsif @body.respond_to?(:to_channel) &&
87+
! object_polluted_with_anyio?(@body, :to_channel)
9588
@body = @body.to_channel # so that we close the channel
9689
transfer_channel(@body, outputstream)
97-
elsif @body.respond_to?(:to_inputstream) && !object_polluted_with_anyio?(@body, :to_inputstream)
90+
elsif @body.respond_to?(:to_inputstream) &&
91+
! object_polluted_with_anyio?(@body, :to_inputstream)
9892
@body = @body.to_inputstream # so that we close the stream
9993
transfer_channel(Channels.newChannel(@body), outputstream)
94+
elsif @body.respond_to?(:body_parts) && @body.body_parts.respond_to?(:to_channel) &&
95+
! object_polluted_with_anyio?(@body.body_parts, :to_channel)
96+
# ActionDispatch::Response "raw" body access in case it's a File
97+
@body = @body.body_parts.to_channel # so that we close the channel
98+
transfer_channel(@body, outputstream)
10099
else
101100
# 1.8 has a String#each method but 1.9 does not :
102101
method = @body.respond_to?(:each_line) ? :each_line : :each
@@ -117,12 +116,16 @@ def write_body(response)
117116
end
118117
end
119118

119+
private
120+
121+
BUFFER_SIZE = 16 * 1024
122+
120123
def transfer_channel(channel, outputstream)
121124
outputchannel = Channels.newChannel outputstream
122125
if channel.respond_to?(:transfer_to)
123126
channel.transfer_to(0, channel.size, outputchannel)
124127
else
125-
buffer = java.nio.ByteBuffer.allocate(16384)
128+
buffer = java.nio.ByteBuffer.allocate(BUFFER_SIZE)
126129
while channel.read(buffer) != -1
127130
buffer.flip
128131
outputchannel.write(buffer)
@@ -135,14 +138,22 @@ def transfer_channel(channel, outputstream)
135138
end
136139
end
137140

141+
@@object_polluted = begin
142+
# Fixnum should not have this method, and it
143+
# shouldn't be on Object
144+
Fixnum.method('to_channel').owner == Object
145+
rescue
146+
false
147+
end
148+
138149
# See http://bugs.jruby.org/5444 - we need to account for pre-1.6
139150
# JRuby where Object was polluted with #to_channel by
140151
# IOJavaAddions.AnyIO
141152
def object_polluted_with_anyio?(obj, meth)
142-
begin
153+
@@object_polluted && begin
143154
# The object should not have this method, and
144155
# it shouldn't be on Object
145-
@@object_polluted && obj.method(meth).owner == Object
156+
obj.method(meth).owner == Object
146157
rescue
147158
false
148159
end

src/spec/ruby/jruby/rack/response_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,35 @@ class << str; undef_method :each; end if str.respond_to?(:each)
199199

200200
@response.write_body(@servlet_response)
201201
end
202+
203+
it "streams a file using a channel if wrapped in body_parts",
204+
:lib => [ :rails30, :rails31, :rails32 ] do
205+
require 'action_dispatch/http/response'
206+
207+
path = File.expand_path('../../files/image.jpg', File.dirname(__FILE__))
208+
file = File.open(path, 'rb')
209+
headers = {
210+
"Content-Disposition"=>"attachment; filename=\"image.jpg\"",
211+
"Content-Transfer-Encoding"=>"binary",
212+
"Content-Type"=>"image/jpeg"
213+
}
214+
# we're emulating the body how rails returns it (for a file response)
215+
body = ActionDispatch::Response.new(200, headers, file)
216+
body = Rack::BodyProxy.new(body) { nil } if defined?(Rack::BodyProxy)
217+
# Rack::BodyProxy not available with Rails 3.0.x
218+
# with 3.2 there's even more wrapping with ActionDispatch::BodyProxy
219+
220+
response = JRuby::Rack::Response.new [ 200, headers, body ]
221+
stream = self.stream
222+
response.should_receive(:transfer_channel).with do |ch, s|
223+
s.should == stream
224+
ch.should be_a java.nio.channels.FileChannel
225+
ch.size.should == File.size(path)
226+
end
202227

228+
response.write_body(@servlet_response)
229+
end
230+
203231
it "uses #transfer_to to copy the stream if available" do
204232
channel = mock "channel"
205233
@body.should_receive(:to_channel).and_return channel

0 commit comments

Comments
 (0)