At Evil Martians, we use Carrierwave to store billions of files in S3 cloud and we faced with such issues:
- with carrierwave-backgrounder, logic becomes too complex
- it creates bunch of callbacks and magick attributes inside AR::Base model
- Rails 4 way prefers using Service and Value objects for complex logic inside the Model
So what we need, is the solution to:
- download remote image
- save it locally
- process it (including resize and watermarks)
- transfer it to S3 in background if we need to
- backup it
- reprocess photo if size was changed
Add this line to your application's Gemfile:
gem 'storage'
And then execute:
$ bundle
Or install it yourself as:
$ gem install storage
Then you can configure Storage
in Rails initializer:
# config/initializers/storage.rb
Storage.setup do |config|
secrets = Rails.application.secrets[:s3]
if secrets.nil?
raise ArgumentError.new("secrets.yml doesn't have credentials for S3")
end
# only if you use Amazon S3
config.s3_credentials = {
access_key_id: secrets['access_key'],
secret_access_key: secrets['secret_key'],
region: secrets['region']
}
# only if you use Amazon S3
config.bucket_name = "my-app_#{Rails.env}"
end
Firstly, you need to declare Storage
model (almost like Uploader
you used in CarrierWave):
# app/storages/cover_photo_storage.rb
class CoverPhotoStorage < Storage::Model
version :original
version :thumb, size: "200x200"
version :big, size: "300x300"
# leave this if you want to use S3 as a storage
store_remotely
# define how you would like to modify the image
def process_image(version, image)
# image is original, instance of MiniMagick::Image
# you can transform existing image object or return the new one
if version.options[:size].present?
image.resize(version.options[:size])
end
end
# optionally: redefine to use custom uploads path
# default is: /uploads/:model_name/:id/:field/:version_name/:filename
def key(version, filename)
File.join("uploads", model.class.name.underscore, model.id.to_s, field_name, version, filename)
end
end
And then mount CoverPhotoStorage into your model:
# app/models/post.rb
class Post < ActiveRecord::Base
def cover_photo
@cover_photo ||= CoverPhotoStorage.new(self, :cover_photo)
end
end
Don't forget to add cover_photo
column into your DB scheme
Now you can use Storage API:
post = Post.create!
post.cover_photo.download("http://example.com/photo.jpg")
post.cover_photo.present?
=> true
post.cover_photo.url
=> 'https://yourbucker.s3-eu-west-1.amazonaws.com/uploads/post/1/original/photo.jpg'
post.cover_photo.url(:big)
=> 'https://yourbucker.s3-eu-west-1.amazonaws.com/uploads/post/1/big/photo.jpg'
post.cover_photo.remove
=> true
post.cover_photo.present?
=> false
post.cover_photo.store(File.open('/var/www/somefile.jpg'))
# or store with custom name:
post.cover_photo.store(File.open('/var/www/somefile.jpg'), filename: 'photo.jpg')
post.cover_photo.present?
=> true
post.cover_photo.local_path
=> /path/to/rails/public/uploads/post/1/big/photo.jpg
# to reprocess all records
Post.find_each do |post|
post.cover_photo.reprocess
end
You may want to proxy/alias S3 assets throught your servers to hide original URL or to reduce S3 trafic using caching.
It's easy to write your own Remote
backend:
class CustomRemote < Storage::Remote
def url_for(filename, with_protocol: false)
protocol_prefix = if with_protocol
"http:"
else
""
end
"#{protocol_prefix}//storage.yourcompany.com/#{filename}"
end
end
class PhotoStorage < Storage::Model
version :original
version :big
def remote_klass
CustomRemote
end
end
post = Post.last
post.photo.url # returns your custom url instead of URL on amazonaws.com domain
=> //storage.yourcompany.com/uploads/post/1/big/1.jpg
Using Piet, Storage can optimize your images with jpegoptim
and optipng
:
class PhotoStorage < Storage::Model
include Storage::Helpers::OptimizeHelper
# declare versions...
def process_image(version, image)
optimize_with_piet(image)
end
end
- Fork it ( https://github.com/[my-github-username]/storage/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request