Skip to content

Upload to Amazon S3

jayarjo edited this page Apr 28, 2013 · 21 revisions

Being able to upload files to Amazon S3, especially in HTML5, has been a goal for quite some time and while it was somehow possible in Flash and Silverlight, HTML5 was out of the game. Amazon S3 simply refused to send Access-Control-Allow-Origin header - that single miraculous one that makes AJAX requests to suddenly reach the server across domains. Finally, after continuous lament from users, with bleeding shouts like: _"Two and a half year later, still no cigar..." - Amazon made it happen.

So now it's possible.

Table of Contents

## Disclaimer
  • We assume that you already have active S3 account.
  • We describe the most generic scenario. Feel free to customize it to your needs.
  • This is a working draft. Suggestions are welcome.
## Preface

Do not expect it to just work. It is definitely achievable, but still requires some effort (not that much though). Each runtime has it's own specifics and requirements. Flash and Silverlight are similar in some sense and in general can share exactly the same configuration, both server- and client-side. But again - there's an option.

## Prepare server-side (S3)

First, you need to create a bucket and make it accessible. We simply grant Upload/Delete permissions to Everyone.

edit bucket permissions

And done.

Next step.

### ... for HTML5 runtime

In the same Permissions section there is an option to Add CORS Configuration (if you do not know what the CORS is - HTML5 Rocks has an excellent write-up about it):

Add CORS Configuration

As always there are some options, but we will use the most generic configuration to make sure that S3 bucket is indeed compatible with our HTML5 upload:

<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedHeader>*</AllowedHeader>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
    </CORSRule>
</CORSConfiguration>

What we say here is that we allow cross-domain access from any domain, using requests with any headers, via GET or POST. Preflight requests will be cached for 3000 secs.

### ... for Flash runtime

To support cross-origin requests, Flash requires crossdomain.xml - special policy file at the root of your bucket with such content for example:

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

Again, we simply allow all domains here. You might want to restrict this to specific ones only. Also notice an attribute secure="false" - it will make your bucket accessible via HTTPS. If you want to allow only secured connections, then set this attribute to true.

Finally, do not forget to make crossdomain.xml public - files do not automatically inherit bucket permissions and you have to set them manually.

### ... for Silverlight runtime

In fact you can stop here and do nothing else for Silverlight.

2. Generating a policy and a signature

Uploading to S3 with multipart requires a policy and a signature. See this article for detailed information. These two will be passed to Plupload as multipart params (more on this later).

Heres a working policy generator written in Ruby. The code is intentionnaly simple and uses globals, please do not blindly copy-paste :

require 'base64'
require 'json'
require 'digest/sha1'

$ACL = 'public-read' # Change this according to your needs
$BUCKET = 'YOUR_BUCKET'
$AWS_SECRET = 'YOUR_SECRET'

def policy
  conditions = [
    ["starts-with", "$utf8", ""],
    # Change this path if you need, but adjust the javascript config
    ["starts-with", "$key", "uploads"],
    ["starts-with", "$filename", ""],
    { "bucket" => $BUCKET },
    { "acl" => $ACL }
  ]

  policy = {
    # Valid for 3 hours. Change according to your needs
    'expiration' => (Time.now.utc + 3600 * 3).iso8601,
    'conditions' => conditions
  }

  Base64.encode64(JSON.dump(policy)).gsub("\n","")
end

def signature
  Base64.encode64(
    OpenSSL::HMAC.digest(
      OpenSSL::Digest::Digest.new('sha1'),
      $AWS_SECRET, policy
    )
  ).gsub("\n","")
end

3. Configuring Plupload

{
  // General settings
  runtimes : 'flash,html5',

  // Flash settings
  flash_swf_url : '/plupload/src/moxie/bin/flash/Moxie.swf',

  // S3 specific settings
  url : "https://<%= $BUCKET %>.s3.amazonaws.com:443/",
  file_name_name: false, // Custom option to our fork to remove file_name_name
  multipart: true,
  multipart_params : {
    // Dummy filename to ensure the field is sent in HTML just like Flash
    // This allow to have a consistent AWS policy for both HTML and Flash
    filename: 'filename',
    utf8: true,

    AWSAccessKeyId: "YOUR_AWS_ACCESS_KEY_ID",
    acl: "public-read", // See http://docs.aws.amazon.com/AmazonS3/latest/dev/ACLOverview.html#CannedACL
    // See Generating a policy and a signature
    policy: "YOUR_POLICY",
    signature: "YOUR_SIGNATURE",

    // This is basically the resulting location of the file on S3.
    // See http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html#RESTObjectPOST-requests-form-fields
    //
    // WARNING : Change this to suite your needs.
    // You need to insert some sort of unique identifier to avoid
    // overriding files if they share the same filename.
    key: "uploads/${filename}",
  }
}

4. Putting it all together

I made a little Sinatra one-file-app that shows how to use all of this together.

Caveats

  • S3's success_action_redirect does not work in Chrome. I think the problem is that Moxie's XHR does not handle well redirect codes, but investigation is needed.
  • This demo requires a patched version of Plupload. Pull request #750 has to be merged for this to work with master.

Refs