Uploading from Rails to AWS S3 with PreSigned URLs

2 minute read

Last time, I walked through directly uploading an image to an S3 bucket from the browser with no server involved. That last bit is important. If you are using a server, then PreSigned URLs are a better choice as they do not require a world-writable bucket. The world-writable version scales out to infinity, or at least to S3s limits, which are simply amazing, but at lower volumes or when your servers need to be involved anyway, pre-signing URLs comes with less risk of people filling up your buckets with crap.

Let’s look at how to do this in Rails. To sign requests we need the aws-sdk gem. In your Gemfile:

gem 'aws-sdk', '~> 2'

In order to access AWS, the gem needs to find your credentials.

The gem first looks for ENV['AWS_ACCESS_KEY_ID'] and ENV['AWS_SECRET_ACCESS_KEY']. In production you would commonly set these environment variables in the web server.

Alternatively, you can setup a Aws::Credentials object:

credentials = Aws::Credentials.new('ID', 'KEY')

This approach works well in production if you securely store the credentials using something like sekrets.

Finally, if you’re playing around on your dev machine and have a properly formatted AWS credential file it will be used automatically. Typically, you wouldn’t do this in production.

Now that credentials are out of the way, we need an Aws::S3::Resource object:

s3_resource = Aws::S3::Resource::new(region: 'us-west-1')

If we’re passing in credentials, it’s

s3_resource = Aws::S3::Resource::new(region: 'us-west-1', credentials: credentials)

Once we have the resource object, all we need to get the presigned URL is the bucket name, the path to the file we want to access.

object = s3_resource.bucket('bucket42').object(path)
presigned_url = object.presigned_url(:put)

path is the full path of the file in the bucket (or just the filename, if it’s in the root of the bucket). The above gets permission to write the object, you could also request a read only URL by using :get instead of :put By default, the presigned url becomes invalid after 900 seconds (15 minutes). You can adjust this by passing in the expires_in option (in seconds):

object = s3_resource.bucket('bucket42').object(path)
presigned_url = object.presigned_url(:put, expires_in: 5.minutes.to_i)

Once we have the URL we can directly upload an image from Rails (presumes a file field called photo in your form):

uri = URI.parse(presigned_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

request = Net::HTTP::Put.new(uri.request_uri)
request['Content-Type'] = params[:photo][:data].content_type
request.body = params[:photo][:data].read

response = http.request(request)

if response.kind_of? Net::HTTPSuccess
  # All good
else
  # handle failure
end

Next time we’ll look at combining server generated signatures with a browser side AJAX upload.

Comments