views:

815

answers:

3

In my app, I have a requirement that is stumping me.

I have a file stored in S3, and when a user clicks on a link in my app, I log in the DB they've clicked the link, decrease their 'download credit' allowance by one and then I want to prompt the file for download.

I don't simply want to redirect the user to the file because it's stored in S3 and I don't want them to have the link of the source file (so that I can maintain integrity and access)

It looks like send_file() wont work with a remote source file, anyone recommend a gem or suitable code which will do this?

+2  A: 

You would need to stream the file content to the user while reading it from the S3 bucket/object.

If you use the AWS::S3 library something like this may work:

 send_file_headers!( :length=>S3Object.about(<s3 object>, <s3 bucket>)["content-length"], :filename=><the filename> )
 render :status => 200, :text => Proc.new { |response, output|
   S3Object.stream(<s3 object>, <s3 bucket>) do |chunk|
     output.write chunk
   end
 }

This code is mostly copied form the send_file code which by itself works only for local files or file-like objects

N.B. I would anyhow advise against serving the file from the rails process itself. If possible/acceptable for your use case I'd use an authenticated GET to serve the private data from the bucket.

Using an authenticated GET you can keep the bucket and its objects private, while allowing temporary permission to read a specific object content by crafting a URL that includes an authentication signature token. The user is simply redirected to the authenticated URL, and the token can be made valid for just a few minutes.

Using the above mentioned AWS::S3 you can obtain an authenticated GET url in this way:

 time_of_exipry = Time.now + 2.minutes
 S3Object.url_for(<s3 object>, <s3 bucket>,
                  :expires => time_of_exipry)
LucaM
the S3Ojbect.url_for looks like the best approach
klochner
We used a redirect for a year, but now we really want Safari/Mac to work:http://stackoverflow.com/questions/1995589/html5-audio-safari-live-broadcast-vs-notsend_file_headers! is private, neither send_data or send_file accepts remote files; workaroundable but just FYI
kain
A: 

You can read the file from S3 and write it locally to a non-public directory, then use X-Sendfile (apache) or X-Accel-Redirect (nginx) to serve the content.

For nginx you would include something like the following in your config:


            location /private {
                                 internal;
                                 alias /path/to/private/directory/;
            }

Then in your rails controller, you do the following:


   response.headers['Content-Type'] = your_content_type
   response.headers['Content-Disposition'] = "attachment; filename=#{your_file_name}"
   response.headers['Cache-Control'] =  "private"
   response.headers['X-Accel-Redirect'] = path_to_your_file
   render :nothing=>true

A good writeup of the process is here

klochner
what you say is correct but would there be any way to do so without the files being local to the web server? The problem in the case at hand is that the files are stored on the S3 web service.
LucaM
I don't think you can grant people per-user direct access to your S3 files unless they have their own S3 account. You'll have to download the files from S3 to your local server and send from there.
klochner
s3 files are often public, just using obfuscated URIs. Seems like the effect you are trying to achieve can be done without copying the files, see the discussion here: http://www.ruby-forum.com/topic/132214. You will have to do 2 stages, though: login, and then send the file request.
austinfromboston
obfuscation != security
klochner
I just verified Luca's approach in the s3 api, untested but looks right - +1
klochner
A: 

What about if what I want is to prompt the user with a download window 'save as' or similar, not render the image on the browser...