Skip to content

Commit

Permalink
Introduce send_file :x_sendfile => true to send an X-Sendfile respons…
Browse files Browse the repository at this point in the history
…e header.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8628 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
jeremy committed Jan 11, 2008
1 parent ebfd03b commit 9889d86
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 25 deletions.
2 changes: 2 additions & 0 deletions actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*

* Introduce send_file :x_sendfile => true to send an X-Sendfile response header. [Jeremy Kemper]

* Fixed ActionView::Helpers::ActiveRecordHelper::form for when protect_from_forgery is used #10739 [jeremyevans]

* Provide nicer access to HTTP Headers. Instead of request.env["HTTP_REFERRER"] you can now use request.headers["Referrer"]. [Koz]
Expand Down
54 changes: 31 additions & 23 deletions actionpack/lib/action_controller/streaming.rb
Expand Up @@ -4,10 +4,13 @@ module Streaming
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,
:disposition => 'attachment'.freeze,
:stream => true,
:buffer_size => 4096
:stream => true,
:buffer_size => 4096,
:x_sendfile => false
}.freeze

X_SENDFILE_HEADER = 'X-Sendfile'.freeze

protected
# Sends the file by streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes
Expand All @@ -22,15 +25,15 @@ module Streaming
# Defaults to File.basename(path).
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:stream</tt> - whether to send the file to the user agent as it is read (true)
# or to read the entire file before sending (false). Defaults to true.
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# * <tt>:url_based_filename</tt> - set to true if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# * <tt>:url_based_filename</tt> - set to true if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting :filename overrides this option).
#
# The default Content-Type and Content-Disposition headers are
Expand Down Expand Up @@ -67,19 +70,24 @@ def send_file(path, options = {}) #:doc:

@performed_render = false

if options[:stream]
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
while buf = file.read(len)
output.write(buf)
end
end
}
if options[:x_sendfile]
logger.info "Sending #{X_SENDFILE_HEADER} header #{path}" if logger
head options[:status], X_SENDFILE_HEADER => path
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
if options[:stream]
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
while buf = file.read(len)
output.write(buf)
end
end
}
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
end
end
end

Expand All @@ -90,7 +98,7 @@ def send_file(path, options = {}) #:doc:
# * <tt>:filename</tt> - Suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
#
Expand All @@ -105,7 +113,7 @@ def send_file(path, options = {}) #:doc:
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" unless logger.nil?
logger.info "Sending data #{options[:filename]}" if logger
send_file_headers! options.merge(:length => data.size)
@performed_render = false
render :status => options[:status], :text => data
Expand All @@ -130,10 +138,10 @@ def send_file_headers!(options)
)

# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache'
end
Expand Down
15 changes: 13 additions & 2 deletions actionpack/test/controller/send_file_test.rb
Expand Up @@ -27,7 +27,7 @@ class SendFileTest < Test::Unit::TestCase
include TestFileUtils

Mime::Type.register "image/png", :png unless defined? Mime::PNG

def setup
@controller = SendFileController.new
@request = ActionController::TestRequest.new
Expand Down Expand Up @@ -55,7 +55,7 @@ def test_file_stream
assert_nothing_raised { response.body.call(response, output) }
assert_equal file_data, output.string
end

def test_file_url_based_filename
@controller.options = { :url_based_filename => true }
response = nil
Expand All @@ -64,6 +64,17 @@ def test_file_url_based_filename
assert_equal "attachment", response.headers["Content-Disposition"]
end

def test_x_sendfile_header
@controller.options = { :x_sendfile => true }

response = nil
assert_nothing_raised { response = process('file') }
assert_not_nil response

assert_equal @controller.file_path, response.headers['X-Sendfile']
assert response.body.blank?
end

def test_data
response = nil
assert_nothing_raised { response = process('data') }
Expand Down

0 comments on commit 9889d86

Please sign in to comment.