2015-08-24 11:13:21 -07:00
'use strict' ;
exports = module . exports = {
2017-04-18 15:33:06 +02:00
backup : backup ,
restore : restore ,
copyBackup : copyBackup ,
2017-04-23 11:34:46 -07:00
removeBackups : removeBackups ,
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' ) ,
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 ,
debug = require ( 'debug' ) ( 'box:storage/s3' ) ,
once = require ( 'once' ) ,
2017-04-21 15:28:25 -07:00
PassThrough = require ( 'stream' ) . PassThrough ,
2017-04-18 15:33:06 +02:00
path = require ( 'path' ) ,
2017-04-21 15:28:25 -07:00
targz = require ( './targz.js' ) ;
2015-08-24 11:13:21 -07:00
2017-04-20 16:40:35 +02:00
var FILE _TYPE = '.tar.gz.enc' ;
2017-04-18 15:33:06 +02: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-04-18 15:33:06 +02:00
// internal only
2016-03-31 09:48:01 -07:00
function getBackupCredentials ( apiConfig , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2015-08-24 11:13:21 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-03-31 09:48:01 -07:00
assert ( apiConfig . accessKeyId && apiConfig . secretAccessKey ) ;
2015-11-06 18:22:29 -08:00
var credentials = {
2017-04-20 19:56:06 -07:00
signatureVersion : apiConfig . signatureVersion || 'v4' ,
2016-12-07 10:47:06 +01:00
s3ForcePathStyle : true ,
2016-03-31 09:48:01 -07:00
accessKeyId : apiConfig . accessKeyId ,
secretAccessKey : apiConfig . secretAccessKey ,
2016-03-31 09:48:38 -07:00
region : apiConfig . region || 'us-east-1'
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
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
function getBackupFilePath ( apiConfig , backupId ) {
2016-09-16 10:58:34 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-18 15:33:06 +02:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2016-09-16 10:58:34 +02:00
2017-04-18 15:33:06 +02:00
return path . join ( apiConfig . prefix , backupId . endsWith ( FILE _TYPE ) ? backupId : backupId + FILE _TYPE ) ;
2016-09-16 10:58:34 +02:00
}
2017-04-18 15:33:06 +02:00
// storage api
2017-04-20 14:11:26 -07:00
function backup ( apiConfig , backupId , sourceDirectories , callback ) {
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-18 15:33:06 +02:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-04-20 14:11:26 -07:00
assert ( Array . isArray ( sourceDirectories ) ) ;
2016-09-16 11:21:08 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-18 15:33:06 +02:00
callback = once ( callback ) ;
2016-09-16 11:21:08 +02:00
2017-04-18 15:33:06 +02:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
2016-09-16 11:21:08 +02:00
2017-04-20 14:11:26 -07:00
debug ( '[%s] backup: %j -> %s' , backupId , sourceDirectories , backupFilePath ) ;
2015-08-25 10:01:04 -07:00
2016-03-31 09:48:01 -07:00
getBackupCredentials ( apiConfig , function ( error , credentials ) {
2015-08-25 10:01:04 -07:00
if ( error ) return callback ( error ) ;
2017-04-21 15:28:25 -07:00
var passThrough = new PassThrough ( ) ;
2015-08-25 10:01:04 -07:00
var params = {
2016-04-04 11:44:24 -07:00
Bucket : apiConfig . bucket ,
2017-04-18 15:33:06 +02:00
Key : backupFilePath ,
2017-04-21 15:28:25 -07:00
Body : passThrough
2015-08-25 10:01:04 -07:00
} ;
2017-04-18 15:33:06 +02:00
var s3 = new AWS . S3 ( credentials ) ;
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
s3 . upload ( params , { partSize : 10 * 1024 * 1024 , queueSize : 1 } , function ( error ) {
2017-04-18 15:33:06 +02:00
if ( error ) {
2017-04-23 22:09:05 -07:00
debug ( '[%s] backup: s3 upload error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
}
2015-08-27 09:26:19 -07:00
2017-04-18 15:33:06 +02:00
callback ( null ) ;
} ) ;
2017-04-21 15:28:25 -07:00
2017-04-24 15:37:03 -07:00
targz . create ( sourceDirectories , apiConfig . key || '' , passThrough , callback ) ;
2015-08-25 10:01:04 -07:00
} ) ;
}
2015-08-26 16:14:51 -07:00
2017-04-20 15:35:52 +02:00
function restore ( apiConfig , backupId , destination , callback ) {
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-04-20 15:35:52 +02:00
assert . strictEqual ( typeof destination , 'string' ) ;
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-21 15:28:25 -07:00
callback = once ( callback ) ;
2017-04-27 09:43:34 -07:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
debug ( '[%s] restore: %s -> %s' , backupId , backupFilePath , destination ) ;
2016-09-19 15:03:38 +02:00
2017-04-18 15:33:06 +02:00
getBackupCredentials ( 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-04-21 15:28:25 -07:00
var s3get = s3 . getObject ( params ) . createReadStream ( ) ;
2017-04-21 15:06:54 -07:00
2017-04-21 15:28:25 -07:00
s3get . on ( 'error' , function ( error ) {
// TODO ENOENT for the mock, fix upstream!
if ( error . code === 'NoSuchKey' || error . code === 'ENOENT' ) return callback ( new BackupsError ( BackupsError . NOT _FOUND ) ) ;
2017-04-21 15:06:54 -07:00
2017-04-23 22:09:05 -07:00
debug ( '[%s] restore: s3 stream error.' , backupId , error ) ;
2017-04-21 15:28:25 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-21 15:28:25 -07:00
2017-04-27 09:43:34 -07:00
targz . extract ( s3get , destination , apiConfig . key || '' , callback ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2016-09-16 18:14:36 +02:00
}
2017-04-18 15:33:06 +02:00
function copyBackup ( apiConfig , oldBackupId , newBackupId , callback ) {
2016-03-31 09:48:01 -07:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-18 15:33:06 +02:00
assert . strictEqual ( typeof oldBackupId , 'string' ) ;
assert . strictEqual ( typeof newBackupId , 'string' ) ;
2015-09-21 14:02:00 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-03-31 09:48:01 -07:00
getBackupCredentials ( apiConfig , function ( error , credentials ) {
2015-09-21 14:02:00 -07:00
if ( error ) return callback ( error ) ;
var params = {
2017-04-18 15:33:06 +02:00
Bucket : apiConfig . bucket ,
Key : getBackupFilePath ( apiConfig , newBackupId ) ,
CopySource : path . join ( apiConfig . bucket , getBackupFilePath ( apiConfig , oldBackupId ) )
2015-09-21 14:02:00 -07:00
} ;
var s3 = new AWS . S3 ( credentials ) ;
2017-04-19 13:20:24 +02:00
s3 . copyObject ( params , function ( error ) {
2017-04-20 19:27:12 -07:00
if ( error && error . code === 'NoSuchKey' ) return callback ( new BackupsError ( BackupsError . NOT _FOUND , 'Old backup not found' ) ) ;
2017-04-18 15:33:06 +02:00
if ( error ) {
2017-04-23 22:09:05 -07:00
debug ( 'copyBackup: s3 copy error.' , error ) ;
2017-04-20 19:27:12 -07:00
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
}
callback ( null ) ;
} ) ;
2015-09-21 14:02:00 -07:00
} ) ;
}
2016-10-10 15:04:28 +02:00
2017-04-23 11:34:46 -07:00
function removeBackups ( apiConfig , backupIds , callback ) {
2016-10-10 15:04:28 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-23 11:34:46 -07:00
assert ( Array . isArray ( backupIds ) ) ;
2016-10-10 15:04:28 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-18 17:33:59 +02:00
getBackupCredentials ( apiConfig , function ( error , credentials ) {
if ( error ) return callback ( error ) ;
var params = {
Bucket : apiConfig . bucket ,
2017-04-23 11:34:46 -07:00
Delete : {
Objects : [ ] // { Key }
}
2017-04-18 17:33:59 +02:00
} ;
2016-10-10 15:04:28 +02:00
2017-04-23 11:34:46 -07:00
backupIds . forEach ( function ( backupId ) {
params . Delete . Objects . push ( { Key : getBackupFilePath ( apiConfig , backupId ) } ) ;
} ) ;
2017-04-18 17:33:59 +02:00
var s3 = new AWS . S3 ( credentials ) ;
2017-04-23 22:09:05 -07:00
s3 . deleteObjects ( params , function ( error , data ) {
if ( error ) debug ( 'removeBackups: Unable to remove %s. Not fatal.' , params . Key , error ) ;
else debug ( 'removeBackups: Deleted: %j Errors: %j' , data . Deleted , data . Errors ) ;
2017-04-18 17:33:59 +02:00
callback ( null ) ;
} ) ;
} ) ;
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-04-20 17:23:31 -07:00
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' ) ) ;
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-04-20 19:56:06 -07:00
if ( 'signatureVersion' in apiConfig && typeof apiConfig . prefix !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'signatureVersion must be a string' ) ) ;
2017-04-22 11:52:47 -07:00
if ( 'endpoint' in apiConfig && typeof apiConfig . prefix !== '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
getBackupCredentials ( apiConfig , function ( error , credentials ) {
if ( error ) return callback ( error ) ;
var params = {
Bucket : apiConfig . bucket ,
2017-04-18 16:51:54 +02:00
Key : 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-04-20 17:23:31 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2016-10-11 11:46:28 +02:00
var params = {
Bucket : apiConfig . bucket ,
2017-04-18 16:51:54 +02:00
Key : apiConfig . prefix + '/cloudron-testfile'
2016-10-11 11:46:28 +02:00
} ;
s3 . deleteObject ( params , function ( error ) {
2017-04-20 17:23:31 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
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-04-21 10:31:43 +02:00
function backupDone ( backupId , appBackupIds , callback ) {
assert . strictEqual ( typeof backupId , 'string' ) ;
assert ( Array . isArray ( appBackupIds ) ) ;
2017-01-04 16:22:58 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
callback ( ) ;
}