Skip to content

Instantly share code, notes, and snippets.

@weaver
Created October 2, 2012 15:31
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save weaver/3820194 to your computer and use it in GitHub Desktop.
Save weaver/3820194 to your computer and use it in GitHub Desktop.
S3 Upload Example
node_modules
.#*
*~

S3 Upload Example

This example demonstrates how to upload a file directly to S3 from a browser. The Node server generates a policy and signature, authorizing the upload.

Once a file has been successfully uploaded, S3 redirects the browser to a Node callback url.

Get Started

To get started:

npm install
export AWS_KEY='your-aws-key'
export AWS_SECRET='your-key-secret'
export S3_BUCKET='your-bucket-name'
node app.js

Uploaded Files

Files are uploaded into example/{{uuid}}/filename.ext. By default, S3 will serve uploaded files as application/octet-stream. Pass a contentType parameter to choose a more specific type. For example:

open http://localhost:3000/?contentType=text/html

The policy generated for the upload allows any content type, so the Content-Type could also be set client side.

var Express = require('express'),
Cons = require('consolidate'),
Uuid = require('node-uuid'),
Path = require('path'),
Url = require('url'),
Crypto = require('crypto'),
app = Express();
// ## App Configuration ##
// The canonical base url for this site is used to create success
// redirect urls.
app.set('Base Url', Url.parse('http://localhost:3000/'));
// The name of the bucket is used to create bucket urls and upload
// policies.
app.set('S3 Bucket Name', process.env['S3_BUCKET']);
// A folder in the bucket that files are uploaded into. This is
// enforced by the policy. The entire upload destination is
// {{S3 Folder}}/{{UUID}}/{{filename}}
app.set('S3 Folder', 'example');
// The visibility of uploaded files. Choose `private` if downloads
// must be authorized with a signature. The default is `public-read`
// because files are uploaded over https and have a random name.
app.set('S3 ACL', 'public-read');
// The AWS key id. Set this in the environment.
app.set('AWS Key', process.env['AWS_KEY']);
// The AWS key secret. Set this in the environment.
app.set('AWS Secret', process.env['AWS_SECRET']);
// When an upload policy is generated, it's only valid for a certain
// period of time. Specify the period in milliseconds from the time the
// policy is created. A fresh policy is created for each upload form
// request. The default here is 1 minute.
app.set('Upload Valid Millis', 1000 * 60 * 1);
// Restrict the uploaded file to a maximum size. By default, 10MB.
app.set('Max File Size Bytes', 1024 * 1024 * 1024 * 10);
// ## Express Configuration ##
app.engine('html', Cons.swig);
app.set('view engine', 'html');
app.set('views', __dirname + '/views');
app.disable('view cache');
app.use(Express.logger('tiny'));
app.use(app.router);
app.use(function(err, req, res, next) {
console.error(err.stack);
res.send(500, 'server error');
});
process.nextTick(function() {
var base = app.get('Base Url');
// Calculated settings
app.set('S3 Bucket', 'https://s3.amazonaws.com/' + app.get('S3 Bucket Name') + '/');
app.set('S3 Bucket Url', Url.parse(app.get('S3 Bucket')));
app.listen(base.port || (base.protocol === 'https:' ? 443 : 80));
console.log('Listening:', Url.format(base));
});
// ## Helpers ##
var urlBase64 = (function() {
var alphabet = { '+': '-', '/': '_' };
return function urlBase64(buffer) {
return buffer.toString('base64')
.replace(/[\+\/]/g, function(token) {
return alphabet[token];
})
.replace(/=+$/, '');
};
})();
function uuid() {
var buffer = new Buffer(16);
Uuid.v4(null, buffer);
return urlBase64(buffer);
}
function siteUrl(dest, query) {
var to = dest;
if (query) {
to = Url.parse(dest);
to.query = query;
}
return Url.resolve(app.get('Base Url'), to);
}
// ## S3 ##
// Generate a policy and signature for upload options.
// See also: http://aws.amazon.com/articles/1434
function makePolicy(expires, conditions) {
var when = new Date(Date.now() + expires),
obj = { expiration: when.toJSON(), conditions: conditions },
bytes = new Buffer(JSON.stringify(obj));
// console.log('policy bytes', bytes);
return bytes.toString('base64');
}
function sign(policy) {
var secret = app.get('AWS Secret');
return Crypto.createHmac('sha1', secret)
.update(policy)
.digest('base64');
}
function addPolicy(opt) {
var expires = app.get('Upload Valid Millis'),
bucket = app.get('S3 Bucket Name'),
maxSize = app.get('Max File Size Bytes');
opt.policy = makePolicy(expires, [
{bucket: bucket},
['starts-with', '$key', Path.dirname(opt.fileKey) + '/'],
{acl: opt.acl},
{success_action_redirect: opt.success},
['starts-with', '$Content-Type', ''],
['content-length-range', 0, maxSize]
]);
opt.signature = sign(opt.policy);
// console.log('policy', opt.policy);
// console.log('signature', opt.signature);
return opt;
}
function s3Url(key) {
return Url.resolve(app.get('S3 Bucket Url'), key);
}
// ## Views ##
// Create a random, unique name for each upload by generating a
// uuid. Accept a `contentType` parameter to specify the content type of
// the uploaded file. This could also be set on the client-side.
// Once the file has been successfully uploaded, the browser will be
// redirected to the success url `/uploaded`. S3 adds some query
// parameters including the file's key and its etag. Additional
// application parameters (like an upload session token) can be
// encoded into the url before passing it to S3.
app.get('/', function(req, res) {
res.render('index', addPolicy({
action: app.get('S3 Bucket'),
fileKey: Path.join(app.get('S3 Folder'), uuid(), '${filename}'),
accessKey: app.get('AWS Key'),
acl: app.get('S3 ACL'),
success: siteUrl('/uploaded', { example: 'parameter' }),
contentType: req.param('contentType') || 'application/octet-stream'
}));
});
app.get('/uploaded', function(req, res) {
var info = req.query;
console.log('uploaded', info);
res.render('uploaded', {
name: Path.basename(info.key),
href: s3Url(info.key)
});
});
<DOCTYPE html>
<html>
<head>
<title>S3 Upload Example</title>
</head>
<body>
<h1>Upload an SVG</h1>
<form method="POST" action="{{ action }}" enctype="multipart/form-data">
<input type="hidden" name="key" value="{{ fileKey }}" />
<input type="hidden" name="AWSAccessKeyId" value="{{ accessKey }}" />
<input type="hidden" name="acl" value="{{ acl }}" />
<input type="hidden" name="success_action_redirect" value="{{ success }}" />
<input type="hidden" name="policy" value="{{ policy }}" />
<input type="hidden" name="signature" value="{{ signature }}" />
<input type="hidden" name="Content-Type" value="{{ contentType }}" />
<input name="file" type="file" />
<input type="submit" value="Upload!" />
</form>
</body>
</html>
{
"name": "s3-upload",
"description": "Example HTTP upload directly to S3 from browser",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "3.0.0rc4",
"consolidate": "0.4.0",
"swig": "0.12.0",
"node-uuid": "1.3.3"
}
}
<DOCTYPE html>
<html>
<head>
<title>Uploaded!</title>
</head>
<body>
<h1>Upload Success</h1>
<a href="{{ href }}">{{ name }}</a>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment