2015-08-24 11:13:21 -07:00
'use strict' ;
exports = module . exports = {
2017-09-17 18:50:29 -07:00
upload : upload ,
download : download ,
2017-09-23 14:27:35 -07:00
downloadDir : downloadDir ,
2017-09-17 18:50:29 -07:00
copy : copy ,
2017-09-23 11:09:36 -07:00
remove : remove ,
2017-09-27 17:34:49 -07:00
removeDir : removeDir ,
2016-10-11 11:36:25 +02:00
2017-01-04 16:22:58 -08:00
backupDone : backupDone ,
2017-04-18 19:15:56 +02:00
testConfig : testConfig ,
// Used to mock AWS
_mockInject : mockInject ,
_mockRestore : mockRestore
2015-08-24 11:13:21 -07:00
} ;
2017-04-20 15:35:52 +02:00
var assert = require ( 'assert' ) ,
2017-09-23 11:09:36 -07:00
async = require ( 'async' ) ,
2016-09-19 15:03:38 +02:00
AWS = require ( 'aws-sdk' ) ,
2017-04-18 15:33:06 +02:00
BackupsError = require ( '../backups.js' ) . BackupsError ,
2017-10-05 10:01:09 -07:00
chunk = require ( 'lodash.chunk' ) ,
2017-09-26 12:28:33 -07:00
config = require ( '../config.js' ) ,
2017-04-18 15:33:06 +02:00
debug = require ( 'debug' ) ( 'box:storage/s3' ) ,
2017-10-04 11:00:30 -07:00
EventEmitter = require ( 'events' ) ,
2017-09-23 14:27:35 -07:00
fs = require ( 'fs' ) ,
2017-10-05 10:01:09 -07:00
https = require ( 'https' ) ,
2017-09-23 14:27:35 -07:00
mkdirp = require ( 'mkdirp' ) ,
2017-04-21 15:28:25 -07:00
PassThrough = require ( 'stream' ) . PassThrough ,
2017-04-18 15:33:06 +02:00
path = require ( 'path' ) ,
2017-09-26 12:28:33 -07:00
S3BlockReadStream = require ( 's3-block-read-stream' ) ,
superagent = require ( 'superagent' ) ;
2015-08-24 11:13:21 -07:00
2017-04-18 19:15:56 +02:00
// test only
var originalAWS ;
function mockInject ( mock ) {
originalAWS = AWS ;
AWS = mock ;
}
function mockRestore ( ) {
AWS = originalAWS ;
}
2017-10-04 11:49:38 -07:00
var gCachedCaasCredentials = { issueDate : null , credentials : null } ;
2017-10-10 23:42:24 -07:00
function getCaasConfig ( apiConfig , callback ) {
2017-09-26 12:28:33 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
assert ( apiConfig . token ) ;
2017-10-04 11:49:38 -07:00
if ( ( new Date ( ) - gCachedCaasCredentials . issueDate ) <= ( 1.75 * 60 * 60 * 1000 ) ) { // caas gives tokens with 2 hour limit
2017-10-04 19:08:07 -07:00
return callback ( null , gCachedCaasCredentials . credentials ) ;
2017-10-04 11:49:38 -07:00
}
2017-10-04 19:08:07 -07:00
debug ( 'getCaasCredentials: getting new credentials' ) ;
2017-11-28 16:52:52 -08:00
var url = config . apiServerOrigin ( ) + '/api/v1/boxes/' + apiConfig . fqdn + '/awscredentials' ;
2017-09-26 12:28:33 -07:00
superagent . post ( url ) . query ( { token : apiConfig . token } ) . timeout ( 30 * 1000 ) . end ( function ( error , result ) {
if ( error && ! error . response ) return callback ( error ) ;
if ( result . statusCode !== 201 ) return callback ( new Error ( result . text ) ) ;
if ( ! result . body || ! result . body . credentials ) return callback ( new Error ( 'Unexpected response: ' + JSON . stringify ( result . headers ) ) ) ;
var credentials = {
signatureVersion : 'v4' ,
accessKeyId : result . body . credentials . AccessKeyId ,
secretAccessKey : result . body . credentials . SecretAccessKey ,
sessionToken : result . body . credentials . SessionToken ,
2017-10-10 23:42:24 -07:00
region : apiConfig . region || 'us-east-1' ,
2017-10-11 13:57:05 -07:00
maxRetries : 5 ,
retryDelayOptions : {
2018-02-22 12:27:45 -08:00
customBackoff : ( ) => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
2018-02-22 12:19:23 -08:00
} ,
httpOptions : {
connectTimeout : 10000 // https://github.com/aws/aws-sdk-js/pull/1446
2017-10-11 13:57:05 -07:00
}
2017-09-26 12:28:33 -07:00
} ;
if ( apiConfig . endpoint ) credentials . endpoint = new AWS . Endpoint ( apiConfig . endpoint ) ;
2017-10-04 11:49:38 -07:00
gCachedCaasCredentials = {
issueDate : new Date ( ) ,
credentials : credentials
} ;
2017-09-26 12:28:33 -07:00
callback ( null , credentials ) ;
} ) ;
}
2017-10-10 23:42:24 -07:00
function getS3Config ( apiConfig , callback ) {
2016-03-31 09:48:01 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2015-08-24 11:13:21 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-10 23:42:24 -07:00
if ( apiConfig . provider === 'caas' ) return getCaasConfig ( apiConfig , callback ) ;
2017-09-26 12:28:33 -07:00
2015-11-06 18:22:29 -08:00
var credentials = {
2017-04-20 19:56:06 -07:00
signatureVersion : apiConfig . signatureVersion || 'v4' ,
2017-10-09 18:46:14 -07:00
s3ForcePathStyle : true , // Force use path-style url (http://endpoint/bucket/path) instead of host-style (http://bucket.endpoint/path)
2016-03-31 09:48:01 -07:00
accessKeyId : apiConfig . accessKeyId ,
secretAccessKey : apiConfig . secretAccessKey ,
2017-10-10 23:42:24 -07:00
region : apiConfig . region || 'us-east-1' ,
2017-10-11 13:57:05 -07:00
maxRetries : 5 ,
retryDelayOptions : {
2018-02-22 12:27:45 -08:00
customBackoff : ( ) => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
2018-02-22 12:19:23 -08:00
} ,
httpOptions : {
connectTimeout : 10000 // https://github.com/aws/aws-sdk-js/pull/1446
2017-10-11 13:57:05 -07:00
}
2015-11-06 18:22:29 -08:00
} ;
2015-09-09 11:43:50 -07:00
2016-12-07 10:47:06 +01:00
if ( apiConfig . endpoint ) credentials . endpoint = apiConfig . endpoint ;
2015-09-09 11:43:50 -07:00
2017-10-26 16:20:34 -07:00
if ( apiConfig . acceptSelfSignedCerts === true && credentials . endpoint && credentials . endpoint . startsWith ( 'https://' ) ) {
2018-02-22 12:19:23 -08:00
credentials . httpOptions . agent = new https . Agent ( { rejectUnauthorized : false } ) ;
2017-10-05 10:01:09 -07:00
}
2015-11-06 18:22:29 -08:00
callback ( null , credentials ) ;
2015-08-24 11:13:21 -07:00
}
2015-08-25 10:01:04 -07:00
2017-04-18 15:33:06 +02:00
// storage api
2017-09-20 09:57:16 -07:00
function upload ( apiConfig , backupFilePath , sourceStream , callback ) {
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2017-09-20 09:57:16 -07:00
assert . strictEqual ( typeof sourceStream , 'object' ) ;
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-10 23:42:24 -07:00
getS3Config ( apiConfig , function ( error , credentials ) {
2015-08-25 10:01:04 -07:00
if ( error ) return callback ( error ) ;
var params = {
2016-04-04 11:44:24 -07:00
Bucket : apiConfig . bucket ,
2017-04-18 15:33:06 +02:00
Key : backupFilePath ,
2017-09-20 09:57:16 -07:00
Body : sourceStream
2015-08-25 10:01:04 -07:00
} ;
2017-04-18 15:33:06 +02:00
var s3 = new AWS . S3 ( credentials ) ;
2017-10-10 10:47:41 -07:00
2017-04-27 11:40:18 -07:00
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
2017-10-10 10:47:41 -07:00
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
2018-02-27 19:16:03 -08:00
s3 . upload ( params , { partSize : 10 * 1024 * 1024 , queueSize : 1 } , function ( error ) {
2018-02-22 11:06:28 -08:00
if ( error ) {
2018-03-20 16:41:32 -07:00
debug ( 'Error uploading [%s]: s3 upload error.' , backupFilePath , error ) ;
2018-02-22 11:06:28 -08:00
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` Error uploading ${ backupFilePath } . Message: ${ error . message } HTTP Code: ${ error . code } ` ) ) ;
}
callback ( null ) ;
} ) ;
2015-08-25 10:01:04 -07:00
} ) ;
}
2015-08-26 16:14:51 -07:00
2017-09-20 09:57:16 -07:00
function download ( apiConfig , backupFilePath , callback ) {
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-10 23:42:24 -07:00
getS3Config ( apiConfig , function ( error , credentials ) {
2016-09-19 15:03:38 +02:00
if ( error ) return callback ( error ) ;
2017-04-21 15:28:25 -07:00
var params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath
} ;
2017-04-18 15:33:06 +02:00
2017-04-21 15:28:25 -07:00
var s3 = new AWS . S3 ( credentials ) ;
2017-04-18 16:44:49 +02:00
2017-09-20 09:57:16 -07:00
var ps = new PassThrough ( ) ;
2017-10-04 12:32:12 -07:00
var multipartDownload = new S3BlockReadStream ( s3 , params , { blockSize : 64 * 1024 * 1024 /*, logCallback: debug */ } ) ;
2017-04-21 15:06:54 -07:00
2017-04-27 11:50:16 -07:00
multipartDownload . on ( 'error' , function ( error ) {
2017-09-20 09:57:16 -07:00
if ( error . code === 'NoSuchKey' || error . code === 'ENOENT' ) {
ps . emit ( 'error' , new BackupsError ( BackupsError . NOT _FOUND ) ) ;
} else {
2017-11-28 10:28:17 -08:00
debug ( ` download: ${ apiConfig . bucket } : ${ backupFilePath } s3 stream error. ` , error ) ;
2017-11-22 20:26:22 -08:00
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message || error . code ) ) ; // DO sets 'code'
2017-09-20 09:57:16 -07:00
}
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-21 15:28:25 -07:00
2017-09-20 09:57:16 -07:00
multipartDownload . pipe ( ps ) ;
callback ( null , ps ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2016-09-16 18:14:36 +02:00
}
2018-02-22 12:14:13 -08:00
function listDir ( apiConfig , dir , iteratorCallback , callback ) {
2017-10-10 23:42:24 -07:00
getS3Config ( apiConfig , function ( error , credentials ) {
2017-09-23 14:27:35 -07:00
if ( error ) return callback ( error ) ;
var s3 = new AWS . S3 ( credentials ) ;
var listParams = {
Bucket : apiConfig . bucket ,
2018-02-22 12:14:13 -08:00
Prefix : dir
2017-09-23 14:27:35 -07:00
} ;
async . forever ( function listAndDownload ( foreverCallback ) {
2017-10-05 12:02:12 -07:00
s3 . listObjects ( listParams , function ( error , listData ) {
2017-09-23 14:27:35 -07:00
if ( error ) {
debug ( 'remove: Failed to list %s. Not fatal.' , error ) ;
return foreverCallback ( error ) ;
}
2017-10-29 12:30:22 -07:00
if ( listData . Contents . length === 0 ) return foreverCallback ( new Error ( 'Done' ) ) ;
2017-09-27 21:46:24 -07:00
2017-10-29 12:30:22 -07:00
iteratorCallback ( s3 , listData . Contents , function ( error ) {
2017-09-23 14:27:35 -07:00
if ( error ) return foreverCallback ( error ) ;
2017-09-28 10:36:56 -07:00
if ( ! listData . IsTruncated ) return foreverCallback ( new Error ( 'Done' ) ) ;
2017-09-27 21:46:24 -07:00
2017-10-05 12:02:12 -07:00
listParams . Marker = listData . Contents [ listData . Contents . length - 1 ] . Key ; // NextMarker is returned only with delimiter
2017-09-23 14:27:35 -07:00
2017-09-28 10:36:56 -07:00
foreverCallback ( ) ;
2017-09-23 14:27:35 -07:00
} ) ;
} ) ;
} , function ( error ) {
2017-10-02 20:08:00 -07:00
if ( error . message === 'Done' ) return callback ( null ) ;
2017-09-27 21:46:24 -07:00
2017-09-23 14:27:35 -07:00
callback ( error ) ;
} ) ;
} ) ;
}
2017-10-10 20:23:04 -07:00
function downloadDir ( apiConfig , backupFilePath , destDir ) {
2017-09-27 21:46:24 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
assert . strictEqual ( typeof destDir , 'string' ) ;
2017-10-10 20:23:04 -07:00
var events = new EventEmitter ( ) ;
var total = 0 ;
2017-10-11 13:57:05 -07:00
function downloadFile ( s3 , content , iteratorCallback ) {
2017-09-27 21:46:24 -07:00
var relativePath = path . relative ( backupFilePath , content . Key ) ;
2017-10-01 17:35:44 -07:00
2017-10-10 20:23:04 -07:00
events . emit ( 'progress' , ` Downloading ${ relativePath } ` ) ;
2017-09-27 21:46:24 -07:00
mkdirp ( path . dirname ( path . join ( destDir , relativePath ) ) , function ( error ) {
if ( error ) return iteratorCallback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-28 14:26:39 -07:00
download ( apiConfig , content . Key , function ( error , sourceStream ) {
if ( error ) return iteratorCallback ( error ) ;
var destStream = fs . createWriteStream ( path . join ( destDir , relativePath ) ) ;
destStream . on ( 'open' , function ( ) {
sourceStream . pipe ( destStream ) ;
} ) ;
destStream . on ( 'error' , function ( error ) {
return iteratorCallback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
destStream . on ( 'finish' , iteratorCallback ) ;
2017-09-27 21:46:24 -07:00
} ) ;
} ) ;
2017-10-11 13:57:05 -07:00
}
2017-10-29 12:30:22 -07:00
const concurrency = 10 ;
2017-10-11 13:57:05 -07:00
2017-10-29 12:30:22 -07:00
listDir ( apiConfig , backupFilePath , function ( s3 , objects , done ) {
2017-10-11 13:57:05 -07:00
total += objects . length ;
async . eachLimit ( objects , concurrency , downloadFile . bind ( null , s3 ) , done ) ;
2017-10-10 20:23:04 -07:00
} , function ( error ) {
events . emit ( 'progress' , ` Downloaded ${ total } files ` ) ;
events . emit ( 'done' , error ) ;
} ) ;
return events ;
2017-09-27 21:46:24 -07:00
}
2018-02-03 22:00:33 -08:00
// https://github.com/aws/aws-sdk-js/blob/2b6bcbdec1f274fe931640c1b61ece999aae7a19/lib/util.js#L41
// https://github.com/GeorgePhillips/node-s3-url-encode/blob/master/index.js
// See aws-sdk-js/issues/1302
function encodeCopySource ( bucket , path ) {
2018-02-04 00:15:41 -08:00
var output = encodeURI ( path ) ;
2018-02-03 22:00:33 -08:00
// AWS percent-encodes some extra non-standard characters in a URI
output = output . replace ( /[+!"#$@&'()*+,:;=?@]/g , function ( ch ) {
return '%' + ch . charCodeAt ( 0 ) . toString ( 16 ) . toUpperCase ( ) ;
} ) ;
// the slash at the beginning is optional
return ` / ${ bucket } / ${ output } ` ;
}
2017-10-04 11:00:30 -07:00
function copy ( apiConfig , oldFilePath , newFilePath ) {
2016-03-31 09:48:01 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof oldFilePath , 'string' ) ;
assert . strictEqual ( typeof newFilePath , 'string' ) ;
2017-10-04 11:00:30 -07:00
2017-10-11 13:57:05 -07:00
var events = new EventEmitter ( ) , retryCount = 0 ;
2015-09-21 14:02:00 -07:00
2017-10-11 13:57:05 -07:00
function copyFile ( s3 , content , iteratorCallback ) {
2017-09-27 21:46:24 -07:00
var relativePath = path . relative ( oldFilePath , content . Key ) ;
2015-09-21 14:02:00 -07:00
2017-10-04 16:54:56 +02:00
function done ( error ) {
2017-10-03 15:40:01 -07:00
if ( error && error . code === 'NoSuchKey' ) return iteratorCallback ( new BackupsError ( BackupsError . NOT _FOUND , ` Old backup not found: ${ content . Key } ` ) ) ;
2017-09-27 21:46:24 -07:00
if ( error ) {
2017-10-11 13:57:05 -07:00
debug ( 'copy: s3 copy error when copying %s %s' , content . Key , error ) ;
return iteratorCallback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` Error copying ${ content . Key } : ${ error . message } ${ error . code } ` ) ) ;
2017-09-27 21:46:24 -07:00
}
2017-09-22 14:40:37 -07:00
2017-10-04 11:00:30 -07:00
iteratorCallback ( null ) ;
2017-10-04 16:54:56 +02:00
}
var copyParams = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath )
} ;
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
2018-02-22 10:31:56 -08:00
// Exoscale takes too long to copy 5GB
const largeFileLimit = apiConfig . provider === 'exoscale-sos' ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024 ;
if ( content . Size < largeFileLimit ) {
2018-02-22 12:41:18 -08:00
events . emit ( 'progress' , ` Copying ${ relativePath || oldFilePath } ` ) ;
2017-10-04 11:00:30 -07:00
2018-02-03 22:00:33 -08:00
copyParams . CopySource = encodeCopySource ( apiConfig . bucket , content . Key ) ;
2017-10-11 13:57:05 -07:00
s3 . copyObject ( copyParams , done ) . on ( 'retry' , function ( response ) {
++ retryCount ;
2018-02-22 12:41:18 -08:00
events . emit ( 'progress' , ` Retrying ( ${ response . retryCount + 1 } ) copy of ${ relativePath || oldFilePath } . Status code: ${ response . httpResponse . statusCode } ` ) ;
2017-10-11 13:57:05 -07:00
} ) ;
return ;
2017-10-04 16:54:56 +02:00
}
2018-02-22 12:41:18 -08:00
events . emit ( 'progress' , ` Copying (multipart) ${ relativePath || oldFilePath } ` ) ;
2017-10-04 11:00:30 -07:00
2017-10-04 16:54:56 +02:00
s3 . createMultipartUpload ( copyParams , function ( error , result ) {
if ( error ) return done ( error ) ;
2018-02-22 10:31:56 -08:00
// Exoscale (96M) was suggested by exoscale. 1GB - rather random size for others
const chunkSize = apiConfig . provider === 'exoscale-sos' ? 96 * 1024 * 1024 : 1024 * 1024 * 1024 ;
2017-10-04 16:54:56 +02:00
var uploadId = result . UploadId ;
var uploadedParts = [ ] ;
var partNumber = 1 ;
var startBytes = 0 ;
var endBytes = 0 ;
var size = content . Size - 1 ;
function copyNextChunk ( ) {
2018-02-22 10:31:56 -08:00
endBytes = startBytes + chunkSize ;
2017-10-04 16:54:56 +02:00
if ( endBytes > size ) endBytes = size ;
var params = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath ) ,
2018-02-03 22:00:33 -08:00
CopySource : encodeCopySource ( apiConfig . bucket , content . Key ) , // See aws-sdk-js/issues/1302
2017-10-04 16:54:56 +02:00
CopySourceRange : 'bytes=' + startBytes + '-' + endBytes ,
PartNumber : partNumber ,
UploadId : uploadId
} ;
s3 . uploadPartCopy ( params , function ( error , result ) {
if ( error ) return done ( error ) ;
uploadedParts . push ( { ETag : result . CopyPartResult . ETag , PartNumber : partNumber } ) ;
if ( endBytes < size ) {
startBytes = endBytes + 1 ;
partNumber ++ ;
return copyNextChunk ( ) ;
}
var params = {
Bucket : apiConfig . bucket ,
Key : path . join ( newFilePath , relativePath ) ,
MultipartUpload : { Parts : uploadedParts } ,
UploadId : uploadId
} ;
s3 . completeMultipartUpload ( params , done ) ;
2017-10-11 13:57:05 -07:00
} ) . on ( 'retry' , function ( response ) {
++ retryCount ;
2018-02-22 12:41:18 -08:00
events . emit ( 'progress' , ` Retrying ( ${ response . retryCount + 1 } ) multipart copy of ${ relativePath || oldFilePath } . Status code: ${ response . httpResponse . statusCode } ` ) ;
2017-10-04 16:54:56 +02:00
} ) ;
}
copyNextChunk ( ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-10-11 13:57:05 -07:00
}
var total = 0 , concurrency = 4 ;
2018-02-22 12:41:18 -08:00
listDir ( apiConfig , oldFilePath , function listDirIterator ( s3 , objects , done ) {
2017-10-11 13:57:05 -07:00
total += objects . length ;
if ( retryCount === 0 ) concurrency = Math . min ( concurrency + 1 , 10 ) ; else concurrency = Math . max ( concurrency - 1 , 5 ) ;
2018-02-22 12:41:18 -08:00
events . emit ( 'progress' , ` ${ retryCount } errors so far. concurrency set to ${ concurrency } ` ) ;
2017-10-11 13:57:05 -07:00
retryCount = 0 ;
async . eachLimit ( objects , concurrency , copyFile . bind ( null , s3 ) , done ) ;
2017-10-04 11:00:30 -07:00
} , function ( error ) {
2017-10-10 20:23:04 -07:00
events . emit ( 'progress' , ` Copied ${ total } files ` ) ;
2017-10-04 11:00:30 -07:00
events . emit ( 'done' , error ) ;
} ) ;
return events ;
2015-09-21 14:02:00 -07:00
}
2016-10-10 15:04:28 +02:00
2017-09-27 17:34:49 -07:00
function remove ( apiConfig , filename , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof filename , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-10-10 23:42:24 -07:00
getS3Config ( apiConfig , function ( error , credentials ) {
2017-09-27 17:34:49 -07:00
if ( error ) return callback ( error ) ;
var s3 = new AWS . S3 ( credentials ) ;
var deleteParams = {
Bucket : apiConfig . bucket ,
Delete : {
Objects : [ { Key : filename } ]
}
} ;
2018-02-22 11:05:29 -08:00
// deleteObjects does not return error if key is not found
2017-09-27 17:34:49 -07:00
s3 . deleteObjects ( deleteParams , function ( error ) {
2018-02-22 12:14:13 -08:00
if ( error ) debug ( ` remove: Unable to remove ${ deleteParams . Key } . error: ${ error . message } ` ) ;
2017-09-27 17:34:49 -07:00
2018-02-22 11:05:29 -08:00
callback ( error ) ;
2017-09-27 17:34:49 -07:00
} ) ;
} ) ;
}
2017-10-10 20:23:04 -07:00
function removeDir ( apiConfig , pathPrefix ) {
2016-10-10 15:04:28 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-09-23 11:09:36 -07:00
assert . strictEqual ( typeof pathPrefix , 'string' ) ;
2016-10-10 15:04:28 +02:00
2017-10-10 20:23:04 -07:00
var events = new EventEmitter ( ) ;
var total = 0 ;
2018-02-22 12:14:13 -08:00
listDir ( apiConfig , pathPrefix , function listDirIterator ( s3 , objects , done ) {
2017-10-29 12:30:22 -07:00
total += objects . length ;
2018-02-22 11:01:04 -08:00
const chunkSize = apiConfig . provider !== 'digitalocean-spaces' ? 1000 : 100 ; // throttle objects in each request
var chunks = chunk ( objects , chunkSize ) ;
2017-10-11 13:57:05 -07:00
2018-02-22 12:14:13 -08:00
async . eachSeries ( chunks , function deleteFiles ( contents , iteratorCallback ) {
var deleteParams = {
Bucket : apiConfig . bucket ,
Delete : {
Objects : contents . map ( function ( c ) { return { Key : c . Key } ; } )
}
} ;
events . emit ( 'progress' , ` Removing ${ contents . length } files from ${ contents [ 0 ] . Key } to ${ contents [ contents . length - 1 ] . Key } ` ) ;
// deleteObjects does not return error if key is not found
s3 . deleteObjects ( deleteParams , function ( error /*, deleteData */ ) {
if ( error ) events . emit ( 'progress' , ` Unable to remove ${ deleteParams . Key } ${ error . message } ` ) ;
iteratorCallback ( error ) ;
} ) ;
} , done ) ;
2017-10-29 12:30:22 -07:00
} , function ( error ) {
2017-10-10 20:23:04 -07:00
events . emit ( 'progress' , ` Removed ${ total } files ` ) ;
events . emit ( 'done' , error ) ;
} ) ;
return events ;
2016-10-10 15:04:28 +02:00
}
2016-10-11 11:36:25 +02:00
function testConfig ( apiConfig , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-27 10:25:36 -07:00
if ( apiConfig . provider === 'caas' ) {
2017-09-27 17:34:49 -07:00
if ( typeof apiConfig . token !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'token must be a string' ) ) ;
2017-09-27 10:25:36 -07:00
} else {
if ( typeof apiConfig . accessKeyId !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'accessKeyId must be a string' ) ) ;
if ( typeof apiConfig . secretAccessKey !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'secretAccessKey must be a string' ) ) ;
}
2017-04-20 17:23:31 -07:00
if ( typeof apiConfig . bucket !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'bucket must be a string' ) ) ;
if ( typeof apiConfig . prefix !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'prefix must be a string' ) ) ;
2017-09-27 10:25:36 -07:00
if ( 'signatureVersion' in apiConfig && typeof apiConfig . signatureVersion !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'signatureVersion must be a string' ) ) ;
if ( 'endpoint' in apiConfig && typeof apiConfig . endpoint !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'endpoint must be a string' ) ) ;
2016-10-11 11:36:25 +02:00
2016-10-11 11:46:28 +02:00
// attempt to upload and delete a file with new credentials
2017-10-10 23:42:24 -07:00
getS3Config ( apiConfig , function ( error , credentials ) {
2016-10-11 11:46:28 +02:00
if ( error ) return callback ( error ) ;
var params = {
Bucket : apiConfig . bucket ,
2017-07-19 14:20:35 +02:00
Key : path . join ( apiConfig . prefix , 'cloudron-testfile' ) ,
2016-10-11 11:46:28 +02:00
Body : 'testcontent'
} ;
var s3 = new AWS . S3 ( credentials ) ;
s3 . putObject ( params , function ( error ) {
2017-11-22 20:26:22 -08:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message || error . code ) ) ; // DO sets 'code'
2016-10-11 11:46:28 +02:00
var params = {
Bucket : apiConfig . bucket ,
2017-07-19 14:20:35 +02:00
Key : path . join ( apiConfig . prefix , 'cloudron-testfile' )
2016-10-11 11:46:28 +02:00
} ;
s3 . deleteObject ( params , function ( error ) {
2017-11-22 20:26:22 -08:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message || error . code ) ) ; // DO sets 'code'
2016-10-11 11:46:28 +02:00
2017-04-18 16:51:54 +02:00
callback ( ) ;
2016-10-11 11:46:28 +02:00
} ) ;
} ) ;
} ) ;
2016-10-11 11:36:25 +02:00
}
2017-01-04 16:22:58 -08:00
2017-09-26 12:28:33 -07:00
function backupDone ( apiConfig , backupId , appBackupIds , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-21 10:31:43 +02:00
assert . strictEqual ( typeof backupId , 'string' ) ;
assert ( Array . isArray ( appBackupIds ) ) ;
2017-01-04 16:22:58 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-26 12:28:33 -07:00
if ( apiConfig . provider !== 'caas' ) return callback ( ) ;
2017-11-29 11:36:44 -08:00
debug ( '[%s] backupDone: %s apps %j' , backupId , backupId , appBackupIds ) ;
2017-09-26 12:28:33 -07:00
2017-11-28 16:52:52 -08:00
var url = config . apiServerOrigin ( ) + '/api/v1/boxes/' + apiConfig . fqdn + '/backupDone' ;
2017-09-26 12:28:33 -07:00
var data = {
boxVersion : config . version ( ) ,
2017-11-29 11:36:44 -08:00
backupId : backupId ,
2017-09-26 12:28:33 -07:00
appId : null , // now unused
appVersion : null , // now unused
2017-11-29 11:36:44 -08:00
appBackupIds : appBackupIds
2017-09-26 12:28:33 -07:00
} ;
2017-11-28 16:52:52 -08:00
superagent . post ( url ) . send ( data ) . query ( { token : apiConfig . token } ) . timeout ( 30 * 1000 ) . end ( function ( error , result ) {
2017-09-26 12:28:33 -07:00
if ( error && ! error . response ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error ) ) ;
if ( result . statusCode !== 200 ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , result . text ) ) ;
return callback ( null ) ;
} ) ;
2017-01-04 16:22:58 -08:00
}