39

I am using following code for sending the file in Rails.

if File.exist?(file_path)
  send_file(file_path, type: 'text/excel') 
  File.delete(file_path)
end

In this I am trying to send the file and delete the file from server once it is been send successfully. But I am facing issue is, the delete operation is getting executed while sending is performing and due to that I don't see anything in browser.

So is there any way in Rails, once the send_file operation is completed delete the file from server.

Any help on this would be highly appreciated.

Thanks,
Chetan

Chetan Kalore
  • 403
  • 1
  • 4
  • 11
  • I don't this is possible inside the request handler, and you will want a delayed job or similar mechanism to clean up, allowing enough time for the download to of completed – Neil Slater Aug 14 '13 at 12:46

3 Answers3

51

Because you're using send_file, Rails will pass the request along to your HTTP server (nginx, apache, etc. - See the Rails documentation on send_file regarding X-Sendfile headers). Because of this, when you try to delete the file, Rails doesn't know that it's still being used.

You can try using send_data instead, which will block until the data is sent, allowing your File.delete request to succeed. Keep in mind that send_data requires a data stream as its argument though, not a path, so you need to open the file first:

File.open(file_path, 'r') do |f|
  send_data f.read, type: "text/excel"
end
File.delete(file_path)

The other option would be a background job that periodically checks for temp files to delete.

Dylan Markow
  • 123,080
  • 26
  • 284
  • 201
  • Also keep in mind that send_data will likely use more resources to send same content as `send_file`, but no need to obsess about the difference unless this will be a heavily-used feature of the site. – Neil Slater Aug 14 '13 at 13:51
  • @NeilSlater Yes, for a basic text file that generates quickly it shouldn't be a big deal. But for generating large/slow files, a send_file + background job solution would probably be better. – Dylan Markow Aug 14 '13 at 13:55
  • Thanks Dylan for responding on this. Following worked for me: if File.exist?(file_path) File.open(file_path, 'rb') do |f| send_data f.read, type: "application/excel", filename: "file_name.xls" end end – Chetan Kalore Aug 16 '13 at 06:19
  • Thanks Dylan for simple solution (y) – Taimoor Changaiz Nov 13 '15 at 14:25
7

If you are generating on the fly the file you are trying to send, a solution is to use the Tempfile class:

begin
  # The second argument is optional:
  temp_file = Tempfile.new(filename, temp_directory)

  # ... edit temp_file as needed.

  # By default, temp files are 0600,
  # so this line might be needed depending on your configuration:
  temp_file.chmod(0644)
  send_file temp_file
ensure
  temp_file.close
end

Contrary to what is indicated in this question, this works as expected (the file stays on the server long enough to be served, but ultimately gets deleted); this post seems to indicate this is due to updates in Rails 3.2.11, something I couldn’t verify.

Community
  • 1
  • 1
louije
  • 101
  • 1
  • 4
  • I'm pretty sure if you want to actually delete the tempfile, you need to do something like temp_file.unlink – Michael Wu Jun 27 '14 at 19:38
  • @MichaelWu According to the [documentation](http://www.ruby-doc.org/stdlib-2.1.2/libdoc/tempfile/rdoc/Tempfile.html#method-i-close), TempFile#close with no parameter ends up calling #unlink automatically, "when the object is finalized". It worked as expected for me. – louije Jul 04 '14 at 10:16
  • 6
    I wouldn't count on this working 100% of the time. It will delete the file when the Tempfile is garbage collected, and there are no guarantees that this will happen before nginx/apache has opened the file. It depends on the internal implementation of send_file in Rails. send_data is a better alternative. – mrbrdo Mar 10 '15 at 06:37
4

This works for me! With send_data you can delete file before send.

file = File.open(Rails.root.join('public', 'uploads', filename), "rb")
contents = file.read
file.close

File.delete(filepath) if File.exist?(filepath)

send_data(contents, :filename => filename)
kristina
  • 41
  • 4