Rails download file direct from S3 with content-disposition = attachment? - ruby-on-rails

This is my controller
Cotroller
def download
data = open(#attachment.file.url).read
#attachment.clicks = #attachment.clicks.to_i + 1
#attachment.save
send_data data, :type => #attachment.content_type, :filename => #attachment.name
end
example:
#attachment.file.url = "http://my_bucket.cloudfront.net/uploads/attachment/file/50/huge_file.pptx"
I did this, but if #attachement is a huge file (eg. 300MB), my server crash.
I want to allow users to download the file in the browser directly from my AWS server?
2) tip: Do you suggest to download file from S3 (where they are stored) or with CloudFront?

If you using carrierwave gem, you can try this to track number of clicks
def download
#attachment.clicks.to_i += 1
#attachment.save
redirect_to #attachment.file.url(query: {"response-content-disposition" => "attachment;"})
end
references:
Rails carrierwave S3 get url with Content-Disposition header

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}\""}
}

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.

Ruby on Rails AWS S3 Download URL

How can I form a url link for a user so that when the user clicks on the link, it forces them to download the AWS S3 object?
I've seen these two solutions: Using send_file to download a file from Amazon S3? and Using send_file to download a file from Amazon S3? however, they seem to reference an old AWS S3 v1 SDK and there does not seem to be a url_for in the v2 AWS S3 SDK.
Thanks.
Ended up using the following code snippet to solve. Hope this helps others.
presigner = Aws::S3::Presigner.new
url = presigner.presigned_url(:get_object, #method
bucket: ENV['S3_BUCKET'], #name of the bucket
key: s3_key, #key name
expires_in: 7.days.to_i, #time should be in seconds
response_content_disposition: "attachment; filename=\"#{filename}\""
).to_s
Here's what I got:
def user_download_url(s3_filename, download_filename=nil)
s3_filename = s3_filename.to_s # converts pathnames to string
download_filename ||= s3_filename.split('/').last
url_options = {
expires_in: 60.minutes,
response_content_disposition: "attachment; filename=\"#{download_filename}\""
}
object = bucket.object(s3_filename)
object.exists? ? object.presigned_url(:get, url_options).to_s : nil
end
def bucket
#bucket ||= Aws::S3::Resource.new(region: ENV['AWS_REGION']).bucket(ENV['AWS_S3_BUCKET'])
end
To create a link for downloading, simply put redirect_to user_download_url(s3_file_path) in a controller action, and create a link to that controller action.

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.

Zip up all Paperclip attachments stored on S3

Paperclip is a great upload plugin for Rails. Storing uploads on the local filesystem or Amazon S3 seems to work well. I'd just assume store files on the localhost, but the use of S3 is required for this app as it will be hosted on Heroku.
How would I go about getting all of my uploads/attachments from S3 in a single zipped download?
Getting a zip of files from the local filesystem seems straight forward. It's getting the files from S3 that has me puzzled. I think it may have something to do with the way that rubyzip handles files referenced by URL. I've tried various approaches but can't seem to avoid errors.
format.zip {
registrations_with_attachments = Registration.find_by_sql('SELECT * FROM registrations WHERE abstract_file_name NOT LIKE ""')
headers['Cache-Control'] = 'no-cache'
tmp_filename = "#{RAILS_ROOT}/tmp/tmp_zip_" <<
Time.now.to_f.to_s <<
".zip"
# rubyzip gem version 0.9.1
# rdoc http://rubyzip.sourceforge.net/
Zip::ZipFile.open(tmp_filename, Zip::ZipFile::CREATE) do |zip|
#get all of the attachments
# attempt to get files stored on S3
# FAIL
registrations_with_attachments.each { |e| zip.add("abstracts/#{e.abstract.original_filename}", e.abstract.url(:original, false)) }
# => No such file or directory - http://s3.amazonaws.com/bucket/original/abstract.txt
# Should note that these files in S3 bucket are publicly accessible. No ACL.
# works with local storage. Thanks to Henrik Nyh
# registrations_with_attachments.each { |e| zip.add("abstracts/#{e.abstract.original_filename}", e.abstract.path(:original)) }
end
send_data(File.open(tmp_filename, "rb+").read, :type => 'application/zip', :disposition => 'attachment', :filename => tmp_filename.to_s)
File.delete tmp_filename
}
You almost certainly want to use e.abstract.to_file.path instead of e.abstract.url(...).
See:
Paperclip::Storage::S3::to_file (should return a TempFile)
TempFile::path
UPDATE
From the changelog:
New in 3.0.1:
API CHANGE: #to_file has been removed. Use the #copy_to_local_file method instead.
#vlard's solution is ok. However I've run into some issues with the to_file. It creates a tempfile and the garbage collector deletes (sometimes) the file before it was added to the zip file. Therefor, I'm getting random Errno::ENOENT: No such file or directory errors.
So I'm using the following code now (I've kept the initial code variables names for consistency with the initial question)
format.zip {
registrations_with_attachments = Registration.find_by_sql('SELECT * FROM registrations WHERE abstract_file_name NOT LIKE ""')
headers['Cache-Control'] = 'no-cache'
#please note that using nanoseconds option in strftime reduces the risks concerning the situation where 2 or more users initiate the download in the same time
tmp_filename = "#{RAILS_ROOT}/tmp/tmp_zip_" <<
Time.now.strftime('%Y-%m-%d-%H%M%S-%N').to_s <<
".zip"
# rubyzip gem version 0.9.4
zip = Zip::ZipFile.open(tmp_filename, Zip::ZipFile::CREATE)
zip.close
registrations_with_attachments.each { |e|
file_to_add = e.file.to_file
zip = Zip::ZipFile.open(tmp_filename)
zip.add("abstracts/#{e.abstract.original_filename}", file_to_add.path)
zip.close
puts "added #{file_to_add.path} to #{tmp_filename}" #force garbage collector to keep the file_to_add until after the file has been added to zip
}
send_data(File.open(tmp_filename, "rb+").read, :type => 'application/zip', :disposition => 'attachment', :filename => tmp_filename.to_s)
File.delete tmp_filename
}

Resources