Tuesday, April 21, 2009

Getting S3 and SWFUpload to Cooperate in Rails

Updated on 4/27: Added the workaround for the bug in the Mac's flash plugin.

(First things first, there is a rails plugin that automates a lot of this process. I didn't like the way it worked for a few reasons (like making swfobject.js into one big javascript variable), so I decided not to use it. PJ at GitHub mentioned that they use it, so it must not be all bad.)

These are some stumbling blocks I ran into and I thought I'd try to save others some frustration. I'll try to link or acknowledge the particular sources where I read these nuggets, but I've been googling like mad the last few days and might have forgotten.

Signature and Policy generation
elcgit decided to reinvent some wheels with the signing and policy generation bit, and wrote his own base64 encoding routines. This is unnecessary, because ruby includes openssl and base 64 encoding capabilities in the stdlib. In fact, amazon provides sample code for this on this helpful documentation page:

require 'base64'
require 'openssl'
require 'digest/sha1'

policy = Base64.encode64(policy_document).gsub("\n","")

signature = Base64.encode64(
aws_secret_key, policy)

S3 Post Params

  • How to pass the required POST parameters to S3 with SWFUpload might seem somewhat obvious, but I wasn't sure how to do this in the beginning. You merely need a line like when setting up your SWFUpload object:

    post_params : <%= @s3.to_json %>,

    I made a class to encapsulate all the S3 stuff, created an instance in the controller, and then call the .to_json method (delegated to hash) so that I didn't have to specify all that stuff in the view.

  • S3 requires a field called "file" to be present in the POST parameters, but by default Flash (and therefore SWFUpload) name this field something else. You can change this by including this line in your SWFUpload configuration:

    file_post_name : "file",

  • S3 requires all POST parameters to be present in the policy that's signed and passed in the parameters, so make sure to add this line to tell S3 to expect a parameter that SWFUpload provides by default:

    ["starts-with", "$Filename", ""]

  • To work around an apparent bug in the Mac's flash plugin, I had to add the "success_action_status" key to the S3 policy and POST parameters with a value of 201. According to the S3 docs, the default return code of 204 is sent with no response body, which apparently triggers the bug.

SWFUpload and Return Codes
Depending on a few options you specify, S3 can return a 200, 201 or 204 for a successful upload. By default, SWFUpload treats any return code other than 200 as an error. You can fix this on the S3 side by supplying the success_action_status parameter, but I chose to make it so that SWFUpload would regard those three return codes as successes by adding this to my SWFUpload config:

http_success : [ 200, 201, 204 ],

Host the SWFUpload File on S3
SWFUpload does not have very good support for uploading to a different domain than the actual .swf file is hosted on due to crossdomain issues. I spent a day or two trying to make this work with the proper settings in a crossdomain.xml file, but gave up and decided to host the .swf on S3. This adds a bit of a deployment headache, but it's a much simpler solution (and possibly the only one).

Debug By Uploading to Rails
SWFUpload does not provide a lot of debugging help; sure, it has a debug mode which gives some info, but for a lot of server-side things it's not very helpful (I'd have been saved a lot of trouble if SWFUpload would display the response body and not just the return code on a failed upload, for example). You can get a look at the POST parameters being sent if you set SWFUpload's upload_url setting to a dummy action in your app and looking at the logs.

Functional and Acceptance Testing
I've recently started using Cucumber for testing and love it. Unfortunately, I wasn't able to get it to work with SWFUpload properly; this led to me essentially "flying blind" with no tests and having to do a lot of testing in the browser (yuck!). If I can find out how to do this (or someone else lets me know), I'll update this entry to have that info.