How to trigger download with Rails send_data from AJAX post - ruby-on-rails

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'

Related

When you upload multiple files using ActiveStorage, is it possible to determine what was just uploaded?

I have a model (let's call it Message) that's using Active Storage with has_many_attached to store multiple uploads through a Javascript routine. I'd like to know the URL of the uploads as soon as these are received and processed by the Update action in my controller. Ideally, the response would be in the form of a JSON string, containing the URL of the file that is just uploaded by the Javascript routine.
Other than having to loop through all the uploads, is there a way to obtain this information as they're uploaded?
I'm not using the DirectUpload javascript suggested in the Active Storage guide, in case you're wondering. The Javascript I'm using sends the files to a custom PATCH action /message/:id/new_upload (where :id is the ID of the Message record), with the BLOB file as its only parameter.
I've tried to use something like this:
def new_upload
#message.uploads.attach(params[:uploads])
respond_to do |format|
format.json {render json: { location: "The URL would go here"} }
end
end
But the attach line at the beginning doesn't return anything (obviously), all it is is doing is saving the attachments. Is there anything I can use to figure out what was just uploaded?
UPDATE
I've managed to make it "work" by doing something like this:
def new_upload
#message.uploads.attach(params[:uploads])
upload_url = rails_blob_url(#message.uploads.blobs.last)
respond_to do |format|
format.json {render json: { location: upload_url} }
end
end
However, this only works for one attachment (the last). Thankfully I only require to upload one file at a time, but I'd prefer to infer the URL by something other than checking the last upload.

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: handling errors within respond_to block (csv)

I have a search method on my controller that responds to either the html or CSV format. The html format renders the search results as expected, and I want the CSV format to work by downloading a CSV file of the results.
Most of the time, send_data is called and the CSV file is generated. However there are situations in which I don't want to generate the CSV, and instead show an error (for example when a user has used all of their allotted exports for the month). The following code is a simplified version of what I'd like to do, however this doesn't seem to like how I'm attempting to handle the error.
respond_to do |format|
format.html do
#results = ...
render "index"
end
format.csv do
#results = ...
if user_can_export?(#results)
send_data generate_csv(#results), filename: "Search-Results.csv"
else
flash[:error] = "Unable to export search results."
render "index"
end
end
end
Is there any way for me to break out of this block and render HTML or am I stuck generating a csv file here? I'd prefer to not handle this situation by sending a csv file with an error message contained in it, but that seems like my best option at the moment. Help is appreciated!
You need to set the content-type header to text/html instead of text/csv.
render template: "things/index.html.erb", content_type: "text/html"
Also if you want to display a flash message in the current request cycle you need to use flash.now[:error] = "Unable to export search results"

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

How do can I show image files from outside the public directory?

It's pretty trivial to use the image_tag method to serve an image in the public directory, but what about images that I don't want to be publicly accessible, images for admins only, maybe.
How should my controller and view look so that an image_tag method serves an image from root/private/images rather than root/public. Also, that would be completely secure, right? If I had private images, and wanted to serve them only to admins, a simple if statement to check if the user is an admin and then using an image_tag to serve them the private image from outside the public folder would be secure?
I'd be interested in how to do this without paperclip. (paperclip not even mentioned so it's not a duplicate)
Assuming the image is related to a model such as one with paperclip attached, you could do this. (And I'd really recommend this).
def show
#image = Image.find(params[:id])
published_image = #image.published_image
respond_to do |format|
format.all {
if published_image
send_file published_image.attached_image.path, :type=> published_image.attached_image_content_type
else
raise ActionController::RoutingError.new('Not Found')
end
}
end
end
Assuming you have another way to tell this, you could have all routes beginning with /admin/image route to a controller action, and then there call render file on the rest of the path. Example:
routes.rb
get 'admin/photo/*other', to: 'admin#photos'
admin.controller.rb
def photos
if current_user.admin?
render file: Rails.root.join("private","admin","images",request.path.gsub("/admin/photo/","")
else
render :file => "public/401.html", :status => :unauthorized
end
end
view
image_tag "/admin/photos/id.jpg"
This would in theory work, but it might be more complicated than i'm thinking
Images here would be hosted in public/admin/images, but linked to as /admin/photos/id
I'm not sure how you are handling authentication for admins but assuming you have a separate login.
Store your images as BLOB in MySQL.
Return the requested image files using send_data in rails controller with authentication
This link have an example

Resources