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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<% 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.

1
2
3
4
5
<?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.

1
2
3
4
5
6
7
8
9
10
11
  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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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!

Comments

Copyright © 2013 - Tom Pesman - Powered by Octopress