mirror of https://github.com/rails/rails
Add send_stream to do for dynamic streams what send_data does for static files (#41488)
This commit is contained in:
parent
b8d5279f17
commit
90049a4107
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue