Direct (and simple!) AJAX upload to AWS S3 from (AngularJS) Single Page App Direct (and simple!) AJAX upload to AWS S3 from (AngularJS) Single Page App angularjs angularjs

Direct (and simple!) AJAX upload to AWS S3 from (AngularJS) Single Page App


I finally managed. The key points were:

  1. Let go of Angular's $http, and use native XMLHttpRequest instead.
  2. Use the getSignedUrl feature of AWS's SDK, instead on implementing my own signature-generating workflow like many libraries do.
  3. Set the AWS configuration to use the proper signature version (v4 at the time of writing) and region ('eu-central-1' in the case of Frankfurt).

Here below is a step-by-step guide of what I did; it uses AngularJS and NodeJS on the server, but should be rather easy to adapt to other stacks, especially because it deals with the most pathological cases (SPA on a different domain that the server, with a bucket in a recent - at the time of writing - region).


Workflow summary

  1. The user selects a file in the browser; your JavaScript keeps a reference to it.
  2. the client sends a request to your server to obtain a signed upload URL.
  3. Your server chooses a name for the object to put in the bucket (make sure to avoid name collisions!).
  4. The server obtains a signed URL for your object using the AWS SDK, and sends it back to the client. This involves the object's name and the AWS credentials.
  5. Given the file and the signed URL, the client sends a PUT request directly to your S3 Bucket.

Before you start

Make sure that:

  • Your server has the AWS SDK
  • Your server has AWS credentials with proper access rights to your bucket
  • Your S3 bucket has a proper CORS configuration for your client.

Step 1: setup a SPA-friendly file upload form / widget.

All that matters is to have a workflow that eventually gives you programmatic access to a File object - without uploading it.

In my case, I used the ng-file-select and ng-file-drop directives of the excellent angular-file-upload library. But there are other ways of doing it (see this post for example.).

Note that you can access useful information in your file object such as file.name, file.type etc.

Step 2: Get a signed URL for the file on your server

On your server, you can use the AWS SDK to obtain a secure, temporary URL to PUT your file from someplace else (like your frontend).

In NodeJS, I did it this way:

// ---------------------------------// some initial configurationvar aws = require('aws-sdk');aws.config.update({  accessKeyId: process.env.AWS_ACCESS_KEY,  secretAccessKey: process.env.AWS_SECRET_KEY,  signatureVersion: 'v4',  region: 'eu-central-1'});// ---------------------------------// now say you want fetch a URL for an object named `objectName`var s3 = new aws.S3();var s3_params = {  Bucket: MY_BUCKET_NAME,  Key: objectName,  Expires: 60,  ACL: 'public-read'};s3.getSignedUrl('putObject', s3_params, function (err, signedUrl) {  // send signedUrl back to client  // [...]});

You'll probably want to know the URL to GET your object to (typically if it's an image). To do this, I simply removed the query string from the URL:

var url = require('url');// ...var parsedUrl = url.parse(signedUrl);parsedUrl.search = null;var objectUrl = url.format(parsedUrl);

Step 3: send the PUT request from the client

Now that your client has your File object and the signed URL, it can send the PUT request to S3. My advice in Angular's case is to just use XMLHttpRequest instead of the $http service:

var signedUrl, file;// ...var d_completed = $q.defer(); // since I'm working with Angular, I use $q for asynchronous control flow, but it's not mandatoryvar xhr = new XMLHttpRequest();xhr.file = file; // not necessary if you create scopes like thisxhr.onreadystatechange = function(e) {  if ( 4 == this.readyState ) {    // done uploading! HURRAY!    d_completed.resolve(true);  }};xhr.open('PUT', signedUrl, true);xhr.setRequestHeader("Content-Type","application/octet-stream");xhr.send(file);

Acknowledgements

I would like to thank emil10001 and Will Webberley, whose publications were very valuable to me for this issue.


You can use the ng-file-upload $upload.http method in conjunction with the aws-sdk getSignedUrl to accomplish this. After you get the signedUrl back from your server, this is the client code:

var fileReader = new FileReader();fileReader.readAsArrayBuffer(file);fileReader.onload = function(e) {$upload.http({  method: 'PUT',  headers: {'Content-Type': file.type != '' ? file.type : 'application/octet-stream'},  url: signedUrl,  data: e.target.result}).progress(function (evt) {  var progressPercentage = parseInt(100.0 * evt.loaded / evt.total);  console.log('progress: ' + progressPercentage + '% ' + file.name);}).success(function (data, status, headers, config) {  console.log('file ' + file.name + 'uploaded. Response: ' + data);});


To do multipart uploads, or those larger than 5 GB, this process gets a bit more complicated, as each part needs its own signature. Conveniently, there is a JS library for that:

https://github.com/TTLabs/EvaporateJS

via https://github.com/aws/aws-sdk-js/issues/468