Uploading Files to Amazon S3 seems like something that should be relatively easy, given how common of a task it is. Oddly enough, I had to cobble a solution together with a mix of official and unofficial documentation, Stack Overflow answers, and GitHub issue comments. To make this easier for my future self and hopefully lend a hand to someone else in the same situation, I'm logging every piece of the puzzle required to be able to upload files to Amazon S3 using the Paperclip gem in a Rails app.
Issue #1 - Uninitialized constant Paperclip::Storage::S3::AWS
This was my first big trip up. At the the time that I'm writing this, the current version of the Paperclip gem (v4.3.4) is only compatible with the 1.x version of the aws-sdk gem. You'll need to specify that in your Gemfile. Support for the 2.x version of the aws-sdk is scheduled with the release of Paperclip v5.
Gemfile
gem "paperclip"
gem "aws-sdk", "< 2.0"
Issue #2 - AWS::S3::Errors::NoSuchKey No Such Key
This somewhat cryptic error came up after I had flipped the switch to S3, but still had pre-S3 images in my database. I was able to resolve this pretty easily by clearing all of my pre-S3 images and starting from scratch.
Issue #3 - "The bucket you are attempting to access must be addressed using the specified endpoint."
By default, Paperclip uses http://s3.amazonaws.com/my-bucket-name/ as the base URL for your uploaded files. If you get the above bucket/endpoint error from S3, it's because S3 wants you to use http://my-bucket-name.s3.amazonaws.com/ instead. Luckily this is easily fixed by adding the following two lines to your Paperclip configuration.
config/initializers/paperclip.rb
Paperclip::Attachment.default_options[:url] = ":s3_domain_url"
Paperclip::Attachment.default_options[:path] = "/:class/:attachment/:id_partition/:style/:filename"
NB: Most of the documentation and articles about Paperclip say to configure it in the various environment based config files (i.e. /config/environments/production.rb). That said, I prefer to keep this kind of configuration in an initializer. Either way, you want to make sure that Paperclip is properly configured.
Putting it all Together
Once all of the issues are out of the way, the whole upload process is pretty seamless.
If you want to take advantage of Paperclip's URL Obfuscation on S3, you just need to update the :path option and include the :hash_secret option. You just need to add the following 2 lines to your Paperclip config.
config/initializers/paperclip.rb
Paperclip::Attachment.default_options[:path] = "/:class/:attachment/:id_partition/:style/:hash.:extension"
Paperclip::Attachment.default_options[:hash_secret] = "yourHashSecretHere"
I like to move anything variable in a config file into the environment variables. So rather than using the string, I moved the hash secret to an environment variable, e.g. ENV["PAPERCLIP_HASH_SECRET"]
. This shouldn't cause any problems as long as you're using something like the dotenv gem to manage your environment variables locally. If you're deploying to Heroku, just be sure to set the environment variable:
$ heroku config:set PAPERCLIP_HASH_SECRET=yourHashSecretHere
Finally, here is the full Paperclip initializer that I used:
config/initializers/paperclip.rb
Paperclip::Attachment.default_options[:storage] = :s3
Paperclip::Attachment.default_options[:s3_region] = "us-west-2"
Paperclip::Attachment.default_options[:s3_protocol] = "https"
Paperclip::Attachment.default_options[:s3_credentials] = {
bucket: ENV["S3_BUCKET_NAME"],
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
}
Paperclip::Attachment.default_options[:url] = ":s3_domain_url"
Paperclip::Attachment.default_options[:path] = "/:class/:attachment/:id_partition/:style/:hash.:extension"
Paperclip::Attachment.default_options[:hash_secret] = ENV["PAPERCLIP_HASH_SECRET"]
Again, if you're deploying to Heroku, be sure to set all of your environment variables:
$ heroku config:set S3_BUCKET_NAME=your_bucket_name
$ heroku config:set AWS_ACCESS_KEY_ID=your_access_key_id
$ heroku config:set AWS_SECRET_ACCESS_KEY=your_secret_access_key
That should do it!