Created
February 16, 2017 13:38
-
-
Save dsggregory/a0ad0363ff89029007ae58e000f4969d to your computer and use it in GitHub Desktop.
Rails sending a temporary file in the way of send_file()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Respond from a controller with a Tempfile and have the middleware unlink and close it. | |
# | |
# Send a file as ActionController::DataStreaming.send_file() does. This however, handles a Tempfile as input | |
# and unlinks/closes once the middleware is done with it. You cannot use send_file() to stream a temporary | |
# file and also control when it is unlinked in your controller because the RACK middleware actually performs | |
# the sending of the file AFTER the controller has returned. Rack may even pass the filename to the web server | |
# and have it make the actual delivery (X-Sendfile). | |
# | |
# Largely ripped directly from ActionController::DataStreaming. | |
# To implement in your Rails app, modify your application_controller.rb as follows: | |
# | |
# require 'path/to/send_tempfile.rb' | |
# class ApplicationController < ActionController::Base | |
# include TempfileStreaming | |
# ... | |
# end | |
# | |
# In your controller, call send_tempfile instead of send_file. Do not close or unlink the Tempfile you sent. | |
require 'action_controller/metal/exceptions' | |
module TempfileStreaming | |
extend ActiveSupport::Concern | |
include ActionController::Rendering | |
DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc: | |
DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc: | |
#protected | |
# Send the tempfile similar to send_file but closes the Tempfile at end to avoid a race condition of file availability. | |
# +io+ - A TempFile that this method will delete once processed. Caller MUST NOT delete or close. | |
# +options+ - same as ActionController::DataStreaming.send_file() | |
def send_tempfile(io, options = {}) #:doc: | |
raise MissingFile, "Input is not a TempFile" unless io.is_a?(Tempfile) | |
options[:filename] ||= 'stream' unless options[:url_based_filename] | |
send_file_headers! options | |
self.status = options[:status] || 200 | |
self.content_type = options[:content_type] if options.key?(:content_type) | |
self.response_body = TempFileBody.new(io) | |
end | |
class TempFileBody #:nodoc: | |
attr_reader :to_io | |
def initialize(io) | |
@to_io = io | |
@to_io.unlink # still readable until we call @to_id.close | |
end | |
# Force Rack:Sendfile to use this since self.response_body does not respond to to_path(). | |
def each | |
begin | |
while chunk = @to_io.read(16384) | |
yield chunk | |
end | |
ensure | |
# WARN: if RACK fails before calling this, the file remains open until the session exits. | |
@to_io.close | |
end | |
end | |
end | |
private | |
def send_file_headers!(options) | |
type_provided = options.has_key?(:type) | |
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) | |
raise ArgumentError, ":type option required" if content_type.nil? | |
if content_type.is_a?(Symbol) | |
extension = Mime[content_type] | |
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension | |
self.content_type = extension | |
else | |
if !type_provided && options[:filename] | |
# If type wasn't provided, try guessing from file extension. | |
content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete('.')) || content_type | |
end | |
self.content_type = content_type | |
end | |
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) | |
unless disposition.nil? | |
disposition = disposition.to_s | |
disposition += %(; filename="#{options[:filename]}") if options[:filename] | |
headers['Content-Disposition'] = disposition | |
end | |
headers['Content-Transfer-Encoding'] = 'binary' | |
response.sending_file = true | |
# 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 | |
# is called for handling the download is run, so let's workaround that | |
response.cache_control[:public] ||= false | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment