12

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?

  • possible duplicate of [how to force send\_data to download the file in the browser?](http://stackoverflow.com/questions/7337508/how-to-force-send-data-to-download-the-file-in-the-browser) – Joshua Pinter Apr 18 '14 at 20:00

3 Answers3

9

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
  • 2,856
  • 1
  • 19
  • 11
  • the S3Ojbect.url_for looks like the best approach – klochner Aug 27 '09 at 20:27
  • 2
    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-not send_file_headers! is private, neither send_data or send_file accepts remote files; workaroundable but just FYI – kain Oct 26 '10 at 05:49
  • I think the way you generate time_of_expiry will actually not work, you need to do: `time_of_expiry = 2.minutes.from_now.to_i` – Raphael Feb 09 '12 at 15:37
  • Or you can do `:expires_in => 60 * 2` – Raphael Feb 09 '12 at 15:42
  • Or you can avoid the `:expires_in` key & let it expire in 5 minutes which is default. [S3 documentation](http://amazon.rubyforge.org/doc/classes/AWS/S3/S3Object.html#M000045) – Alex Mar 21 '12 at 11:18
4

Full image download method using temp file (tested rails 3.2):

def download
  @image = Image.find(params[:image_id])

  open(@image.url) {|img|
    tmpfile = Tempfile.new("download.jpg")
    File.open(tmpfile.path, 'wb') do |f| 
      f.write img.read
    end 
    send_file tmpfile.path, :filename => "great-image.jpg"
  }   
end
Demelziraptor
  • 1,545
  • 1
  • 14
  • 20
3

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
  • 8,077
  • 1
  • 33
  • 45
  • 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 Aug 26 '09 at 09:25
  • 1
    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 Aug 26 '09 at 19:17
  • 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 Aug 27 '09 at 02:08
  • I just verified Luca's approach in the s3 api, untested but looks right - +1 – klochner Aug 27 '09 at 20:26