SWFUpload direct to Amazon S3 in Ruby on Rails

I’m working on various projects and for one certain project this company wanted a file sharing website, like yousendit.com for example, but the site should be in-house. I proposed Amazon S3 for the storage of the files, otherwise the VPS will become very expensive. This file sharing website should also handle large files, so a reliable upload method is desired. SWFUpload is a well known Flash upload application. So the requirements are now complete: Ruby on Rails, Amazon S3 and SWFUpload.

First I created a config file to enter my Amazon S3 credentials. The credentials are dependent on the Ruby on Rails environment.

config/amazon_s3.yml

development:
  bucket_name: BUCKET_NAME
  access_key_id: ACCESS_KEY_ID
  secret_access_key: SECRET_ACCESS_KEY

test:
  bucket_name: BUCKET_NAME
  access_key_id: ACCESS_KEY_ID
  secret_access_key: SECRET_ACCESS_KEY

production:
  bucket_name: BUCKET_NAME
  access_key_id: ACCESS_KEY_ID
  secret_access_key: SECRET_ACCESS_KEY

Created a controller and added the index method. The method reads the S3 settings from the config file and generates the fields required for SWFUpload and S3.

  def index
    filename = "#{RAILS_ROOT}/config/amazon_s3.yml"
    config = YAML.load_file(filename)

    bucket            = config[ENV['RAILS_ENV']]['bucket_name']
    access_key_id     = config[ENV['RAILS_ENV']]['access_key_id']
    secret_access_key = config[ENV['RAILS_ENV']]['secret_access_key']

    key             = ENV['RAILS_ENV']
    acl             = 'public-read'
    expiration_date = 10.hours.from_now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z')
    max_filesize    = 2.gigabyte

    policy = Base64.encode64(
      "{'expiration': '#{expiration_date}',
        'conditions': [
          {'bucket': '#{bucket}'},
          ['starts-with', '$key', '#{key}'],
          {'acl': '#{acl}'},
          {'success_action_status': '201'},
          ['starts-with', '$Filename', ''],
          ['content-length-range', 0, #{max_filesize}]
        ]
      }").gsub(/\n|\r/, '')

    signature = Base64.encode64(
                  OpenSSL::HMAC.digest(
                    OpenSSL::Digest::Digest.new('sha1'),
                    secret_access_key, policy)).gsub("\n","")

    @post = {
      "key" => "#{key}/${filename}",
      "AWSAccessKeyId" => "#{access_key_id}",
      "acl" => "#{acl}",
      "policy" => "#{policy}",
      "signature" => "#{signature}",
      "success_action_status" => "201"
    }

    @upload_url = "http://#{bucket}.s3.amazonaws.com/"
  end

And the index.html.erb view.

<% content_for :head do %>
<link href="/stylesheets/swfupload.css" rel="stylesheet" type="text/css" />
<% end%>

<script type="text/javascript" src="/javascripts/swfupload/swfupload.js"></script>
<script type="text/javascript" src="/javascripts/swfupload/swfupload.queue.js"></script>
<script type="text/javascript" src="/javascripts/swfupload/fileprogress.js"></script>
<script type="text/javascript" src="/javascripts/swfupload/handlers.js"></script>
<script type="text/javascript">
	var swfu;

	window.onload = function() {
		var settings = {
			flash_url : "/assets/swfupload.swf",
			upload_url: "<%= @upload_url %>",
			http_success : [ 200, 201, 204 ], 		// FOR AWS

			file_size_limit : "2 GB",
			file_types : "*.*",
			file_types_description : "All Files",
			file_upload_limit : 100,
			file_queue_limit : 0,
			file_post_name : "file", 				// FOR AWS

			custom_settings : {
				progressTarget : "fsUploadProgress",
				cancelButtonId : "btnCancel"
			},
			debug: <%= ENV['RAILS_ENV']=='development' ? 'true' : 'false' %>,

			// Button settings
			button_image_url : "/images/buttonUploadText.png",
			button_placeholder_id : "spanButtonPlaceHolder",
			button_width: 61,
			button_height: 22,

			// The event handler functions are defined in handlers.js
			file_queued_handler : fileQueued,
			file_queue_error_handler : fileQueueError,
			file_dialog_complete_handler : fileDialogComplete,
			upload_start_handler : uploadStart,
			upload_progress_handler : uploadProgress,
			upload_error_handler : uploadError,
			upload_success_handler : uploadSuccess,
			upload_complete_handler : uploadComplete,
			queue_complete_handler : queueComplete,	// Queue plugin event

			post_params: <%= @post.to_json %>		// FOR AWS
		};

		swfu = new SWFUpload(settings);
     };
</script>

<div id="content">
	<form id="form" action="/upload/upload" method="post" enctype="multipart/form-data">
			<div class="fieldset flash" id="fsUploadProgress">
			<span class="legend">Upload Queue</span>
			</div>
		<div id="divStatus">0 Files Uploaded</div>
			<div>
				<span id="spanButtonPlaceHolder"></span>
				<input id="btnCancel" type="button" value="Cancel All Uploads" onclick="swfu.cancelQueue();" disabled="disabled" style="margin-left: 2px; font-size: 8pt; height: 29px;" />
			</div>

	</form>
</div>

Upload the file crossdomain.xml to the root of your bucket. This is for Flash to upload to a different domain.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-access-from domain="*" secure="false" />
</cross-domain-policy>

Finally took the files from the SWFUpload simpledemo and placed them in the following directories:

  • swfupload.swf in public/assets/
  • fileprogress.js, handles.js, swfupload.js and swfupload.queue.js in public/javascripts/swfupload/
  • buttonUploadText.png in public/images/

Now SWFUpload should be working in your Ruby on Rails application.

Callback
For my application I needed a callback to let my application know there was a file successfully uploaded to the S3 bucket. To get this functionality I added a function to the controller and modified the handlers.js file.

  def upload_done
    file = ShareFile.new

    file.name = params[:name]
    file.filestatus = params[:filestatus]
    file.filetype = params[:type]
    file.size = params[:size]
    file.s3_available = true

    file.save
  end
function uploadSuccess(file, serverData) {
	// HERE: Send a notification upload has succeeded
	new Ajax.Request('/share/upload_done?'+Object.toQueryString(file), {
		method:'get',
		asynchronous: false,
		onSuccess: function(){
			var progress = new FileProgress(file, this.customSettings.progressTarget);
			progress.setStatus("Sending meta data.");
		}
	});
	// HERE: end

	try {
		var progress = new FileProgress(file, this.customSettings.progressTarget);
		progress.setComplete();
		progress.setStatus("Complete.");
		progress.toggleCancel(false);

	} catch (ex) {
		this.debug(ex);
	}
}

When SWFUpload is done uploading it uses the javascript callback to update the status of the form and to send a notification to the Ruby on Rails application.

Good luck!

This entry was posted in Ruby on Rails and tagged , , . Bookmark the permalink.

4 Responses to SWFUpload direct to Amazon S3 in Ruby on Rails

  1. Kris says:

    Thanks for posting this article, it has been a wonderful aid.

    Have you run into any issues with SWFUpload returning a 2038 or 2049 error?

  2. Tom says:

    I did run into some problems with uploading to Amazon S3. To see where the problem lies I used a normal upload form to test if the policy and signature are correct. After that I added to SWFUpload form. Good luck!

  3. Denis says:

    Hi,

    Thanks for your post, for those who use Rails 3, in ‘index.html.erb’ don’t forget to use the ‘raw’ method like this “post_params: < %= raw @post.to_json %> “. Cheers

  4. Hi,

    Just wanted to say thanks for your post – it really helped me to implement SWF uploads to S3 for my app. I looked at various plugins for this functionality but in the end it was useful to implement it following your guide and really know what was going on under the covers.

    Thanks again!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>