S3: A Flexible Approach to Uploading in Rails

S3: A Flexible Approach to Uploading in Rails

S3, Amazon’s Simple Storage Service, is a cheap, scalable, highly available, and overall great solution for storing huge bits of data and thus reducing your database size. For a while now, we’ve realized that our database is on its path to becoming expensive and unmanageable due its massive size. So we decided to migrate some of that data over to S3.

In this article, I explain the approach we took to make any attribute S3able

We created a concern that could be included in any model and automatically upload the contents of a field to s3 on save and read from s3 on access. carrierwave gem is used to abstract API calls in addition to carrierwave-aws gem to setup AWS credentials.

module S3able
  extend ActiveSupport::Concern

  def self.included(klass)
    def klass.s3able(column_name)

      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        self.mount_uploader :s3_#{column_name}, FileUploader
        def #{column_name}
          self.s3_#{column_name}&.read.to_s
        end

        def #{column_name}=(contents)
          current_time_txt_file = Time.now.to_f.to_s + ".txt"
          file = Tempfile.new(current_time_txt_file)
          file.write(contents)
          self.s3_#{column_name} = file
          super(contents)
        end
      RUBY

    end
  end
end

The code basically hijacks the setter and getter for whatever field you pass into mount :field inside your model. The getter uses the carrierwave object to retrieve and read the file. The setter creates a temp file and, again, uses carrierwave to upload the data into s3.

FileUploader used inside the S3able module just sets the store_dir which directs carrierwave on how to create the path and your file in the s3 bucket. Here is how it’s written:

class FileUploader < CarrierWave::Uploader::Base
  def store_dir
    "#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def extension_white_list
    %w(txt)
  end
end

Now, if you drop s3able into any model like this:

class YourModel
  include S3able
  s3able :your_field
end

You turn the setter of that field into a text file uploader and the getter into a file reader. It’s that simple.

Not that this approach would only work for new fields. It can also work for existing fields if you don’t care about the data. You need to tweak the S3able concern to accommodate uploading existing data, but I hope this gives you a good starting point.