|
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) |
|
}); |
|
}); |