UNPKG

s3-sync

Version:

A streaming upload tool for Amazon S3

193 lines (164 loc) 5.32 kB
var LevelWriteStream = require('level-write-stream') , createQueue = require('queue-async') , backoff = require('backoff') , es = require('event-stream') , crypto = require('crypto') , xtend = require('xtend') , mime = require('mime') , once = require('once') , knox = require('knox') , url = require('url') , fs = require('fs') module.exports = s3syncer function s3syncer(db, options) { if (!options) { options = db || {} db = false } options.concurrency = options.concurrency || 16 options.headers = options.headers || {} options.cacheSrc = options.cacheSrc || __dirname + '/.sync' options.cacheDest = options.cacheDest || '/.sync' options.retries = options.retries || 7 options.acl = options.acl || 'public-read' options.force = !!options.force var client = knox.createClient(options) , queue = createQueue(options.concurrency) , region = options.region === 'us-standard' ? false : options.region , secure = options.secure || !('secure' in options) , subdomain = region ? 's3-' + region : 's3' , protocol = secure ? 'https' : 'http' , prefix = options.prefix || '' , hashkey = options.hashKey || function(details) { return details.fullPath } var stream = es.map(function(data, next) { queue.defer(function(details, done) { details.fullPath = details.fullPath || details.src details.path = details.path || details.dest syncFile(details, function(err) { return err ? next(err) : done(), next(null, details) }) }, data) }) stream.getCache = getCache stream.putCache = putCache function syncFile(details, next) { var absolute = details.fullPath , relative = prefix + (details.path.charAt(0) === '/' ? details.path.slice(1) : details.path) relative = relative.replace(/\\/g, '/') var destination = protocol + '://' + subdomain + '.amazonaws.com/' + options.bucket + '/' + relative hashFile(absolute, destination, function(err, md5) { if (err) return next(err) details.md5 = md5 details.url = destination details.fresh = false details.cached = false if (!db) return checkForUpload(next) var key = 'md5:' + hashkey(details) db.get(key, function(err, result) { if (!err && result === md5) { details.cached = true return next(null, details) } checkForUpload(function(err) { if (err) return next(err) db.put(key, md5, next) }) }) }) function checkForUpload(next) { client.headFile(relative, function(err, res) { if (err) return next(err) if ( options.force || res.statusCode === 404 || ( res.headers['x-amz-meta-syncfilehash'] !== details.md5 )) return uploadFile(details, next) if (res.statusCode >= 300) return next(new Error('Bad status code: ' + res.statusCode)) return next(null, details) }) } } function uploadFile(details, next) { var absolute = details.fullPath , relative = prefix + details.path , lasterr , off = backoff.fibonacci({ initialDelay: 1000 }) relative = relative.replace(/\\/g, '/') details.fresh = true off.failAfter(options.retries) off.on('fail', function() { next(lasterr || new Error('unknown error')) }).on('ready', function() { var headers = xtend({ 'x-amz-acl': options.acl , 'x-amz-meta-syncfilehash': details.md5 , 'Content-Type': mime.lookup(absolute) }, options.headers) client.putFile(absolute, relative, headers, function(err, res) { if (!err) { if (res.statusCode < 300) return next(null, details) err = new Error('Bad status code: ' + res.statusCode) } lasterr = err stream.emit('fail', err) off.backoff() }) }).backoff() } function getCache(callback) { callback = once(callback) client.getFile(options.cacheDest, function(err, res) { if (err) return callback(err) if (res.statusCode === 404) return callback(null) es.pipeline( res , es.split() , es.parse() , LevelWriteStream(db)() ).once('close', callback) .once('error', callback) }) } function putCache(callback) { callback = once(callback) db.createReadStream() .pipe(es.stringify()) .pipe(fs.createWriteStream(options.cacheSrc)) .once('error', callback) .once('close', function() { client.putFile(options.cacheSrc, options.cacheDest, function(err) { if (err) return callback(err) fs.unlink(options.cacheSrc, callback) }) }) } function hashFile(filename, destination, callback) { var hash = crypto.createHash('md5') , done = false hash.update(JSON.stringify([ options.headers , destination ])) fs.createReadStream(filename).on('data', function(d) { hash.update(d) }).once('error', function(err) { if (!done) callback(err) done = true }).once('close', function() { if (!done) callback(null, hash.digest('hex')) done = true }) } return stream }