Rails 4, asset pipeline causes user downloadable files to be downloaded twice - ruby-on-rails

I have a folder in my app directory named "uploads" where users can upload files and download files. I don't want the uploads folder to be in the public directory because I want to control download authorization.
In my controller, I have:
send_file Rails.root.join('app', 'uploads', filename), :type => 'application/zip', :disposition => 'inline', :x_sendfile=>true
This actually works fine. The problem is that when I'm on the production server, when I run the rake assets:precompile, and have an assets directory, the file downloads twice. The first time the file downloads, the browser acts as if nothing is going on (no loading spinning), but I see data being transferred in the Google Chrome web developer Network tab. Then after the file has been downloaded, a prompt comes up asking the user if he/she wants to download the file.
Removing the assets folder in the public directory gets rid of this problem, but I want to use the asset pipeline. I also tried changing the asset pipeline requires from require_tree to require_directory.
Does anyone know how to get send_file working properly with the asset pipeline?
Thanks.

For anyone having this problem, I solved it. Pass
'data-no-turbolink' => true
into the link_to helper to stop Turbolinks from messing with the download.
https://github.com/rails/turbolinks/issues/182

But if you are using a form with turbooboost = true, instead of link_to, or even with a link_to you can do it like this:
Inside your controller, and inside your action put:
def download
respond_to do |format|
format.html do
data = "Hello World!"
filename = "Your_filename.docx"
send_data(data, type: 'application/docx', filename: filename)
end
format.js { render js: "window.location.href = '#{controller_download_path(params)}';" }
end
end
Replace controller_download_path with a path to your download action,
and place in your routes both post and get for the same path:
post '/download' => 'your_controller#download', as: :controller_download
get '/download' => 'your_controller#download', as: :controller_download

Related

S3 save old url, change paperclip config, set new url as old

So here is the thing: currently our files, when user downloads them, have names like 897123uiojdkashdu182uiej.pdf. I need to change that to file-name.pdf.
And logically I go and change paperclip.rb config from this:
Paperclip::Attachment.default_options.update({
path: '/:hash.:extension',
hash_secret: Rails.application.secrets.secret_key_base
})
to this:
Paperclip::Attachment.default_options.update({
path: "/attachment/#{SecureRandom.urlsafe_base64(64)}/:filename",
hash_secret: Rails.application.secrets.secret_key_base
})
which works just fine, filenames are great. However, old files are now unaccessable due to the change in the path. So I came up with the following decision
First I made a rake task which will store the old paths in the database:
namespace :paperclip do
desc "Set old urls for attachments"
task :update_old_urls => :environment do
Asset.find_each do |asset|
if asset.attachment
attachment_url = asset.attachment.try!(:url)
file_url = "https:#{attachment_url}"
puts "Set old url asset attachment #{asset.id} - #{file_url}"
asset.update(old_url: file_url)
else
puts "No attachment found in asset #{asset.id}"
end
end
end
end
Now the asset.old_url stores the current url of the file. Then I go and change the config, making the file unaccessable.
Now it's time for the new rake task:
require 'uri'
require 'open-uri'
namespace :paperclip do
desc "Recreate attachments and save them to new destination"
task :move_attachments => :environment do
Asset.find_each do |asset|
unless asset.old_url.blank?
url = asset.old_url
filename = File.basename(asset.attachment.path)
file = File.new("#{Rails.root}/tmp/#{filename}", "wb")
file.write(open(url).read)
if File.exists? file
puts "Re-saving asset attachment #{asset.id} - #{filename}"
asset.attachment = file
asset.save
# if there are multiple styles, you want to recreate them :
asset.attachment.reprocess!
file.close
else
puts "Missing file attachment #{asset.id} - #{filename}"
end
File.delete(file)
end
end
end
end
But my plan didn't work at all, I didn't get access to the files, and the asset.url still isn't equal to asset.old_url.
Would appreciate help very much!
With S3, you can set the "filename upon saving" as a header. Specifically, the user will get to an url https://foo.bar.com/mangled/path/some/weird/hash/whatever?options and when the browser will offer to save, you can control the filename (not the url).
The trick to that relies on the browser reading the Content-Disposition header from the response, if it reads Content-Disposition: attachment; filename="filename.jpg" it will save (or ask the user to save as) filename.jpg, independently on the original URL.
You can force S3 to add this header by adding one more parameter to the URL or by setting a metadata on the file.
The former can be done by passing it to the url method:
has_attached_file :attachment,
s3_url_options: ->(instance) {
{response_content_disposition: "attachment; filename=\"#{instance.filename}\""}
}
Check https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/storage/s3.rb#L221-L225 for the relevant source code.
The latter can be done in bulk via paperclip (and you should also configure it to do it on new uploads). It will also take a long time!!
Asset.find_each do |asset|
next unless asset.attachment
s3_object = asset.attachment.s3_object
s3_object.copy_to(
s3_object,
metadata_directive: 'REPLACE',
content_disposition: "attachment; filename=\"#{asset.filename}\")"
)
end
# for new uploads
has_attached_file :attachment,
s3_headers: ->(att) {
{content_disposition: "attachment; filename=\"#{att.model.filename}\""}
}

