Skip to content

Instantly share code, notes, and snippets.

@weaver
Created October 2, 2012 15:31

Revisions

  1. weaver revised this gist Oct 2, 2012. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions app.js
    Original file line number Diff line number Diff line change
    @@ -77,7 +77,6 @@ var urlBase64 = (function() {
    return function urlBase64(buffer) {
    return buffer.toString('base64')
    .replace(/[\+\/]/g, function(token) {
    console.log('replace', token, alphabet, alphabet[token]);
    return alphabet[token];
    })
    .replace(/=+$/, '');
    @@ -113,7 +112,7 @@ function makePolicy(expires, conditions) {
    obj = { expiration: when.toJSON(), conditions: conditions },
    bytes = new Buffer(JSON.stringify(obj));

    console.log('policy bytes', bytes);
    // console.log('policy bytes', bytes);

    return bytes.toString('base64');
    }
  2. weaver revised this gist Oct 2, 2012. 6 changed files with 233 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions .gitignore
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    node_modules
    .#*
    *~
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -27,3 +27,4 @@ will serve uploaded files as `application/octet-stream`. Pass a

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

    186 changes: 186 additions & 0 deletions app.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,186 @@
    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) {
    console.log('replace', token, alphabet, alphabet[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)
    });
    });
    12 changes: 12 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    {
    "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"
    }
    }
    21 changes: 21 additions & 0 deletions views/index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    <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>
    10 changes: 10 additions & 0 deletions views/uploaded.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    <DOCTYPE html>
    <html>
    <head>
    <title>Uploaded!</title>
    </head>
    <body>
    <h1>Upload Success</h1>
    <a href="{{ href }}">{{ name }}</a>
    </body>
    </html>
  3. weaver created this gist Oct 2, 2012.
    29 changes: 29 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,29 @@
    ## 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.