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
Related
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
In the index view, there is a link to download file:
<%= link_to filename, listing_download_path(:file => filename) %>
In the controller:
def download
pathname = File.join(USER_FOLDER, params[:file])
if File.file?(pathname)
send_file pathname
end
end
end
When the user click download, a file download popup is shown. What's happen after the file is downloaded? Does rails just sit there and do nothing more? If I delete the send_file line, dwonload.html.erb will be rendered. Does send_file skip view rendering?
What if I want to show soemthing like "You have downloaded ..."?
The send_file is a render itself. You could use the approach proposed in this question:
Rails: send_file never renders page or DoubleRender error
Basically your download link will send to a "success" view, from which you call the download file method automatically.
I'm trying to use send_data to return a PNG image as the response for a ajax post request. How do I get the browser to trigger a download on the success callback?
Details
I'm generating a large base64 image using canvas.toDataURL(), and then posting it to Rails (v3.2.6). Rails decodes it to a binary PNG, and sends the image back to the client.
I've also tried send_file but it has the same issue.
Other options
Download image client side: We can't do this because (1) Safari crashes on large base64 URLs, and (2) Safari does not yet support the download attribute on anchor tags which I would need to specify the downloaded image filename.
Use a $.get instead of $.post: We can't do this because we need to send our canvas.toDataURL() with the request to the server. GET requests URIs have size limitations.
create a function in controller
def ajax_download
send_file "path_to_file/" + params[:file]
end
and then in controller action
respond_to do |format|
#java_url = "/home/ajax_download?file=#{file_name}"
format.js {render :partial => "downloadFile"}
end
and make a partial in view folder name with _downloadFile.js.erb and write this line
window.location.href = "<%=#java_url %>"
You can't download a file to disk from JS. It's a security concern. See the blog post below for a good workaround.
http://johnculviner.com/post/2012/03/22/Ajax-like-feature-rich-file-downloads-with-jQuery-File-Download.aspx
Do not just copy and paste the accepted answer. It is a massive security risk that cannot be understated. Although the technique is clever, delivering a file based on a parameter anybody can enter allows access to any file anybody can imagine is lying around.
Here's an example of a more secure way to use the same technique. It assumes there is a user logged in who has an API token, but you should be able to adapt it to your own scenario.
In the action:
current_user.pending_download = file_name
current_user.save!
respond_to do |format|
#java_url = "/ajax_download?token=#{current_user.api_token}"
format.js {render :partial => "downloadFile"}
end
Create a function in the controller
def ajax_download
if params[:token] == current_user.api_token
send_file "path_to_file/" + current_user.pending_download
current_user.pending_download = ''
current_user.save!
else
redirect_to root_path, notice: "Unauthorized"
end
end
Make a partial in view folder name with _downloadFile.js.erb
window.location.href = "<%=#java_url %>"
And of course you will need a route that points to /ajax_download in routes.rb
get 'ajax_download', to: 'controller#ajax_download'
I moved my file storage to Rackspace Cloudfiles and it broke my send_file action.
old
def full_res_download
#asset = Asset.find(params[:id])
#file = "#{Rails.root}/public#{#asset.full_res}"
send_file #file
end
new
def full_res_download
#asset = Asset.find(params[:id])
#file = "http://86e.r54.cf1.rackcdn.com/uploads/fake/filepath.mov"
send_file #file
end
When the files were in the public file. the code worked great. When you click in the link the file would download and the webpage would not change. Now it gives this error.
Cannot read file http://86e.r54.cf1.rackcdn.com/uploads/fake/filepath.mov
What am i missing?
Thank you so much for your time.
what worked
def full_res_download
#asset = Asset.find(params[:id])
#file = open("http://86e.r54.cf1.rackcdn.com/uploads/fake/filepath.mov")
send_file( #file, :filename => File.basename(#asset.file.path.to_s))
end
real code
controler.rb
def web_video_download
#asset = Asset.find(params[:id])
#file = open(CDNURL + #asset.video_file.path.to_s)
send_file( #file, :filename => File.basename(#asset.video_file.path.to_s))
end
development.rb
CDNURL = "http://86e.r54.cf1.rackcdn.com/"
send_file opens a local file and sends it using a rack middleware. You should just redirect to the url since you're not hosting the file anymore.
As one of the comments points out, in some situations you may not be able to use a redirect, for various reasons. If this is the case, you would have to download the file and relay it back to the user after retrieving it. The effect of this is that the transfer to the user would do the following:
Request gets to your server, processing in your action begins.
Your action requests the file from the CDN, and waits until the file has been fully retrieved.
Your server can now relay the file on to the end user.
This is as compared to the case with a redirect:
Request gets to your server, processing in your action begins.
Your action redirects the user to the CDN.
In both cases the user has to wait for two full connections, but your server has saved some work. As a result, it is more efficient to use a redirect when circumstances allow.
Ok, I've tried all kinds of stuff and I'm not entirely sure this will work. The pdfs I need to merge are on the server and the links to them are hardcoded. pdftk works locally in my terminal, but not with these remote links. So I'm unsure if this will work once I get it up to production.
Basically, I'm trying to write a method that will retrieve a selected group of pdfs and merge them into one pdf for the user to download.
But I'm having a hard time deciphering how to utilize tempfiles and running terminal commands through the rails app.
Here is the method:
def create
#routes = TransitRoute.find(params[:selected_routes])
#selected_routes = #routes.collect {|x| x.new_pdf_link + " "}
Tempfile.open('prefix', "#{Rails.root}/tmp") do |f|
f.print("pdftk #{#selected_routes} cat output temporary.pdf")
f.flush
f.read
end
respond_to do |format|
format.html
end
end
I have a couple questions:
My tempfile has the correct command line written to it:
pdftk 1.pdf 2.pdf cat output new.pdf
How do I get this line run so that the new.pdf is created?
Am I supposed to replace the tempfile with the new pdf, or write the new pdf to it or just make the new pdf in it's own location? If the latter, how do I get it to be temporary?
How do I get a link to the new pdf so users can download it?
Some of this may be basic stuff, but I've never had to mess with tempfiles of making pdfs dynamically like this.
Oh yeah, and this app is also in Rails 2.3
Thanks for any help.
Ok, I have it working. Here's the new code incase someone has advice for improvement or has the same question:
def create
file = Tempfile.new('temp_route_pdf', "#{Rails.root}/tmp/")
#routes = TransitRoute.find(params[:selected_routes])
selected_routes = #routes.collect {|x| x.new_pdf_link + " "}
route_names = #routes.collect {|x| x.short_name + "_"}
#generated_pdf_file = "#{route_names}routes.pdf"
`pdftk #{selected_routes}cat output #{file.path}`
raise Exception unless $?.success?
send_file( "#{file.path}",
:type => "application/pdf",
:filename => "#{#generated_pdf_file}",
:disposition => 'inline')
end