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!
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?
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!
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
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!