Using send_file to download a file from Amazon S3?
Solution 1
In order to send a file from your web server,
you need to download it from S3 (see @nzajt's answer) or
you can
redirect_to @attachment.file.expiring_url(10)
Solution 2
You can also use send_data
.
I like this option because you have better control. You are not sending users to s3, which might be confusing to some users.
I would just add a download method to the AttachmentsController
def download
data = open("https://s3.amazonaws.com/PATTH TO YOUR FILE")
send_data data.read, filename: "NAME YOU WANT.pdf", type: "application/pdf", disposition: 'inline', stream: 'true', buffer_size: '4096'
end
and add the route
get "attachments/download"
Solution 3
Keep Things Simple For The User
I think the best way to handle this is using an expiring S3 url. The other methods have the following issues:
- The file downloads to the server first and then to the user.
- Using
send_data
doesn't produce the expected "browser download". - Ties up the Ruby process.
- Requires an additional
download
controller action.
My implementation looks like this:
In your attachment.rb
def download_url
S3 = AWS::S3.new.buckets[ 'bucket_name' ] # This can be done elsewhere as well,
# e.g config/environments/development.rb
url_options = {
expires_in: 60.minutes,
use_ssl: true,
response_content_disposition: "attachment; filename=\"#{attachment_file_name}\""
}
S3.objects[ self.path ].url_for( :read, url_options ).to_s
end
In your views
<%= link_to 'Download Avicii by Avicii', attachment.download_url %>
That's it.
If you still wanted to keep your download
action for some reason then just use this:
In your attachments_controller.rb
def download
redirect_to @attachment.download_url
end
Thanks to guilleva for his guidance.
Solution 4
I have just migrated my public/system
folder to Amazon S3. Solutions above help but my app accepts different kinds of documents. So if you need the same behavior, this helps for me:
@document = DriveDocument.where(id: params[:id])
if @document.present?
@document.track_downloads(current_user) if current_user
data = open(@document.attachment.expiring_url)
send_data data.read, filename: @document.attachment_file_name, type: @document.attachment_content_type, disposition: 'attachment'
end
The file is being saved in the attachment
field of DriveDocument
object.
I hope this helps.
Solution 5
The following is what ended up working well for me. Getting the raw data from the S3 object and then using send_data
to pass that on to the browser.
Using the aws-sdk
gem documentation found here http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html
full controller method
def download
AWS.config({
access_key_id: "SECRET_KEY",
secret_access_key: "SECRET_ACCESS_KEY"
})
send_data(
AWS::S3.new.buckets["S3_BUCKET"].objects["FILENAME"].read, {
filename: "NAME_YOUR_FILE.pdf",
type: "application/pdf",
disposition: 'attachment',
stream: 'true',
buffer_size: '4096'
}
)
end
David Tuite
Founder and CEO of Developer Portal company roadie.io
Updated on July 30, 2020Comments
-
David Tuite almost 4 years
I have a download link in my app from which users should be able to download files which are stored on s3. These files will be publicly accessible on urls which look something like
https://s3.amazonaws.com/:bucket_name/:path/:to/:file.png
The download link hits an action in my controller:
class AttachmentsController < ApplicationController def show @attachment = Attachment.find(params[:id]) send_file(@attachment.file.url, disposition: 'attachment') end end
But I get the following error when I try to download a file:
ActionController::MissingFile in AttachmentsController#show Cannot read file https://s3.amazonaws.com/:bucket_name/:path/:to/:file.png Rails.root: /Users/user/dev/rails/print Application Trace | Framework Trace | Full Trace app/controllers/attachments_controller.rb:9:in `show'
The file definitely exists and is publicly accessible at the url in the error message.
How do I allow users to download S3 files?
-
mehulkar over 11 yearshow can I use this with non-public files on S3?
-
Homan almost 11 yearsSo in this solution the
open
method does not download the entire file first? And send_data can stream the file from amazon to the user without the user ever knowing the real s3 file path? -
dgilperez over 10 years@MehulKar in that case you need to use
@attachment.file.expiring_url
-
equivalent8 about 10 yearsDefinitely this way, however it seems that the
stream
&buffer_size
options are not needed github.com/rails/rails/blob/master/actionpack/lib/… api.rubyonrails.org/classes/ActionController/DataStreaming.html -
equivalent8 about 10 yearsapi.rubyonrails.org/classes/ActionController/… so just
send_data data.read, type: image.content_type, disposition: 'inline'
to render in browser -
Joshua Pinter about 10 yearsFor large files, this can be confusing for the user who waits for the server to download the file first and then stream it to the user. Plus, it takes up to twice as long then just having the user download directly from S3.
-
Joshua Pinter about 10 yearsThis has to download the file to the server before sending to the user, correct?
-
David Morrow about 10 yearsCorrect, may not be an option depending on how large your files are. In my case they are small PDFs and this was acceptable.
-
BigRon about 9 yearsDoes this download the whole bucket? I have a similar method where I am downloading individual bucket objects based on their key.
-
Joshua Pinter about 9 years@BigRon No, just the individual object in the bucket. But looking at my code snippets again, I think I've sliced out an important part! Thanks for pointing that out!
-
Joshua Pinter about 9 years@BigRon Take a look at that now. Added the piece where you use the S3 bucket to actually get the object.
-
BigRon about 9 yearsnice correction, that looks right. I'll have to try your use of
self.path
. It looks like a more simple usage than my current method -
Joshua Pinter about 9 years@BigRon Let me know if
self.path
works for you or not because we actually have a check that strips out the leading slash but I wasn't sure if that was necessary in all cases or if we have something special in our case:self.path.chr == '/' ? self.path[1..-1] : self.path )
-
Joshua Pinter over 8 yearsBTW, I edited this to use double quotes around the header filename. We ran into an issue where some versions of Internet Explorer (of course) was including the single quotes as part of the downloaded filename. Using double quotes seemed to fix the issue.
-
ArnoHolo about 8 yearsWIth a carrierwave+S3 file, I made it work like this :
article = Article.find params[:id]
file_data = open(article.file.url)
send_data file_data.read, filename: article.filename, type: article.file.content_type, disposition: 'attachment'
-
Dennis almost 8 yearsNote when accessing private files on S3 as shown here, the S3 URL will contain your secret AWS credentials so the browser can make an authenticated request. It won't be obvious if the download works and the user flow stays on your page, but when it doesn't work (for example the file doesn't exist) then the error page on S3 will have the credentials visible on the URL. This is a pretty big security risk.
-
Dennis almost 8 yearsWorth pointing out in addition to @JoshPinter's observation of it being slower (because the data goes through an intermediary instead of direct), it also puts extra load on your server and is blocking, though you can offload it to a background task. Downloading the file directly from S3 is more efficient plus S3 has decent error pages - but your AWS credentials will be visible when accessing private files.
-
jsurf over 7 yearsHow can I download the file inline? My browser opens files in a new tab. I'm using Heroku + Passenger, maybe I have to add an .htaccess file to specify inline downloads?
-
maikovich almost 7 yearsThis is really not a good idea. You store all the file data in memory before you pass it on. Depending on the file size, this can get you ending up with a "No space left on device" exception.
-
kittyminky almost 7 yearsredirecting to the url doesn't automatically download the file, it just brings you to a browser view of it where you can then download it
-
nzajt almost 7 yearsFor us redirecting to S3 isn't a good option, the files we send are not public, so we only want certain users to download said files. I have been doing this for years and never run into a disk space issue, but it could happen if you are using a small server. I also like controlling the user experience, so the download doesn't come from some random S3 or cloudfront url.
-
Maxence over 6 yearsHi Joshua, I am using paperclip with files as private. it works all good. Thoguh on certain occasions i would like the user be able to download the file instead of opening it in the browser tab. Is there a way to add
response_content_disposition
in the controller instead of the model ? -
Joshua Pinter over 6 years@Maxence What if you just add a param for the models'
download_url
method that allows you to change how it's dealt with? -
Meyer over 5 years@HoloHokkaido Your comment is what finally worked for me.
-
Rajan almost 4 yearsWhen possible, please make an effort to provide additional explanation instead of just code. Such answers tend to be more useful as they help members of the community and especially new developers better understand the reasoning of the solution, and can help prevent the need to address follow-up questions.
-
Rajan almost 4 yearsWhen possible, please make an effort to provide additional explanation instead of just code. Such answers tend to be more useful as they help members of the community and especially new developers better understand the reasoning of the solution, and can help prevent the need to address follow-up questions.
-
Sampson Crowley almost 3 yearsI know this post is old, but because this comment is referenced multiple times: no, expiring urls do not leak your credentials. they show your access id which is not a secret, and a presigned hash for authentication. if presigned urls gave out your credentials, people could just recreate urls over and over again when the url expired