Rails send_data streaming PDF as bytes, unless route accessed directly

I've got a simple Rails app that downloads a PDF from a (private) S3 bucket and serves it to the browser.
# app/controllers/file_controller.rb
class FileController < ApplicationController
def send_pdf
s3_file = Aws::S3::Resource.new(
region: "us-east-1",
access_key_id: S3_ACCESS_KEY_ID,
secret_access_key: S3_SECRET_ACCESS_KEY
).bucket('bucket-name').objects({prefix: "file_name"}).first.get.body.string
send_data s3_file, filename: "FileName.pdf", type: 'application/pdf', disposition: 'inline'
end
end
# config/routes.rb
Rails.application.routes.draw do
get 'file', to: 'file#send_pdf', defaults: { format: 'pdf' }
end
When accessing the route directly via URL, it displays the PDF fine.
When opening a link to the route in a new tab, it displays the PDF fine.
When opening a link in the same tab, the PDF data streams as text to the browser instead.
The same behavior occurs in Rails 4 and 5.
I'm probably missing something annoyingly minor here, but how can I get the open in the same tab behavior to properly display the PDF instead of streaming bytes as text?
Update 1:
Chrome gives a Failed to load PDF document error when send_pdf is modified to use send_file instead of send_data. (This error happens regardless of link click or direct route request.)
def send_pdf
# S3 download
temp_file = "#{Rails.root}/tmp/file.pdf"
File.open(temp_file,"wb") do |f|
f.write(s3_file)
f.close
end
send_file temp_file, filename: "FileName.pdf", type: 'application/pdf', disposition: 'inline'
File.delete(temp_file)
end
Have you tried send_file instead of send_data?
http://apidock.com/rails/v4.2.1/ActionController/DataStreaming/send_file

Download zip file from rails 4 to angularjs

I'm trying to download a zip file, sent from a rails 4 application to the front-end.
The zip file construction is working correctly, I can unzip the file and get the content
filename = "cvs_job_#{params[:job_id]}.zip"
archive_path ="#{Rails.root}/tmp/#{filename}"
File.delete(archive_path) if File.exists?(archive_path)
Zip::File.open(archive_path, Zip::File::CREATE) do |zipfile|
params[:user_ids].each do |user_id|
user = User.find(user_id)
zipfile.add("#{user.last_name}_#{user.first_name}.pdf", user.cv_file.path) unless user.cv_file.nil?
end
end
send_file("#{Rails.root}/tmp/#{filename}", :type => 'application/zip', :disposition => 'attachment')
but how am I supposed to handle the response back in the promise?
$http(req).success(function(success){
console.log(success)
})
I saw the zip file in the chrome console, such as :
"...8f�~��/g6�I�-v��=� ..."
I have tried many solutions but none are working.
I thought that I would be able to send the file and download from my front.

Rails 3.1 asset pipeline with PDFKit

