30

I have a Rails application hosted on Heroku. The app generates and stores PDF files on Amazon S3. Users can download these files for viewing in their browser or to save on their computer.

The problem I am having is that although downloading of these files is possible via the S3 URL (like "https://s3.amazonaws.com/my-bucket/F4D8CESSDF.pdf"), it is obviously NOT a good way to do it. It is not desirable to expose to the user so much information about the backend, not to mention the security issues that rise.

Is it possible to have my app somehow retrieve the file data from S3 in a controller, then create a download stream for the user, so that the Amazon URL is not exposed?

futureshocked
  • 2,105
  • 4
  • 23
  • 32

3 Answers3

59

You can create your s3 objects as private and generate temporary public urls for them with url_for method (aws-s3 gem). This way you don't stream files through your app servers, which is more scalable. It also allows putting session based authorization (e.g. devise in your app), tracking of download events, etc.

In order to do this, change direct links to s3 hosted files into links to controller/action which creates temporary url and redirects to it. Like this:

class HostedFilesController < ApplicationController

  def show
    s3_name = params[:id] # sanitize name here, restrict access to only some paths, etc
    AWS::S3::Base.establish_connection!( ... )
    url = AWS::S3::S3Object.url_for(s3_name, YOUR_BUCKET, :expires_in => 2.minutes)
    redirect_to url
  end

end

Hiding of amazon domain in download urls is usually done with DNS aliasing. You need to create CNAME record aliasing your subdomain, e.g. downloads.mydomain, to s3.amazonaws.com. Then you can specify :server option in AWS::S3::Base.establish_connection!(:server => "downloads.mydomain", ...) and S3 gem will use it for generating links.

Serge Balyuk
  • 3,442
  • 1
  • 23
  • 23
  • 1
    Serge, I tried your method. It works, however the URL looks like this: http://s3.amazonaws.com/my-bucket/F4D8CED0.pdf?AWSAccessKeyId=my-actual-key&Expires=1346904098&Signature=Wo7wYH6Ek%2FBot3wd2xYbGt4ybSU%3D. What I would like to see ideally is no mention of amazon, but something like "http://mydomain.com/download/file.pdf. Is this possible? – futureshocked Sep 06 '12 at 04:00
  • 1
    You can get pretty close to that with CNAME, but it will be more like `something.mydomain.com/path/file.pdf`. I've updated answer to reflect that. – Serge Balyuk Sep 06 '12 at 06:43
  • You should set the `content-disposition` as an attachment in order for it to force the browser to download it instead of trying to open it up in the window. – Joshua Pinter Apr 18 '14 at 20:04
11

Yes, this is possible - just fetch the remote file with Rails and either store it temporarily on your server or send it directly from the buffer. The problem with this is of course the fact that you need to fetch the file first before you can serve it to the user. See this thread for a discussion, their solution is something like this:

#environment.rb
require 'open-uri'

#controller
def index
  data = open(params[:file])
  send_data data, :filename => params[:name], ...
end

This issue is also somewhat related.

Community
  • 1
  • 1
l4mpi
  • 5,103
  • 3
  • 34
  • 54
  • This turned out to be simple, thank you. This is my actual implementation: def download file_name = params["file_id"] + ".pdf" data = open("https://s3.amazonaws.com/#{ENV['S3_BUCKET_NAME']}/#{file_name}") send_data data.read, :filename => file_name, :type => "application/pdf", :disposition => 'attachment', :stream => 'true', :buffer_size => '4096' end I have accepted your solution as solution to this problem because it is the simplest one to implement. – futureshocked Sep 06 '12 at 07:19
  • 1
    Serge's one is also good, with the benefit of releasing the Rails app from handling the download, it just needs a bit more work to get it working. – futureshocked Sep 06 '12 at 07:23
  • 1
    Just others know, there are two issues with this I found: 1) It downloads to the server and then to the user, which can take a while; and 2) The experience for the user isn't what they expect. Instead of getting a download in their browser it looks, instead, like it is loading something. Check out http://stackoverflow.com/a/23161355/293280 for a further discussion. – Joshua Pinter Apr 18 '14 at 20:04
-1

First you need create a CNAME in your domain, like explain here.

Second you need create a bucket with the same name that you put in CNAME.

And to finish you need add this configurations in your config/initializers/carrierwave.rb:

CarrierWave.configure do |config|
    ...  
    config.asset_host         = 'http://bucket_name.your_domain.com'
    config.fog_directory      = 'bucket_name.your_domain.com'
    ...
end
monteirobrena
  • 2,562
  • 1
  • 33
  • 45