Anytime I have a project that requires accessing an S3 bucket, my go-to gem has always been Marcel Molina’s aws-s3.
A few rough spots have bubbled up recently and I thought I’d post my solutions in case they’re troubling others.
One-off connections
When you call “.establish_connection!” on AWS::S3::Base, AWS::S3::Object, or AWS::S3::Bucket, the connection details are persisted across all request methods on those classes. This means that if you change the connection in (for example) an admin controller using AWS::S3::S3Object.establish_connection! and then another controller attempts to use AWS::S3::S3Object.store (but expects a different connection), you’ve just had an object posted to the wrong place.
The example code below creates a one-time-use connection and then stores a text file.
file_name = 'file.txt'
local_file_path = "/path/to/#{file_name}"
key = "/bucket_name/#{file_name}"
connection = AWS::S3::Connection.connect(:access_key_id => <your_access_key_id>, :secret_access_key => <your_secret_access_key>)
connection.request(:put, key, {"x-amz-acl" => "public-read"}, open(local_file_path))
Setting response headers
I had a need to store a file and set response headers such that accessing the files’ url would prompt a download in the browser. Amazon allows you to set a handful of response headers (see documentation). Storing the file was done via the aws-s3 gem so sending the response headers at that time wasn’t possible. Instead, I created a method that returned a signed request with the desired response headers specified in the request url.
The example code below builds a signed request with response headers that would cause a download prompt in a browser. Note that this is assuming use of the default connection by referencing “AWS::S3::Base.connection”.
file_name = 'file.csv'
file_path = "/bucket_name/#{file_name}"
s3_connection = AWS::S3::Base.connection
request = Net::HTTP::Get.new(file_path, {})
expiry = Time.now.to_i + 300
response_headers = "response-content-disposition=attachment;filename=#{file_name}"
string_to_sign = AWS::S3::Authentication::CanonicalString.new(request, :expires => expiry) + "?#{response_headers}"
digest = OpenSSL::Digest::Digest.new('sha1')
b64_hmac = [OpenSSL::HMAC.digest(digest, s3_connection.secret_access_key, string_to_sign)].pack("m").strip
signature = CGI.escape(b64_hmac)
"https://s3.amazonaws.com#{file_path}?AWSAccessKeyId=#{s3_connection.access_key_id}&Expires=#{expiry}&Signature=#{signature}&#{response_headers}"