Add send_stream to do for dynamic streams what send_data does for static files (#41488)

This commit is contained in:
David Heinemeier Hansson 2021-02-18 22:35:36 +01:00 committed by GitHub
parent b8d5279f17
commit 90049a4107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 0 deletions

View File

@ -1,3 +1,17 @@
* Add `ActionController::Live#send_stream` that makes it more convenient to send generated streams:
```ruby
send_stream(filename: "subscribers.csv") do |stream|
stream.write "email_address,updated_at\n"
@subscribers.find_each do |subscriber|
stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
end
end
```
*DHH*
* `ActionDispatch::Request#content_type` now returned Content-Type header as it is.
Previously, `ActionDispatch::Request#content_type` returned value does NOT contain charset part.

View File

@ -282,6 +282,41 @@ module ActionController
response.close if response
end
# Sends a stream to the browser, which is helpful when you're generating exports or other running data where you
# don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type.
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
# If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
#
# Example of generating a csv export:
#
# send_stream(filename: "subscribers.csv") do |stream|
# stream.write "email_address,updated_at\n"
#
# @subscribers.find_each do |subscriber|
# stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
# end
# end
def send_stream(filename:, disposition: "attachment", type: nil)
response.headers["Content-Type"] =
(type.is_a?(Symbol) ? Mime[type].to_s : type) ||
Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete(".")) ||
"application/octet-stream"
response.headers["Content-Disposition"] =
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
yield response.stream
ensure
response.stream.close
end
private
# Spawn a new thread to serve up the controller in. This is to get
# around the fact that Rack isn't based around IOs and we need to use

View File

@ -144,6 +144,18 @@ module ActionController
response.stream.close
end
def basic_send_stream
send_stream(filename: "my.csv") do |stream|
stream.write "name,age\ndavid,41"
end
end
def send_stream_with_options
send_stream(filename: "export", disposition: "inline", type: :json) do |stream|
stream.write %[{ name: "David", age: 41 }]
end
end
def blocking_stream
response.headers["Content-Type"] = "text/event-stream"
%w{ hello world }.each do |word|
@ -300,6 +312,22 @@ module ActionController
assert_equal "text/event-stream", @response.headers["Content-Type"]
end
def test_send_stream
get :basic_send_stream
assert_equal "name,age\ndavid,41", @response.body
assert_equal "text/csv", @response.headers["Content-Type"]
assert_match "attachment", @response.headers["Content-Disposition"]
assert_match "my.csv", @response.headers["Content-Disposition"]
end
def test_send_stream_with_optons
get :send_stream_with_options
assert_equal %[{ name: "David", age: 41 }], @response.body
assert_equal "application/json", @response.headers["Content-Type"]
assert_match "inline", @response.headers["Content-Disposition"]
assert_match "export", @response.headers["Content-Disposition"]
end
def test_delayed_autoload_after_write_within_interlock_hook
# Simulate InterlockHook
ActiveSupport::Dependencies.interlock.start_running