How to serve a file for download from Rails? - ruby-on-rails

So this is probably a simple question, but I've never done it before.
I have a Rails action that queries a database and creates a csv string from the query result.
I'd like to take the query string, put it into a .csv file, and when the user makes the http request associated with this method, the .csv file will download onto the user's machine.
How can I do this?
UPDATE
The file is sending from rails, but my angular app on the front end (that requested the csv) is not downloading it.
Here is the angular code I'm using to request the file from the rails app
$scope.csvSubmit = function() {
var csv = $.post('http://ip_addr:3000/api/csv', { 'input': $scope.query_box });
csv.done(function(result){
//empty - after the request is sent I want the csv file to download
})
}

You can use the send_file method, passing the path to the file as the first argument, as see in Rails documentation.
UPDATE
You can use a temporary file to save the CSV, like this:
require 'tempfile'
# automatically creates a file in /tmp
file = Tempfile.new('data.csv', 'w')
file.write('my csv')
file.close
send_file(file.path)
# remove the file from /tmp
file.unlink
UPDATE 2: AngularJS download
There are two ways to accomplish this: you can add a hidden href to download the file in the page and click it, or redirect the user to the Rails URL that sends the file when he clicks in the button. Note that the redirect will use parameters in the url, so it won't work well depending on the structure of query_box.
To add a hidden href to the page with the CSV:
$scope.csvSubmit = function() {
var csv = $.post('http://ip_addr:3000/api/csv', { 'input': $scope.query_box });
csv.done(function(result){
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:attachment/csv,' + encodeURI(result);
hiddenElement.target = '_blank';
hiddenElement.download = 'filename.csv';
hiddenElement.click();
})
}
To use the redirect:
$scope.csvSubmit = function() {
var url = 'http://ip_addr:3000/api/csv/?' + 'input=' + encodeURI($scope.query_box);
window.location = url;
}

I've had to do this plenty of times before. You need to set the response headers to get the browser to force the download.
I like to use the comma gem for rendering csv. Using the gem all you need to do is add the following lines to your controller action.
respond_to do |format|
format.csv do
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=books.csv'
render :csv => Book.limited(50)
end
end
Then you just use the csv format and it works.
If you don't want to use comma. Just change the render line to render your csv string:
render :plain => csv_string_variable

Use send_data to generate a downloadable file from a string in just one line:
send_data #your_data, type: 'text/csv', disposition: 'attachment', filename: 'books.csv'

Related

Generate XML file and redirect to other view with Rails

I have a piece of code that generates an XML file. What I want to, and didn't find the solution, is to generate the XML file and ALSO redirect to another page, to give a feedback message.
My code is
def exportFiles
#files=FileToExport.getComponentToExport
recursive_tree= GitHubRepositorioService.getRecursiveTree('master')
GitHubService.updateFiles(#files, recursive_tree)
xml = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2)
respond_to do |format|
format.xml { send_data render_to_string(:exportFiles), filename: 'exported_module.xml', type: 'application/xml', disposition: 'attachment' }
end
FileToExport.setComponentToExport(nil)
end
As I already use "respond_to" I can't use another redirect sentence... so, there is a way to generate (downloading) that file and redirect to other view?
Unfortunately, this is not possible via the controller as you can't send two responses.
But you could do this via javascript for instance. See this topic for more info Rails how do I - export data with send_data then redirect_to a new page?

rails: create multiple download files from a controller#action?

I would like to create 2 pdf documents from a single click.
I have it working with one file, but I can't seem to get to make 2 download files to happen w/out browser complaining about DoubleRenderError.
This is the code in my controller, where I am calling it twice and breaks. If I do it just once, everything works fine.
respond_to do |format|
format.pdf do
pdf = PrintCardsFront.new(paperSize, cardCount, #cards)
send_data pdf.render, filename: "cards_front_side.pdf", type: "application/pdf"
pdf = PrintCardsBack.new(paperSize, cardCount, #cards)
send_data pdf.render, filename: "cards_backside_side.pdf", type: "application/pdf"
end
end
(Rails 4.2)
You can only send one file in the HTTP Protocol.
Try to zip your two files and send them to the user.

Rails send multiple files to browser using send_data or send_file

I am trying to send multiple files to the browser. I cant call send_data for every record like in the code below because i get a double render error. According to this post i need to create the files and zip them so i can send them in one request.
#records.each do |r|
ActiveRecord::Base.include_root_in_json = true
#json = r.to_json
a = ActiveSupport::MessageEncryptor.new(Rails.application.config.secret_token)
#json_encrypted = a.encrypt_and_sign(#json)
send_data #json_encrypted, :filename => "#{r.name}.json" }
end
I am creating an array of hashes with the #json_encrypted and the file_name for each record. My question is how can i create a file for every record and then bundle them into a zip file to then pass that zip file to send_file. Or better yet have multiple file download dialogs pop up on the screen. One for each file.
file_data = []
#records.each do |r|
ActiveRecord::Base.include_root_in_json = true
#json = r.to_json
a = ActiveSupport::MessageEncryptor.new(Rails.application.config.secret_token)
#json_encrypted = a.encrypt_and_sign(#json)
file_data << { json_encrypted: #json_encrypted, filename: "#{r.name}.json" }
end
So the issue i was having is that send_file does not respond to an ajax call which is how i was posting to that action. I got rid of the ajax, and am sending the necessary data though a hidden_field_tag and submitting it with jquery. The code below creates files for the data, zips them, and passes the zip file to send_data.
file_data.each do |hash|
hash.each do |key, value|
if key == :json_encrypted
#data = value
elsif key == :filename
#file_name = value
end
end
name = "#{Rails.root}/export_multiple/#{#file_name}"
File.open(name, "w+") {|f| f.write(#data)}
`zip -r export_selected "export_multiple/#{#file_name}"`
send_file "#{Rails.root}/export_selected.zip", type: "application/zip", disposition: 'attachment'
end

In rails can't we use send_file and redirect_to together? Gives Render and/or redirect were called multiple times error

Here's my action :
def download
#photo = Photo.friendly.find(params[:photo_id])
if #photo.free?
size = Size.find(params[:size])
img = Magick::Image.read(#photo.file.file.file).first
img.resize!(size.width, size.height)
path = "#{Rails.root}/tempfreedownload/#{size.width}x#{size.height}-#{#photo.file.file.filename}"
File.write(path, '')
img.write path
url1_data = open(path)
send_file url1_data
File.delete path
img.destroy!
downloads = #photo.downloads + 1
#photo.update_attribute(:downloads, downloads)
flash[:success] = 'Thanks for downloading this image!'
redirect_to(#photo)
return
else
render file: "#{Rails.root}/public/404.html", layout: false, status: 404
end
end
What I want to do is send the image to download and then redirect to the photo url. The problem is, in here I get a Render and/or redirect were called multiple times error. How can I fix this?
You are trying to send two responses. One being file data and one being a redirect. Rails doesn't allow you to do this in a controller. It can be achieved with javascript but it's a little hacky. See this: Rails how do I - export data with send_data then redirect_to a new page?
The issue is, you cannot send two responses to a single request. It is like ping pong: there is only one pong after the ping. So you need another ping from the user to hit the ball again on your side.
So when you send the file, the page does not change but the user gets the file.
What about doing it the other way round?
First redirect to the (landing) page, the user should see after the request and then redirect to the download via sendfile.
I did it via a meta-tag, which allows a delay for the redirect to the download page so the landing page will load completely before the redirect starts. Second advantage: no javascript.
class ApplicationController < ActionController::Base
def redirect_later(url, msg, delay = 5)
flash[:redirect_later] = {
url: url,
delay: delay,
msg: msg,
}
end
end
module ApplicationHelper
def redirect_later_tag
return nil unless flash.key?(:redirect_later)
url = flash[:redirect_later][:url]
delay = flash[:redirect_later][:delay]
flash.now[:notice] = flash[:redirect_later][:msg] % [delay]
flash.delete(:redirect_later)
tag("meta",
"http-equiv" => "refresh",
content: "#{delay}; URL=#{url}")
end
end
# in app/views/layouts/application.html.erb in the header section
# where the meta tags are put:
<%= redirect_later_tag %>
In your controller, split your controller action into two actions:
The first one collects all data which are needed to prepare the file to be downloaded and makes sure, they are correct.
In this action you redirect to the landing page. But before you redirect you do
redirect_later download_path, "Download starts in %d seconds."
# here comes your redirect to the landing page
download_path is the url to the second action, which prepares and serves the file via send_file.
You could even use the same action, if you can distinguish the request via the format of the file:
respond_to do |format|
format.html { # your code to redirect to the landing page}
format.png { # your code to send the file via send_file}
end
But keep in mind, that you make sure, that "download_path" includes the format of the file to be downloaded, otherwise you are looping all 5 seconds from the landing page to the landing page.
If you want to be 100% sure, the user gets the file, you could provide a link to the file in the message as a fallback - just in case the redirect won't work.
If you prepare/generate the file to be download: do not generate it with the first request otherwise you need to find a way to keep it somewhere until the second request. Generate it in the second request.
Please try this I think this will work fine :
def download
#photo = Photo.friendly.find(params[:photo_id])
if #photo.free?
size = Size.find(params[:size])
img = Magick::Image.read(#photo.file.file.file).first
img.resize!(size.width, size.height)
path = "#{Rails.root}/tempfreedownload/#{size.width}x#{size.height}-#{#photo.file.file.filename}"
File.write(path, '')
img.write path
url1_data = open(path)
send_file url1_data
File.delete path
img.destroy!
downloads = #photo.downloads + 1
#photo.update_attribute(:downloads, downloads)
flash[:success] = 'Thanks for downloading this image!'
redirect_to(#photo) and return
else
render file: "#{Rails.root}/public/404.html", layout: false, status: 404 and return
end
end

Allowing client to download files from ftp rails

I'm developping a web application in rails 4 and I'm currenty faced with a tiny issue.
I want to make the users of the website to be able to download files from a ftp by clicking on a link. I've decided to go on this:
def download
#item=Item.find(params[:id])
#item.dl_count += 1
#item.save
url = #item.file_url.to_s
redirect_to url and return
end
And, very basically, this in my view:
<%= link_to 'DL', controller: "items", action: "download"%>
However, I'm not quite satisfied by this, as it generates a few mistake like the fact that clicking the link create two GET methods, one responding by 403 Forbidden and the next with a 302 found...
Do you have any idea about how I could improve this?
In Rails you should do:
def download
#item=Item.find(params[:id])
#item.dl_count += 1
#item.save
url = #item.file_url.to_s
send_file url, type: 'image/jpeg', disposition: 'inline'
end
Take a look for more information http://apidock.com/rails/ActionController/DataStreaming/send_file
Note that send_file can send only from local file system.
If you need get file from remote source (should be secure location) like http://example.com/apps/uploads/tfm.zip and avoid store this file in server memory, you can first save file in #{RAILS_ROOT}/tmp/ or system /tmp and then send_file
data = open(url)
filename = "#{RAILS_ROOT}/tmp/my_temp_file"
File.open(filename, 'w') do |f|
f.write data.read
end
send_file filename, ...options...
If Rails can`t read file, you should check file permission

Resources