Uploading from Rails to AWS S3 with PreSigned URLs
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