I am using PDFkit with rails 3.1. In the past I was able to use the render_to_string function and create a pdf from that string. I then add the stylesheets as follows. My issue is that I have no idea how to access them from within the asset pipeline. (This is how I did it in rails 3.0)
html_string = render_to_string(:template => "/faxes/show.html.erb", :layout => 'trade_request')
kit = PDFKit.new(html_string, :page_size => 'Letter')
kit.stylesheets << "#{Rails.root.to_s}/public/stylesheets/trade_request.css"
So my question in how do i get direct access from my controller to my css file through the asset pipline?
I know I can use the Rack Middleware with PDFkit to render the pdf to the browser, but in this case i need to send the pdf off to a third party fax service.
Thanks for your help.
Ryan
Just ran into this issue as well, and I got past it without using the asset pipeline, but accessing the file directly as I would have previously in /public. Don't know what the possible cons are to using this approach.
I guess LESS and SCSS files won't be processed as they would have if accessed through the asset pipeline.
html = render_to_string(:layout => false , :action => 'documents/invoice.html.haml')
kit = PDFKit.new(html, :encoding=>"UTF-8")
kit.stylesheets << "#{Rails.root.to_s}/app/assets/stylesheets/pdf_styles.css"
send_data(kit.to_pdf, :filename => "test_invoice", :type => 'application/pdf')
A little late, but better late than never, eh.
I'd do it like this:
found_asset = Rails.application.assets.find_asset( "trade_request.css" ).digest_path
kit.stylesheets << File.join( Rails.root, "public", "assets", found_asset )
In Rails 3.1.1 stylesheets are written to /public/assets with and without the digest fingerprint.
This means that you should be able to reference these files just by changing the path in your code.
One gotcha though: if the PDF sheet is not referenced in a CSS manifest you'll have to add it to the precompile config:
config.assets.precompile += ['trade_request.css']
This tells sprockets to compile that file on its own.
As a (better) alternative, see if the asset_path helper works in your code. This will reference the correct file in dev and production.
I ended up copying the css file to my public directory and referring to it in the same way i did before with rails 3. For more information check out this question: Access stylesheet_link_tag from controller
I landed here trying to solve this problem and none of the answers seemed to solve the issue for me. I found the accepted answer of this stack overflow post worked for me:
How does one reference compiled assets from the controller in Rails 3.1?
I was even able to serve .css.erb files through this method.
Try
= stylesheet_link_tag "application", 'data-turbolinks-track': 'reload'
You should be able to access the stylesheet using this method:
ActionController::Base.helpers.asset_path("trade_request.css")
Making your code:
html_string = render_to_string(:template => "/faxes/show.html.erb", :layout => 'trade_request')
kit = PDFKit.new(html_string, :page_size => 'Letter')
kit.stylesheets = ActionController::Base.helpers.asset_path("trade_request.css")

how to force send_data to download the file in the browser?

Well my problem is that I'm using send_data on my Rails 3 application to send to the user a file from AWS S3 service with something like
Base.establish_connection!( :access_key_id => 'my_key', :secret_access_key => 'my_super_secret_key')
s3File = S3Object.find dir+filename, "my_unique_bucket"
send_data(open(s3File.url).read,:filename=>filename, :disposition => 'attachment')
but seems like the browser is buffering the file and before buffering it sends the file to download taking no time on the download but at the buffering time it's taking as long as the file size .... but what i need is the user to view the download process as normal, they won't know what happening with the loader only on the browsers tab:
They'd rather see a download process i guess to figure out there's something happening there
is there any way i can do this with send_data?
It's not the browser that's buffering/delaying, it's your Ruby server code.
You're downloading the entire file from S3 before sending it back to the user as an attachment.
It may be better to serve this content to your user directly from S3 using a redirect. Here's a link to building temporary access URLs that will allow a download with a given token for a short period of time:
http://docs.amazonwebservices.com/AmazonS3/latest/dev/S3_QSAuth.html
Base.establish_connection!( :access_key_id => 'my_key', :secret_access_key => 'my_super_secret_key')
s3File = S3Object.find dir+filename, "my_unique_bucket"
redirect_to s3File.url(:expires_in => 30)
Set Your Content Disposition
You'll need to set the content-disposition of the S3 url for it download instead of opening up in the browser. Here is my basic implementation:
Think of attachment as your s3file.
In your attachment.rb
def download_url
s3 = AWS::S3.new.buckets[ 'bucket_name' ]
s3.url_for( :read,
expires_in: 60.minutes,
use_ssl: true,
response_content_disposition: "attachment; filename='#{file_name}'" ).to_s
end
In your views
<%= link_to 'Download Avicii by Avicii', attachment.download_url %>
Thanks to guilleva for his guidance.

Resources