Skip to content

Instantly share code, notes, and snippets.

@dsggregory
Created February 16, 2017 13:38
Show Gist options
  • Save dsggregory/a0ad0363ff89029007ae58e000f4969d to your computer and use it in GitHub Desktop.
Save dsggregory/a0ad0363ff89029007ae58e000f4969d to your computer and use it in GitHub Desktop.
Rails sending a temporary file in the way of send_file()
# 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