Reputation: 2874
I need to be able to serve a large file as a download from a Rails app, from an S3 bucket. I would typically just give the user the URL to the 3S bucket, but I need it to remain masked so I can expire or remove the URL at will. I'm doing something like the following:
def download
purchase = Purchase.find(params[:id])
data = open(purchase[:download_url])
send_data data.read, filename: purchase[:file_name]
end
It works for smaller files but on one that's, say 1GB, because of Heroku's timeout limits, the user gets a big fat 520 error.
I understand what's going on here: the entire file is being opened up in my application before being sent over to the user, and that's why for larger files it times out. What I'm wondering is if there's a way around this without giving the user S3 URL? Is it possible to, rather than download the file, just mask the URL (not redirect)?
Any help is appreciated!
Edit
Based on other answers on SO, I tried to implement an expiring URL:
def download
purchase = Purchase.find(params[:id])
path = purchase[:download_url].sub! 'entire URL until bucket path', ''
s3URL = AWS::S3::S3Object.new(S3_BUCKET, path)
redirect_to s3URL.url_for(:read).to_s
end
That just redirects to an AccessDenied
on AWS, but maybe I'm building the URL incorrectly. Again, any help is appreciated!
Upvotes: 0
Views: 895
Reputation: 14630
You don't need to send the data, just set up a proxy url, that redirects to the S3 url, or whatever else you need to do programatically.
Also bear in mind that S3 urls have expiration baked in.
Upvotes: 0
Reputation: 3968
I think your S3 object url is inaccessible because the bucket/object doesn't have public-read
.
To allow people direct download from your S3 bucket, you can either:
Make S3 bucket public accessible
If you follow this method, you can just give the S3 object url as in your updated question. You just need to configure the bucket in AWS.
Copy and paste the following bucket policy:
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"AddPerm",
"Effect":"Allow",
"Principal": "*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::examplebucket/*"]
}
]
}
Click Save. Now, any users can download any objects inside this bucket as long as they have the URL. As a tip, to disallow guessing the URL, you can rename your object key with reasonable length random string.
Create a presigned url
In this method, only user who has the presigned url can download your S3 object in a certain period (by default, it's one week). After that, he/she need to get a new one. To create presigned S3 object:
def download
purchase = Purchase.find(params[:id])
path = purchase[:download_url].sub! 'entire URL until bucket path', ''
s3 = Aws::S3::Resource.new(region:'us-west-2')
object = s3.bucket(S3_BUCKET).object(path)
redirect_to object.presigned_url(:get)
end
You may also set some options in the object.presigned_url
method like:
object.presigned_url(:get, expires_in: 3600, response_content_disposition: 'attachment; filename=original_file_name.zip')
Read more in http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_url-instance_method.
You need to choose which one is suitable for your case. For non-sensitive and public S3 object, I prefer to make S3 bucket public accessible. But, I think, for your case (purchase something and then download), it's better to implement the presigned URL.
Upvotes: 2