react-deploy
Version:
a deployment script for create react app to s3
1,712 lines (1,536 loc) • 48.4 kB
JavaScript
let AWS = require('aws-sdk')
let EventEmitter = require('events').EventEmitter
let fs = require('graceful-fs')
let url = require('url')
let rimraf = require('rimraf')
let findit = require('findit2')
let Pend = require('pend')
let path = require('path')
// let crypto = require('crypto')
let mkdirp = require('mkdirp')
let assert = require('assert')
let MultipartETag = require('./lib/multipart_etag')
let fdSlicer = require('fd-slicer')
let mime = require('mime')
let StreamSink = require('streamsink')
let PassThrough = require('stream').PassThrough
let ScmTable = require('./lib/scm-table')
const generate = require('./lib/git-commit')
const RSVP = require('rsvp')
let MAX_PUTOBJECT_SIZE = 5 * 1024 * 1024 * 1024
let MAX_DELETE_COUNT = 1000
let MAX_MULTIPART_COUNT = 10000
let MIN_MULTIPART_SIZE = 5 * 1024 * 1024
let TO_UNIX_RE = new RegExp(quotemeta(path.sep), 'g')
exports.createClient = function (options) {
return new Client(options)
}
exports.getPublicUrl = getPublicUrl
exports.getPublicUrlHttp = getPublicUrlHttp
exports.Client = Client
exports.MultipartETag = MultipartETag
exports.AWS = AWS
exports.MAX_PUTOBJECT_SIZE = MAX_PUTOBJECT_SIZE
exports.MAX_DELETE_COUNT = MAX_DELETE_COUNT
exports.MAX_MULTIPART_COUNT = MAX_MULTIPART_COUNT
exports.MIN_MULTIPART_SIZE = MIN_MULTIPART_SIZE
function Client (options) {
options = options || {}
this.s3 = options.s3Client || new AWS.S3(options.s3Options)
this.s3Pend = new Pend()
this.s3Pend.max = options.maxAsyncS3 || 20
this.s3RetryCount = options.s3RetryCount || 3
this.s3RetryDelay = options.s3RetryDelay || 1000
this.multipartUploadThreshold = options.multipartUploadThreshold || (20 * 1024 * 1024)
this.multipartUploadSize = options.multipartUploadSize || (15 * 1024 * 1024)
this.multipartDownloadThreshold = options.multipartDownloadThreshold || (20 * 1024 * 1024)
this.multipartDownloadSize = options.multipartDownloadSize || (15 * 1024 * 1024)
if (this.multipartUploadThreshold < MIN_MULTIPART_SIZE) {
throw new Error('Minimum multipartUploadThreshold is 5MB.')
}
if (this.multipartUploadThreshold > MAX_PUTOBJECT_SIZE) {
throw new Error('Maximum multipartUploadThreshold is 5GB.')
}
if (this.multipartUploadSize < MIN_MULTIPART_SIZE) {
throw new Error('Minimum multipartUploadSize is 5MB.')
}
if (this.multipartUploadSize > MAX_PUTOBJECT_SIZE) {
throw new Error('Maximum multipartUploadSize is 5GB.')
}
}
Client.prototype.deleteObjects = function (s3Params) {
let self = this
let ee = new EventEmitter()
let params = {
Bucket: s3Params.Bucket,
Delete: extend({}, s3Params.Delete),
MFA: s3Params.MFA
}
let slices = chunkArray(params.Delete.Objects, MAX_DELETE_COUNT)
// let errorOccurred = false
let pend = new Pend()
ee.progressAmount = 0
ee.progressTotal = params.Delete.Objects.length
slices.forEach(uploadSlice)
pend.wait(function (err) {
if (err) {
ee.emit('error', err)
return
}
ee.emit('end')
})
return ee
function uploadSlice (slice) {
pend.go(function (cb) {
doWithRetry(tryDeletingObjects, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (err) {
cb(err)
} else {
ee.progressAmount += slice.length
ee.emit('progress')
ee.emit('data', data)
cb()
}
})
})
function tryDeletingObjects (cb) {
self.s3Pend.go(function (pendCb) {
params.Delete.Objects = slice
self.s3.deleteObjects(params, function (err, data) {
pendCb()
cb(err, data)
})
})
}
}
}
// display revisons
Client.prototype.displayRevisions = function () {
let self = this
let params = {
Bucket: self.s3.config.Bucket,
Prefix: 'index:',
MaxKeys: 10
}
let revisions = new RSVP.Promise((resolve, reject) => {
let s3revisionL = []
// console.log(s3, 's3');
return self.s3.listObjects(params, (err, data) => {
if (err) {
reject(err)
}
// needs to get files from s3
data.Contents.forEach((obj) => {
s3revisionL.push({revisionKey: obj.Key.replace(/.html/g, ''), revisionDate: obj.LastModified})
// console.log(obj.Key, obj.LastModified, 'this is s3')
})
resolve(s3revisionL)
})
})
revisions.then((revisions) => {
const table = new ScmTable(this, revisions)
table.display()
})
}
// activate revisions
// upload revisions files
Client.prototype.createRevision = function () {
generate().then((res) => {
let self = this
let newParams = {
Bucket: self.s3.config.Bucket,
CopySource: `${self.s3.config.Bucket}/index.html`,
ContentType: 'text/html',
ACL: 'public-read-write',
Metadata: {
'revision': res.revisionKey,
'updated': (new Date()).toDateString()
},
MetadataDirective: 'COPY',
Key: `index:${res.revisionKey}.html`
}
// console.log(newParams,'newParams');
const uploadFile = new Promise((resolve, reject) => {
self.s3.copyObject(newParams, (copyErr, copyData) => {
if (copyErr) {
reject(copyErr)
} else {
resolve(copyData)
}
})
})
uploadFile.then((rest) => {
console.log(`Revision created successfully `)
})
})
}
//create revision for service worker
Client.prototype.serviceWorker = function () {
generate().then((res) => {
let self = this
let newParams = {
Bucket: self.s3.config.Bucket,
CopySource: `${self.s3.config.Bucket}/service-worker.js`,
ContentType: 'application/javascript',
ACL: 'public-read-write',
Metadata: {
'revision': res.revisionKey,
'updated': (new Date()).toDateString()
},
MetadataDirective: 'COPY',
Key: `service-worker:${res.revisionKey}.js`
}
// console.log(newParams,'newParams');
const uploadFile = new Promise((resolve, reject) => {
self.s3.copyObject(newParams, (copyErr, copyData) => {
if (copyErr) {
reject(copyErr)
} else {
resolve(copyData)
}
})
})
uploadFile.then((rest) => {
console.log(`Revision for service worker created successfully `)
})
})
}
// activate revision
Client.prototype.activateServiceWorkerRevisions = function (activate) {
let self = this
let params = {
Bucket: self.s3.config.Bucket,
Prefix: `${activate}.js`,
MaxKeys: 1
}
if (!activate) return console.log('Please provide activation key')
let actKey = activate.replace(/service-worker:/g, '')
console.log(`Activating service-worker file of key ${actKey}`)
let revisions = new RSVP.Promise((resolve, reject) => {
return self.s3.listObjects(params, (err, data) => {
if (err) {
reject(err)
}
// needs to get files from s3
if (data.Contents.length) {
// let file = data.Contents[0]
let newParams = {
Bucket: self.s3.config.Bucket,
CopySource: `${self.s3.config.Bucket}/${activate}.js`,
ContentType: 'application/javascript',
ACL: 'public-read-write',
Metadata: {
'revision': actKey,
'updated': (new Date()).toDateString()
},
MetadataDirective: 'REPLACE',
Key: 'service-worker.js'
}
self.s3.copyObject(newParams, (copyErr, copyData) => {
if (copyErr) {
// console.log(copyErr)
reject(copyErr)
} else {
// console.log('Copied: ', params.Key)
resolve(copyData)
}
})
}
})
})
// now see the promise
revisions.then((revision) => {
console.log(`Revision activated successfully`)
// let revisionKey = revision.revisionKey.replace(/index:/g,'').replace(/.html:/g,'')
// if(revision == actKey){
// //@TODO save and activate
// }else{
// console.log(`cannot find file of key ${actKey}`)
// }
})
}
// activate revision
Client.prototype.activateRevisions = function (activate) {
let self = this
let params = {
Bucket: self.s3.config.Bucket,
Prefix: `${activate}.html`,
MaxKeys: 1
}
if (!activate) return console.log('Please provide activation key')
let actKey = activate.replace(/index:/g, '')
console.log(`Activating index file of key ${actKey}`)
let revisions = new RSVP.Promise((resolve, reject) => {
return self.s3.listObjects(params, (err, data) => {
if (err) {
reject(err)
}
// needs to get files from s3
if (data.Contents.length) {
// let file = data.Contents[0]
// console.log('====================================');
// console.log("data contains length");
// console.log('====================================');
let newParams = {
Bucket: self.s3.config.Bucket,
CopySource: `${self.s3.config.Bucket}/${activate}.html`,
ContentType: 'text/html',
ACL: 'public-read-write',
Metadata: {
'revision': actKey,
'updated': (new Date()).toDateString()
},
MetadataDirective: 'REPLACE',
Key: 'index.html'
}
self.s3.copyObject(newParams, (copyErr, copyData) => {
if (copyErr) {
// console.log(copyErr)
reject(copyErr)
} else {
// console.log('Copied: ', params.Key)
resolve(copyData)
}
})
}
})
})
// now see the promise
revisions.then((revision) => {
console.log(`Revision activated successfully`)
// let revisionKey = revision.revisionKey.replace(/index:/g,'').replace(/.html:/g,'')
// if(revision == actKey){
// //@TODO save and activate
// }else{
// console.log(`cannot find file of key ${actKey}`)
// }
})
}
Client.prototype.uploadFile = function (params) {
let self = this
let uploader = new EventEmitter()
uploader.progressMd5Amount = 0
uploader.progressAmount = 0
uploader.progressTotal = 0
uploader.abort = handleAbort
uploader.getPublicUrl = function () {
return getPublicUrl(s3Params.Bucket, s3Params.Key, self.s3.config.region, self.s3.config.endpoint)
}
uploader.getPublicUrlHttp = function () {
return getPublicUrlHttp(s3Params.Bucket, s3Params.Key, self.s3.config.endpoint)
}
let localFile = params.localFile
let localFileStat = null
let s3Params = extend({}, params.s3Params)
if (s3Params.ContentType === undefined) {
let defaultContentType = params.defaultContentType || 'application/octet-stream'
s3Params.ContentType = mime.lookup(localFile, defaultContentType)
}
let fatalError = false
let localFileSlicer = null
let parts = []
openFile()
return uploader
function handleError (err) {
if (localFileSlicer) {
localFileSlicer.unref()
localFileSlicer = null
}
if (fatalError) return
fatalError = true
uploader.emit('error', err)
}
function handleAbort () {
fatalError = true
}
function openFile () {
fs.open(localFile, 'r', function (err, fd) {
if (err) return handleError(err)
localFileSlicer = fdSlicer.createFromFd(fd, {autoClose: true})
localFileSlicer.on('error', handleError)
localFileSlicer.on('close', function () {
uploader.emit('fileClosed')
})
// keep an extra reference alive until we decide that we're completely
// done with the file
localFileSlicer.ref()
uploader.emit('fileOpened', localFileSlicer)
fs.fstat(fd, function (err, stat) {
if (err) return handleError(err)
localFileStat = stat
uploader.progressTotal = stat.size
startPuttingObject()
})
})
}
function startPuttingObject () {
if (localFileStat.size >= self.multipartUploadThreshold) {
let multipartUploadSize = self.multipartUploadSize
let partsRequiredCount = Math.ceil(localFileStat.size / multipartUploadSize)
if (partsRequiredCount > MAX_MULTIPART_COUNT) {
multipartUploadSize = smallestPartSizeFromFileSize(localFileStat.size)
}
if (multipartUploadSize > MAX_PUTOBJECT_SIZE) {
let err = new Error('File size exceeds maximum object size: ' + localFile)
err.retryable = false
handleError(err)
return
}
startMultipartUpload(multipartUploadSize)
} else {
doWithRetry(tryPuttingObject, self.s3RetryCount, self.s3RetryDelay, onPutObjectDone)
}
function onPutObjectDone (err, data) {
if (fatalError) return
if (err) return handleError(err)
if (localFileSlicer) {
localFileSlicer.unref()
localFileSlicer = null
}
uploader.emit('end', data)
}
}
function startMultipartUpload (multipartUploadSize) {
doWithRetry(tryCreateMultipartUpload, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (fatalError) return
if (err) return handleError(err)
uploader.emit('data', data)
s3Params = {
Bucket: s3Params.Bucket,
Key: encodeSpecialCharacters(s3Params.Key),
SSECustomerAlgorithm: s3Params.SSECustomerAlgorithm,
SSECustomerKey: s3Params.SSECustomerKey,
SSECustomerKeyMD5: s3Params.SSECustomerKeyMD5
}
queueAllParts(data.UploadId, multipartUploadSize)
})
}
function queueAllParts (uploadId, multipartUploadSize) {
let cursor = 0
let nextPartNumber = 1
let pend = new Pend()
while (cursor < localFileStat.size) {
let start = cursor
let end = cursor + multipartUploadSize
if (end > localFileStat.size) {
end = localFileStat.size
}
cursor = end
let part = {
ETag: null,
PartNumber: nextPartNumber++
}
parts.push(part)
pend.go(makeUploadPartFn(start, end, part, uploadId))
}
pend.wait(function (err) {
if (fatalError) return
if (err) return handleError(err)
completeMultipartUpload()
})
}
function makeUploadPartFn (start, end, part, uploadId) {
return function (cb) {
doWithRetry(tryUploadPart, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (fatalError) return
if (err) return handleError(err)
uploader.emit('part', data)
cb()
})
}
function tryUploadPart (cb) {
if (fatalError) return
self.s3Pend.go(function (pendCb) {
if (fatalError) {
pendCb()
return
}
let inStream = localFileSlicer.createReadStream({start: start, end: end})
let errorOccurred = false
inStream.on('error', function (err) {
if (fatalError || errorOccurred) return
handleError(err)
})
s3Params.ContentLength = end - start
s3Params.PartNumber = part.PartNumber
s3Params.UploadId = uploadId
let multipartETag = new MultipartETag({size: s3Params.ContentLength, count: 1})
let prevBytes = 0
let overallDelta = 0
let pend = new Pend()
let haveETag = pend.hold()
multipartETag.on('progress', function () {
if (fatalError || errorOccurred) return
let delta = multipartETag.bytes - prevBytes
prevBytes = multipartETag.bytes
uploader.progressAmount += delta
overallDelta += delta
uploader.emit('progress')
})
multipartETag.on('end', function () {
if (fatalError || errorOccurred) return
let delta = multipartETag.bytes - prevBytes
uploader.progressAmount += delta
uploader.progressTotal += (end - start) - multipartETag.bytes
uploader.emit('progress')
haveETag()
})
inStream.pipe(multipartETag)
s3Params.Body = multipartETag
self.s3.uploadPart(extend({}, s3Params), function (err, data) {
pendCb()
if (fatalError || errorOccurred) return
if (err) {
errorOccurred = true
uploader.progressAmount -= overallDelta
cb(err)
return
}
pend.wait(function () {
if (fatalError) return
if (!compareMultipartETag(data.ETag, multipartETag)) {
errorOccurred = true
uploader.progressAmount -= overallDelta
cb(new Error('ETag does not match MD5 checksum'))
return
}
part.ETag = data.ETag
cb(null, data)
})
})
})
}
}
function completeMultipartUpload () {
localFileSlicer.unref()
localFileSlicer = null
doWithRetry(tryCompleteMultipartUpload, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (fatalError) return
if (err) return handleError(err)
uploader.emit('end', data)
})
}
function tryCompleteMultipartUpload (cb) {
if (fatalError) return
self.s3Pend.go(function (pendCb) {
if (fatalError) {
pendCb()
return
}
s3Params = {
Bucket: s3Params.Bucket,
Key: s3Params.Key,
UploadId: s3Params.UploadId,
MultipartUpload: {
Parts: parts
}
}
self.s3.completeMultipartUpload(s3Params, function (err, data) {
pendCb()
if (fatalError) return
cb(err, data)
})
})
}
function tryCreateMultipartUpload (cb) {
if (fatalError) return
self.s3Pend.go(function (pendCb) {
if (fatalError) return pendCb()
self.s3.createMultipartUpload(s3Params, function (err, data) {
pendCb()
if (fatalError) return
cb(err, data)
})
})
}
function tryPuttingObject (cb) {
self.s3Pend.go(function (pendCb) {
if (fatalError) return pendCb()
let inStream = localFileSlicer.createReadStream()
inStream.on('error', handleError)
let pend = new Pend()
let multipartETag = new MultipartETag({size: localFileStat.size, count: 1})
pend.go(function (cb) {
multipartETag.on('end', function () {
if (fatalError) return
uploader.progressAmount = multipartETag.bytes
uploader.progressTotal = multipartETag.bytes
uploader.emit('progress')
localFileStat.size = multipartETag.bytes
localFileStat.multipartETag = multipartETag
cb()
})
})
multipartETag.on('progress', function () {
if (fatalError) return
uploader.progressAmount = multipartETag.bytes
uploader.emit('progress')
})
s3Params.ContentLength = localFileStat.size
uploader.progressAmount = 0
inStream.pipe(multipartETag)
s3Params.Body = multipartETag
self.s3.putObject(s3Params, function (err, data) {
pendCb()
if (fatalError) return
if (err) {
cb(err)
return
}
pend.wait(function () {
if (fatalError) return
if (!compareMultipartETag(data.ETag, localFileStat.multipartETag)) {
cb(new Error('ETag does not match MD5 checksum'))
return
}
cb(null, data)
})
})
})
}
}
Client.prototype.downloadFile = function (params) {
let self = this
let downloader = new EventEmitter()
let localFile = params.localFile
let s3Params = extend({}, params.s3Params)
let dirPath = path.dirname(localFile)
downloader.progressAmount = 0
mkdirp(dirPath, function (err) {
if (err) {
downloader.emit('error', err)
return
}
doWithRetry(doDownloadWithPend, self.s3RetryCount, self.s3RetryDelay, function (err) {
if (err) {
downloader.emit('error', err)
return
}
downloader.emit('end')
})
})
return downloader
function doDownloadWithPend (cb) {
self.s3Pend.go(function (pendCb) {
doTheDownload(function (err) {
pendCb()
cb(err)
})
})
}
function doTheDownload (cb) {
let request = self.s3.getObject(s3Params)
let errorOccurred = false
let hashCheckPend = new Pend()
request.on('httpHeaders', function (statusCode, headers, resp) {
if (statusCode >= 300) {
handleError(new Error('http status code ' + statusCode))
return
}
if (headers['content-length'] === undefined) {
let outStream = fs.createWriteStream(localFile)
outStream.on('error', handleError)
downloader.progressTotal = 0
downloader.progressAmount = -1
request.on('httpData', function (chunk) {
downloader.progressTotal += chunk.length
downloader.progressAmount += chunk.length
downloader.emit('progress')
outStream.write(chunk)
})
request.on('httpDone', function () {
if (errorOccurred) return
downloader.progressAmount += 1
downloader.emit('progress')
outStream.end()
cb()
})
} else {
let contentLength = parseInt(headers['content-length'], 10)
downloader.progressTotal = contentLength
downloader.progressAmount = 0
downloader.emit('progress')
downloader.emit('httpHeaders', statusCode, headers, resp)
let eTag = cleanETag(headers.etag)
let eTagCount = getETagCount(eTag)
let outStream = fs.createWriteStream(localFile)
let multipartETag = new MultipartETag({size: contentLength, count: eTagCount})
let httpStream = resp.httpResponse.createUnbufferedStream()
httpStream.on('error', handleError)
outStream.on('error', handleError)
hashCheckPend.go(function (cb) {
multipartETag.on('end', function () {
if (multipartETag.bytes !== contentLength) {
handleError(new Error('Downloaded size does not match Content-Length'))
return
}
if (eTagCount === 1 && !multipartETag.anyMatch(eTag)) {
handleError(new Error('ETag does not match MD5 checksum'))
return
}
cb()
})
})
multipartETag.on('progress', function () {
downloader.progressAmount = multipartETag.bytes
downloader.emit('progress')
})
outStream.on('close', function () {
if (errorOccurred) return
hashCheckPend.wait(cb)
})
httpStream.pipe(multipartETag)
httpStream.pipe(outStream)
multipartETag.resume()
}
})
request.send(handleError)
function handleError (err) {
if (!err) return
if (errorOccurred) return
errorOccurred = true
cb(err)
}
}
}
/* params:
* - recursive: false
* - s3Params:
* - Bucket: params.s3Params.Bucket,
* - Delimiter: null,
* - Marker: null,
* - MaxKeys: null,
* - Prefix: prefix,
*/
Client.prototype.listObjects = function (params) {
let self = this
let ee = new EventEmitter()
let s3Details = extend({}, params.s3Params)
let recursive = !!params.recursive
let abort = false
ee.progressAmount = 0
ee.objectsFound = 0
ee.dirsFound = 0
findAllS3Objects(s3Details.Marker, s3Details.Prefix, function (err, data) {
if (err) {
ee.emit('error', err)
return
}
ee.emit('end')
})
ee.abort = function () {
abort = true
}
return ee
function findAllS3Objects (marker, prefix, cb) {
if (abort) return
doWithRetry(listObjects, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (abort) return
if (err) return cb(err)
ee.progressAmount += 1
ee.objectsFound += data.Contents.length
ee.dirsFound += data.CommonPrefixes.length
ee.emit('progress')
ee.emit('data', data)
let pend = new Pend()
if (recursive) {
data.CommonPrefixes.forEach(recurse)
data.CommonPrefixes = []
}
if (data.IsTruncated) {
pend.go(findNext1000)
}
pend.wait(function (err) {
cb(err)
})
function findNext1000 (cb) {
let nextMarker = data.NextMarker || data.Contents[data.Contents.length - 1].Key
findAllS3Objects(nextMarker, prefix, cb)
}
function recurse (dirObj) {
let prefix = dirObj.Prefix
pend.go(function (cb) {
findAllS3Objects(null, prefix, cb)
})
}
})
function listObjects (cb) {
if (abort) return
self.s3Pend.go(function (pendCb) {
if (abort) {
pendCb()
return
}
s3Details.Marker = marker
s3Details.Prefix = prefix
self.s3.listObjects(s3Details, function (err, data) {
pendCb()
if (abort) return
cb(err, data)
})
})
}
}
}
/* params:
* - deleteRemoved - delete s3 objects with no corresponding local file. default false
* - localDir - path on local file system to sync
* - s3Params:
* - Bucket (required)
* - Key (required)
*/
Client.prototype.uploadDir = function (params) {
return syncDir(this, params, true)
}
Client.prototype.downloadDir = function (params) {
return syncDir(this, params, false)
}
Client.prototype.deleteDir = function (s3Params) {
let self = this
let ee = new EventEmitter()
let bucket = s3Params.Bucket
let mfa = s3Params.MFA
let listObjectsParams = {
recursive: true,
s3Params: {
Bucket: bucket,
Prefix: s3Params.Prefix
}
}
let finder = self.listObjects(listObjectsParams)
let pend = new Pend()
ee.progressAmount = 0
ee.progressTotal = 0
finder.on('error', function (err) {
ee.emit('error', err)
})
finder.on('data', function (objects) {
ee.progressTotal += objects.Contents.length
ee.emit('progress')
if (objects.Contents.length > 0) {
pend.go(deleteThem)
}
function deleteThem (cb) {
let params = {
Bucket: bucket,
Delete: {
Objects: objects.Contents.map(keyOnly),
Quiet: true
},
MFA: mfa
}
let deleter = self.deleteObjects(params)
deleter.on('error', function (err) {
finder.abort()
ee.emit('error', err)
})
deleter.on('end', function () {
ee.progressAmount += objects.Contents.length
ee.emit('progress')
cb()
})
}
})
finder.on('end', function () {
pend.wait(function () {
ee.emit('end')
})
})
return ee
}
Client.prototype.copyObject = function (_s3Params) {
let self = this
let ee = new EventEmitter()
let s3Params = extend({}, _s3Params)
delete s3Params.MFA
doWithRetry(doCopyWithPend, self.s3RetryCount, self.s3RetryDelay, function (err, data) {
if (err) {
ee.emit('error', err)
} else {
ee.emit('end', data)
}
})
function doCopyWithPend (cb) {
self.s3Pend.go(function (pendCb) {
doTheCopy(function (err, data) {
pendCb()
cb(err, data)
})
})
}
function doTheCopy (cb) {
self.s3.copyObject(s3Params, cb)
}
return ee
}
Client.prototype.moveObject = function (s3Params) {
let self = this
let ee = new EventEmitter()
let copier = self.copyObject(s3Params)
let copySource = s3Params.CopySource
let mfa = s3Params.MFA
copier.on('error', function (err) {
ee.emit('error', err)
})
copier.on('end', function (data) {
ee.emit('copySuccess', data)
let slashIndex = copySource.indexOf('/')
let sourceBucket = copySource.substring(0, slashIndex)
let sourceKey = copySource.substring(slashIndex + 1)
let deleteS3Params = {
Bucket: sourceBucket,
Delete: {
Objects: [
{
Key: sourceKey
}
],
Quiet: true
},
MFA: mfa
}
let deleter = self.deleteObjects(deleteS3Params)
deleter.on('error', function (err) {
ee.emit('error', err)
})
let deleteData
deleter.on('data', function (data) {
deleteData = data
})
deleter.on('end', function () {
ee.emit('end', deleteData)
})
})
return ee
}
Client.prototype.downloadBuffer = function (s3Params) {
let self = this
let downloader = new EventEmitter()
s3Params = extend({}, s3Params)
downloader.progressAmount = 0
doWithRetry(doDownloadWithPend, self.s3RetryCount, self.s3RetryDelay, function (err, buffer) {
if (err) {
downloader.emit('error', err)
return
}
downloader.emit('end', buffer)
})
return downloader
function doDownloadWithPend (cb) {
self.s3Pend.go(function (pendCb) {
doTheDownload(function (err, buffer) {
pendCb()
cb(err, buffer)
})
})
}
function doTheDownload (cb) {
let errorOccurred = false
let request = self.s3.getObject(s3Params)
let hashCheckPend = new Pend()
request.on('httpHeaders', function (statusCode, headers, resp) {
if (statusCode >= 300) {
handleError(new Error('http status code ' + statusCode))
return
}
let contentLength = parseInt(headers['content-length'], 10)
downloader.progressTotal = contentLength
downloader.progressAmount = 0
downloader.emit('progress')
downloader.emit('httpHeaders', statusCode, headers, resp)
let eTag = cleanETag(headers.etag)
let eTagCount = getETagCount(eTag)
let outStream = new StreamSink()
let multipartETag = new MultipartETag({size: contentLength, count: eTagCount})
let httpStream = resp.httpResponse.createUnbufferedStream()
httpStream.on('error', handleError)
outStream.on('error', handleError)
hashCheckPend.go(function (cb) {
multipartETag.on('end', function () {
if (multipartETag.bytes !== contentLength) {
handleError(new Error('Downloaded size does not match Content-Length'))
return
}
if (eTagCount === 1 && !multipartETag.anyMatch(eTag)) {
handleError(new Error('ETag does not match MD5 checksum'))
return
}
cb()
})
})
multipartETag.on('progress', function () {
downloader.progressAmount = multipartETag.bytes
downloader.emit('progress')
})
outStream.on('finish', function () {
if (errorOccurred) return
hashCheckPend.wait(function () {
cb(null, outStream.toBuffer())
})
})
httpStream.pipe(multipartETag)
httpStream.pipe(outStream)
multipartETag.resume()
})
request.send(handleError)
function handleError (err) {
if (!err) return
if (errorOccurred) return
errorOccurred = true
cb(err)
}
}
}
Client.prototype.downloadStream = function (s3Params) {
let self = this
let downloadStream = new PassThrough()
s3Params = extend({}, s3Params)
doDownloadWithPend(function (err) {
if (err) downloadStream.emit('error', err)
})
return downloadStream
function doDownloadWithPend (cb) {
self.s3Pend.go(function (pendCb) {
doTheDownload(function (err) {
pendCb()
cb(err)
})
})
}
function doTheDownload (cb) {
let errorOccurred = false
let request = self.s3.getObject(s3Params)
// let hashCheckPend = new Pend()
request.on('httpHeaders', function (statusCode, headers, resp) {
if (statusCode >= 300) {
handleError(new Error('http status code ' + statusCode))
return
}
downloadStream.emit('httpHeaders', statusCode, headers, resp)
let httpStream = resp.httpResponse.createUnbufferedStream()
httpStream.on('error', handleError)
downloadStream.on('finish', function () {
if (errorOccurred) return
cb()
})
httpStream.pipe(downloadStream)
})
request.send(handleError)
function handleError (err) {
if (!err) return
if (errorOccurred) return
errorOccurred = true
cb(err)
}
}
}
function syncDir (self, params, directionIsToS3) {
let ee = new EventEmitter()
let finditOpts = {
fs: fs,
followSymlinks: (params.followSymlinks == null) ? true : !!params.followSymlinks
}
let localDir = params.localDir
let deleteRemoved = params.deleteRemoved === true
let fatalError = false
let prefix = params.s3Params.Prefix ? ensureSlash(params.s3Params.Prefix) : ''
let bucket = params.s3Params.Bucket
let listObjectsParams = {
recursive: true,
s3Params: {
Bucket: bucket,
Marker: null,
MaxKeys: null,
Prefix: prefix
}
}
let getS3Params = params.getS3Params
let baseUpDownS3Params = extend({}, params.s3Params)
let upDownFileParams = {
localFile: null,
s3Params: baseUpDownS3Params,
defaultContentType: params.defaultContentType
}
delete upDownFileParams.s3Params.Prefix
ee.activeTransfers = 0
ee.progressAmount = 0
ee.progressTotal = 0
ee.progressMd5Amount = 0
ee.progressMd5Total = 0
ee.objectsFound = 0
ee.filesFound = 0
ee.deleteAmount = 0
ee.deleteTotal = 0
ee.doneFindingFiles = false
ee.doneFindingObjects = false
ee.doneMd5 = false
let allLocalFiles = []
let allS3Objects = []
let localFileCursor = 0
let s3ObjectCursor = 0
let objectsToDelete = []
findAllS3Objects()
startFindAllFiles()
return ee
function flushDeletes () {
if (objectsToDelete.length === 0) return
let thisObjectsToDelete = objectsToDelete
objectsToDelete = []
let params = {
Bucket: bucket,
Delete: {
Objects: thisObjectsToDelete,
Quiet: true
}
}
let deleter = self.deleteObjects(params)
deleter.on('error', handleError)
deleter.on('end', function () {
if (fatalError) return
ee.deleteAmount += thisObjectsToDelete.length
ee.emit('progress')
checkDoMoreWork()
})
}
function checkDoMoreWork () {
if (fatalError) return
let localFileStat = allLocalFiles[localFileCursor]
let s3Object = allS3Objects[s3ObjectCursor]
// need to wait for a file or object. checkDoMoreWork will get called
// again when that happens.
if (!localFileStat && !ee.doneMd5) return
if (!s3Object && !ee.doneFindingObjects) return
// need to wait until the md5 is done computing for the local file
if (localFileStat && !localFileStat.multipartETag) return
// localFileStat or s3Object could still be null - in that case we have
// reached the real end of the list.
// if they're both null, we've reached the true end
if (!localFileStat && !s3Object) {
// if we don't have any pending deletes or uploads, we're actually done
flushDeletes()
if (ee.deleteAmount >= ee.deleteTotal &&
ee.progressAmount >= ee.progressTotal &&
ee.activeTransfers === 0) {
ee.emit('end')
// prevent checkDoMoreWork from doing any more work
fatalError = true
}
// either way, there's nothing else to do in this method
return
}
// special case for directories when deleteRemoved is true and we're
// downloading a dir from S3. We don't add directories to the list
// unless this case is true, so we assert that fact here.
if (localFileStat && localFileStat.isDirectory()) {
assert.ok(!directionIsToS3)
assert.ok(deleteRemoved)
localFileCursor += 1
setImmediate(checkDoMoreWork)
if (!s3Object || s3Object.key.indexOf(localFileStat.s3Path) !== 0) {
deleteLocalDir()
}
return
}
if (directionIsToS3) {
if (!localFileStat) {
deleteS3Object()
} else if (!s3Object) {
uploadLocalFile()
} else if (localFileStat.s3Path < s3Object.key) {
uploadLocalFile()
} else if (localFileStat.s3Path > s3Object.key) {
deleteS3Object()
} else if (!compareMultipartETag(s3Object.ETag, localFileStat.multipartETag)) {
// both file cursor and s3 cursor should increment
s3ObjectCursor += 1
uploadLocalFile()
} else {
skipThisOne()
}
} else {
if (!localFileStat) {
downloadS3Object()
} else if (!s3Object) {
deleteLocalFile()
} else if (localFileStat.s3Path < s3Object.key) {
deleteLocalFile()
} else if (localFileStat.s3Path > s3Object.key) {
downloadS3Object()
} else if (!compareMultipartETag(s3Object.ETag, localFileStat.multipartETag)) {
// both file cursor and s3 cursor should increment
localFileCursor += 1
downloadS3Object()
} else {
skipThisOne()
}
}
function deleteLocalDir () {
let fullPath = path.join(localDir, localFileStat.path)
ee.deleteTotal += 1
rimraf(fullPath, function (err) {
if (fatalError) return
if (err && err.code !== 'ENOENT') return handleError(err)
ee.deleteAmount += 1
ee.emit('progress')
checkDoMoreWork()
})
}
function deleteLocalFile () {
localFileCursor += 1
setImmediate(checkDoMoreWork)
if (!deleteRemoved) return
ee.deleteTotal += 1
let fullPath = path.join(localDir, localFileStat.path)
fs.unlink(fullPath, function (err) {
if (fatalError) return
if (err && err.code !== 'ENOENT') return handleError(err)
ee.deleteAmount += 1
ee.emit('progress')
checkDoMoreWork()
})
}
function downloadS3Object () {
s3ObjectCursor += 1
setImmediate(checkDoMoreWork)
let fullPath = path.join(localDir, toNativeSep(s3Object.key))
if (getS3Params) {
getS3Params(fullPath, s3Object, haveS3Params)
} else {
startDownload()
}
function haveS3Params (err, s3Params) {
if (fatalError) return
if (err) return handleError(err)
if (!s3Params) {
// user has decided to skip this file
return
}
upDownFileParams.s3Params = extend(extend({}, baseUpDownS3Params), s3Params)
startDownload()
}
function startDownload () {
ee.progressTotal += s3Object.Size
let fullKey = s3Object.Key
upDownFileParams.s3Params.Key = fullKey
upDownFileParams.localFile = fullPath
let downloader = self.downloadFile(upDownFileParams)
let prevAmountDone = 0
ee.activeTransfers++
ee.emit('fileDownloadStart', fullPath, fullKey)
downloader.on('error', handleError)
downloader.on('progress', function () {
if (fatalError) return
let delta = downloader.progressAmount - prevAmountDone
prevAmountDone = downloader.progressAmount
ee.progressAmount += delta
ee.emit('progress')
})
downloader.on('end', function () {
ee.activeTransfers--
ee.emit('fileDownloadEnd', fullPath, fullKey)
ee.emit('progress')
checkDoMoreWork()
})
}
}
function skipThisOne () {
s3ObjectCursor += 1
localFileCursor += 1
setImmediate(checkDoMoreWork)
}
// create a revision over here
// console.log(localFileStat.path, 'localFileStat');
// if(localFileStat )
// createRevison()
// function createRevison() {
// //use generatior
// generate().then((res)=> {
// fs.copyFile(path.join(__dirname, '../../build/index.html'), path.join(__dirname, '../../build/index:'+res.revisionKey+'.html'), (err) => {
// if (err) throw err;
// console.log(`${res.revisionKey}.html was copied to destination`);
// })
// })
// }
function uploadLocalFile () {
localFileCursor += 1
setImmediate(checkDoMoreWork)
let fullPath = path.join(localDir, localFileStat.path)
// ***
if (getS3Params) {
getS3Params(fullPath, localFileStat, haveS3Params)
} else {
upDownFileParams.s3Params = baseUpDownS3Params
startUpload()
}
function haveS3Params (err, s3Params) {
if (fatalError) return
if (err) return handleError(err)
if (!s3Params) {
// user has decided to skip this file
return
}
upDownFileParams.s3Params = extend(extend({}, baseUpDownS3Params), s3Params)
startUpload()
}
function startUpload () {
ee.progressTotal += localFileStat.size
let fullKey = prefix + localFileStat.s3Path
upDownFileParams.s3Params.Key = fullKey
upDownFileParams.localFile = fullPath
let uploader = self.uploadFile(upDownFileParams)
let prevAmountDone = 0
let prevAmountTotal = localFileStat.size
ee.activeTransfers++
ee.emit('fileUploadStart', fullPath, fullKey)
uploader.on('error', handleError)
uploader.on('progress', function () {
if (fatalError) return
let amountDelta = uploader.progressAmount - prevAmountDone
prevAmountDone = uploader.progressAmount
ee.progressAmount += amountDelta
let totalDelta = uploader.progressTotal - prevAmountTotal
prevAmountTotal = uploader.progressTotal
ee.progressTotal += totalDelta
ee.emit('progress')
})
uploader.on('end', function () {
ee.activeTransfers--
ee.emit('fileUploadEnd', fullPath, fullKey)
ee.emit('progress')
checkDoMoreWork()
})
}
}
function deleteS3Object () {
s3ObjectCursor += 1
setImmediate(checkDoMoreWork)
if (!deleteRemoved) return
objectsToDelete.push({Key: s3Object.Key})
ee.deleteTotal += 1
ee.emit('progress')
assert.ok(objectsToDelete.length <= 1000)
if (objectsToDelete.length === 1000) {
flushDeletes()
}
}
}
function handleError (err) {
if (fatalError) return
fatalError = true
ee.emit('error', err)
}
function findAllS3Objects () {
let finder = self.listObjects(listObjectsParams)
finder.on('error', handleError)
finder.on('data', function (data) {
if (fatalError) return
ee.objectsFound += data.Contents.length
ee.emit('progress')
data.Contents.forEach(function (object) {
object.key = object.Key.substring(prefix.length)
allS3Objects.push(object)
})
checkDoMoreWork()
})
finder.on('end', function () {
if (fatalError) return
ee.doneFindingObjects = true
ee.emit('progress')
checkDoMoreWork()
})
}
function startFindAllFiles () {
findAllFiles(function (err) {
if (fatalError) return
if (err) return handleError(err)
ee.doneFindingFiles = true
ee.emit('progress')
allLocalFiles.sort(function (a, b) {
if (a.s3Path < b.s3Path) {
return -1
} else if (a.s3Path > b.s3Path) {
return 1
} else {
return 0
}
})
startComputingMd5Sums()
})
}
function startComputingMd5Sums () {
let index = 0
computeOne()
function computeOne () {
if (fatalError) return
let localFileStat = allLocalFiles[index]
if (!localFileStat) {
ee.doneMd5 = true
ee.emit('progress')
checkDoMoreWork()
return
}
if (localFileStat.multipartETag) {
index += 1
setImmediate(computeOne)
return
}
let fullPath = path.join(localDir, localFileStat.path)
let inStream = fs.createReadStream(fullPath)
let multipartETag = new MultipartETag()
inStream.on('error', handleError)
let prevBytes = 0
multipartETag.on('progress', function () {
let delta = multipartETag.bytes - prevBytes
prevBytes = multipartETag.bytes
ee.progressMd5Amount += delta
})
multipartETag.on('end', function () {
if (fatalError) return
localFileStat.multipartETag = multipartETag
checkDoMoreWork()
ee.emit('progress')
index += 1
computeOne()
})
inStream.pipe(multipartETag)
multipartETag.resume()
}
}
function findAllFiles (cb) {
let dirWithSlash = ensureSep(localDir)
let walker = findit(dirWithSlash, finditOpts)
walker.on('error', function (err) {
walker.stop()
// when uploading, we don't want to delete based on a nonexistent source directory
// but when downloading, the destination directory does not have to exist.
if (!directionIsToS3 && err.path === dirWithSlash && err.code === 'ENOENT') {
cb()
} else {
cb(err)
}
})
walker.on('directory', function (dir, stat, stop, linkPath) {
if (fatalError) return walker.stop()
// we only need to save directories when deleteRemoved is true
// and we're syncing to disk from s3
if (!deleteRemoved || directionIsToS3) return
let relPath = path.relative(localDir, linkPath || dir)
if (relPath === '') return
stat.path = relPath
stat.s3Path = toUnixSep(relPath) + '/'
stat.multipartETag = new MultipartETag()
allLocalFiles.push(stat)
})
walker.on('file', function (file, stat, linkPath) {
if (fatalError) return walker.stop()
let relPath = path.relative(localDir, linkPath || file)
stat.path = relPath
stat.s3Path = toUnixSep(relPath)
ee.filesFound += 1
ee.progressMd5Total += stat.size
ee.emit('progress')
allLocalFiles.push(stat)
})
walker.on('end', function () {
cb()
})
}
}
function ensureChar (str, c) {
return (str[str.length - 1] === c) ? str : (str + c)
}
function ensureSep (dir) {
return ensureChar(dir, path.sep)
}
function ensureSlash (dir) {
return ensureChar(dir, '/')
}
function doWithRetry (fn, tryCount, delay, cb) {
let tryIndex = 0
tryOnce()
function tryOnce () {
fn(function (err, result) {
if (err) {
if (err.retryable === false) {
cb(err)
} else {
tryIndex += 1
if (tryIndex >= tryCount) {
cb(err)
} else {
setTimeout(tryOnce, delay)
}
}
} else {
cb(null, result)
}
})
}
}
function extend (target, source) {
for (let propName in source) {
target[propName] = source[propName]
}
return target
}
function chunkArray (array, maxLength) {
let slices = [array]
while (slices[slices.length - 1].length > maxLength) {
slices.push(slices[slices.length - 1].splice(maxLength))
}
return slices
}
function cleanETag (eTag) {
return eTag ? eTag.replace(/^\s*'?\s*"?\s*(.*?)\s*"?\s*'?\s*$/, '$1') : ''
}
function compareMultipartETag (eTag, multipartETag) {
return multipartETag.anyMatch(cleanETag(eTag))
}
function getETagCount (eTag) {
let match = (eTag || '').match(/[a-fA-F0-9]{32}-(\d+)$/)
return match ? parseInt(match[1], 10) : 1
}
function keyOnly (item) {
return {
Key: item.Key,
VersionId: item.VersionId
}
}
function encodeSpecialCharacters (filename) {
// Note: these characters are valid in URIs, but S3 does not like them for
// some reason.
return encodeURI(filename).replace(/[!'()* ]/g, function (char) {
return '%' + char.charCodeAt(0).toString(16)
})
}
function getPublicUrl (bucket, key, bucketLocation, endpoint) {
let nonStandardBucketLocation = (bucketLocation && bucketLocation !== 'us-east-1')
let hostnamePrefix = nonStandardBucketLocation ? ('s3-' + bucketLocation) : 's3'
let parts = {
protocol: 'https:',
hostname: hostnamePrefix + '.' + (endpoint || 'amazonaws.com'),
pathname: '/' + bucket + '/' + encodeSpecialCharacters(key)
}
return url.format(parts)
}
function getPublicUrlHttp (bucket, key, endpoint) {
let parts = {
protocol: 'http:',
hostname: bucket + '.' + (endpoint || 's3.amazonaws.com'),
pathname: '/' + encodeSpecialCharacters(key)
}
return url.format(parts)
}
function toUnixSep (str) {
return str.replace(TO_UNIX_RE, '/')
}
function toNativeSep (str) {
return str.replace(/\//g, path.sep)
}
function quotemeta (str) {
return String(str).replace(/(\W)/g, '\\$1')
}
function smallestPartSizeFromFileSize (fileSize) {
let partSize = Math.ceil(fileSize / MAX_MULTIPART_COUNT)
return (partSize < MIN_MULTIPART_SIZE) ? MIN_MULTIPART_SIZE : partSize
}