Starting from a fresh Rails application (I’m using 2.0.2), install AttachmentFu:
1 script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/
Edit config/amazon_s3.yml and put this:
config/amazon_s3.yml
1 development: 2 bucket_name: amazon-sqs-development-yourname 3 access_key_id: "your key" 4 secret_access_key: "your secret access key" 5 queue_name: amazon-sqs-development-resizer-yourname
queue_name is new. AttachmentFu does not require this, but we are going to reuse the file from our own code, so better put all configuration in the same place.
Generate a scaffolded Photo model using:
1 $ script/generate scaffold photo filename:string size:integer content_type:string width:integer height:integer parent_id:integer thumbnail:string
Edit app/views/photos/new.erb.html and replace everything with this:
app/views/photos/new.erb.html
1 <h1>New photo</h1> 2 3 <%= error_messages_for :photo %> 4 5 <% form_for(@photo, :html => {:multipart => true}) do |f| %> 6 <p> 7 <label for="photo_uploaded_data">File:</label> 8 <%= f.file_field :uploaded_data %> 9 </p> 10 11 <p> 12 <%= f.submit "Create" %> 13 </p> 14 <% end %> 15 16 <%= link_to 'Back', photos_path %>
What we did here is simply tell Rails to use a multipart encoded form, and to only provide us with a single file upload field.
Edit app/models/photo.rb and add the AttachmentFu plugin configuration:
app/models/photo.rb
1 class Photo < ActiveRecord::Base 2 has_attachment :content_type => :image, :storage => :s3 3 validates_as_attachment 4 end
Start your server and confirm you can upload a file. No thumbnails were generated as we did not configure any thumbnailing to do. We don’t actually want AttachmentFu to handle that, so we can’t just specify it in the has_attachment call.
To use RightScale’s AWS SQS component, we have to configure it with the access key and secret access key. Add this to the end of the Photo class:
app/models/photo.rb
1 class Photo < ActiveRecord::Base 2 def queue 3 self.class.queue 4 end 5 6 class << self 7 def queue 8 # This creates the queue if it doesn't exist 9 @queue ||= sqs.queue(aws_config["queue_name"]) 10 end 11 12 def sqs 13 @sqs ||= RightAws::Sqs.new( 14 aws_config["access_key_id"], aws_config["secret_access_key"], 15 :logger => logger) 16 end 17 18 def aws_config 19 return @aws_config if @aws_config 20 21 @aws_config = YAML.load(File.read(File.join(RAILS_ROOT, "config", "amazon_s3.yml"))) 22 @aws_config = @aws_config[RAILS_ENV] 23 raise ArgumentError, "Missing #{RAILS_ENV} configuration from config/amazon_s3.yml file." if @aws_config.nil? 24 @aws_config 25 end 26 end 27 end
#aws_config is a method that reads the configuration. #sqs is a method that provides access to an instance of RightScale::Sqs, pre-configured with the correct access keys. #queue uses #sqs to get or create a named queue. There’s also an instance version of #queue, to ease our code later on.
Let’s add the request sending:
app/models/photo.rb
1 class Photo < ActiveRecord::Base 2 def send_resize_request 3 # Don't send a resize request for thumbnails 4 return true unless self.parent_id.blank? 5 6 params = Hash.new 7 params[:id] = self.id 8 params[:sizes] = Hash.new 9 params[:sizes][:square] = "75x75" 10 params[:sizes][:thumbnail] = "100x" 11 12 begin 13 queue.push(params.to_yaml) 14 rescue 15 logger.warn {"Unable to send resize request. Error: #{$!.message}"} 16 logger.warn {$!.backtrace.join("\n")} 17 18 # Don't raise the error so the request goes through. 19 # We don't want the user to see a 500 error because 20 # we can't talk to Amazon. 21 end 22 end 23 end
Now, this is getting interesting. AttachmentFu knows if the current model is a thumbnail or not by looking at parent_id. If it’s nil, we are the parent, else we are a thumbnail. We do the same thing here.
Then, we setup a couple of parameters to send to the resizer. Notice we send the actual thumbnail sizes in the message itself.
Next, we do the most important part: queue.push. This sends a message string (limited to 256 KiB) to Amazon SQS, and returns. If there is an error, we don’t actually want to prevent the request from completing, so we rescue any exceptions and log them. If you have the ExceptionNotifier plugin installed, this is a good place to log to it.
Now that we have a way to send the resize request, we have to execute it at some point. The controller is not the right place to do it. If you create Photo models from more than one controller, you’re bound to forget to call #send_resize_request. It’s better to do it in an #after_create callback, which we’ll do with a single line:
app/models/photo.rb
1 class Photo < ActiveRecord::Base 2 after_create :send_resize_request 3 end
Next, we have to receive the messages. So, we write a new method in Photo:
app/models/photo.rb
1 class Photo < ActiveRecord::Base 2 class << self 3 def fetch_and_thumbnail 4 messages = queue.receive_messages(20) 5 return if messages.blank? 6 7 logger.debug {"==> Photo\#fetch_and_thumbnail -- received #{messages.size} messages"} 8 messages.each do |message| 9 params = YAML.load(message.body) 10 photo = Photo.find_by_id(params[:id]) 11 if photo.blank? then 12 # The Photo was deleted before we got a chance to thumbnail it. 13 # We must delete the message, or we'll always get it afterwards. 14 message.delete 15 next 16 end 17 18 photo.generate_thumbnails(params[:sizes]) 19 message.delete 20 end 21 end 22 end 23 end
The first thing we do is see if there are any messages. The call to #queue is the helper method we defined earlier on. We ask to receive up to 20 messages at a time. If there were no messages, we simply return.
Then, for each message, we have to process it, so we iterate over each message, retrieving the original parameters Hash. The important thing to do is to delete the message after we have processed it, or else the message will still be visible next time around.
#generate_thumbnails is important, but uninteresting in this discussion.