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 ,
2016-10-11 11:36:25 +02:00
removeBackup : removeBackup ,
2017-04-18 15:33:06 +02:00
getDownloadStream : getDownloadStream ,
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 ,
crypto = require ( 'crypto' ) ,
debug = require ( 'debug' ) ( 'box:storage/s3' ) ,
mkdirp = require ( 'mkdirp' ) ,
once = require ( 'once' ) ,
path = require ( 'path' ) ,
2017-04-21 10:50:25 -07:00
progress = require ( 'progress-stream' ) ,
2017-04-18 15:33:06 +02:00
tar = require ( 'tar-fs' ) ,
zlib = require ( 'zlib' ) ;
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-20 14:11:26 -07:00
var pack = tar . pack ( '/' , {
entries : sourceDirectories . map ( function ( m ) { return m . source ; } ) ,
map : function ( header ) {
sourceDirectories . forEach ( function ( m ) {
header . name = header . name . replace ( new RegExp ( '^' + m . source + '(/?)' ) , m . destination + '$1' ) ;
} ) ;
return header ;
}
} ) ;
2017-04-20 15:35:52 +02:00
var gzip = zlib . createGzip ( { } ) ;
2017-04-18 15:33:06 +02:00
var encrypt = crypto . createCipher ( 'aes-256-cbc' , apiConfig . key || '' ) ;
2017-04-21 10:50:25 -07:00
var progressStream = progress ( { time : 10000 } ) ; // display a progress every 10 seconds
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
pack . on ( 'error' , function ( error ) {
console . error ( '[%s] backup: tar stream error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-20 15:35:52 +02:00
gzip . on ( 'error' , function ( error ) {
console . error ( '[%s] backup: gzip stream error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-20 15:35:52 +02:00
encrypt . on ( 'error' , function ( error ) {
console . error ( '[%s] backup: encrypt stream error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-21 10:50:25 -07:00
progressStream . on ( 'progress' , function ( progress ) {
debug ( '[%s] backup: %s @ %s' , backupId , Math . round ( progress . transferred / 1024 / 1024 ) + 'M' , Math . round ( progress . speed / 1024 / 1024 ) + 'Mbps' ) ;
} ) ;
pack . pipe ( gzip ) . pipe ( encrypt ) . pipe ( progressStream ) ;
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 10:50:25 -07:00
Body : progressStream
2015-08-25 10:01:04 -07:00
} ;
2017-04-18 15:33:06 +02:00
var s3 = new AWS . S3 ( credentials ) ;
2017-04-19 13:20:24 +02:00
s3 . upload ( params , function ( error ) {
2017-04-18 15:33:06 +02:00
if ( error ) {
console . error ( '[%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 ) ;
} ) ;
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-18 15:33:06 +02:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
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-20 15:35:52 +02:00
mkdirp ( destination , function ( error ) {
2017-04-20 19:27:12 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
var params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath
} ;
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
var s3 = new AWS . S3 ( credentials ) ;
2017-04-18 16:44:49 +02:00
2017-04-20 15:35:52 +02:00
var s3get = s3 . getObject ( params ) . createReadStream ( ) ;
var decrypt = crypto . createDecipher ( 'aes-256-cbc' , apiConfig . key || '' ) ;
var gunzip = zlib . createGunzip ( { } ) ;
2017-04-21 10:50:25 -07:00
var progressStream = progress ( { time : 10000 } ) ; // display a progress every 10 seconds
2017-04-20 15:35:52 +02:00
var extract = tar . extract ( destination ) ;
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02: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-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
console . error ( '[%s] restore: s3 stream error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2017-04-18 15:33:06 +02:00
2017-04-21 10:50:25 -07:00
progressStream . on ( 'progress' , function ( progress ) {
debug ( '[%s] restore: %s @ %s' , backupId , Math . round ( progress . transferred / 1024 / 1024 ) + 'M' , Math . round ( progress . speed / 1024 / 1024 ) + 'Mbps' ) ;
} ) ;
2017-04-20 15:35:52 +02:00
decrypt . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: decipher stream error.' , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2016-09-16 18:14:36 +02:00
2017-04-20 15:35:52 +02:00
gunzip . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: gunzip stream error.' , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2017-04-18 15:33:06 +02:00
2017-04-20 15:35:52 +02:00
extract . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: extract stream error.' , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 15:33:06 +02:00
} ) ;
2017-04-20 15:35:52 +02:00
extract . on ( 'finish' , function ( ) {
debug ( '[%s] restore: done.' , backupId ) ;
2017-04-21 17:21:10 +02:00
callback ( null ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2017-04-18 15:33:06 +02:00
2017-04-21 10:50:25 -07:00
s3get . pipe ( progressStream ) . pipe ( decrypt ) . pipe ( gunzip ) . pipe ( extract ) ;
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 ) {
console . error ( '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
2016-10-10 15:45:12 +02:00
function removeBackup ( apiConfig , backupId , appBackupIds , callback ) {
2016-10-10 15:04:28 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2016-10-10 15:45:12 +02:00
assert ( Array . isArray ( appBackupIds ) ) ;
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 ,
Key : getBackupFilePath ( apiConfig , backupId )
} ;
2016-10-10 15:04:28 +02:00
2017-04-18 17:33:59 +02:00
var s3 = new AWS . S3 ( credentials ) ;
s3 . deleteObject ( params , function ( error ) {
if ( error ) console . error ( 'Unable to remove %s. Not fatal.' , params . Key , error ) ;
callback ( null ) ;
} ) ;
} ) ;
2016-10-10 15:04:28 +02:00
}
2016-10-11 11:36:25 +02:00
2017-04-18 15:33:06 +02:00
function getDownloadStream ( apiConfig , backupId , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-18 17:33:59 +02:00
callback = once ( callback ) ;
2017-04-18 15:33:06 +02:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
debug ( '[%s] getDownloadStream: %s %s' , backupId , backupId , backupFilePath ) ;
2017-04-18 17:33:59 +02:00
getBackupCredentials ( apiConfig , function ( error , credentials ) {
if ( error ) return callback ( error ) ;
var params = {
Bucket : apiConfig . bucket ,
Key : backupFilePath
} ;
var s3 = new AWS . S3 ( credentials ) ;
2017-04-19 13:20:24 +02:00
s3 . headObject ( params , function ( error ) {
2017-04-18 19:15:56 +02:00
// TODO ENOENT for the mock, fix upstream!
if ( error && ( error . code === 'NotFound' || error . code === 'ENOENT' ) ) return callback ( new BackupsError ( BackupsError . NOT _FOUND ) ) ;
2017-04-20 19:27:12 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 17:33:59 +02:00
var s3get = s3 . getObject ( params ) . createReadStream ( ) ;
var decrypt = crypto . createDecipher ( 'aes-256-cbc' , apiConfig . key || '' ) ;
s3get . on ( 'error' , function ( error ) {
if ( error . code === 'NoSuchKey' ) return callback ( new BackupsError ( BackupsError . NOT _FOUND ) ) ;
console . error ( '[%s] getDownloadStream: s3 stream error.' , backupId , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-18 17:33:59 +02:00
} ) ;
decrypt . on ( 'error' , function ( error ) {
console . error ( '[%s] getDownloadStream: decipher stream error.' , error ) ;
2017-04-20 19:27:12 -07:00
callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error . message ) ) ;
2017-04-18 17:33:59 +02:00
} ) ;
s3get . pipe ( decrypt ) ;
callback ( null , decrypt ) ;
} ) ;
} ) ;
2017-04-18 15:33:06 +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' ) ) ;
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 ( ) ;
}