2016-09-15 11:29:45 +02:00
'use strict' ;
exports = module . exports = {
2017-04-11 11:00:55 +02:00
backup : backup ,
restore : restore ,
2017-04-17 14:46:19 +02:00
copyBackup : copyBackup ,
2017-04-17 15:45:58 +02:00
removeBackup : removeBackup ,
2016-09-16 11:21:08 +02:00
2017-04-11 11:00:55 +02:00
getDownloadStream : getDownloadStream ,
2016-09-16 10:59:17 +02:00
2017-01-04 16:22:58 -08:00
backupDone : backupDone ,
2016-10-11 11:36:25 +02:00
testConfig : testConfig
2016-09-15 11:29:45 +02:00
} ;
2017-04-20 15:15:49 +02:00
var assert = require ( 'assert' ) ,
2016-10-10 16:10:51 +02:00
async = require ( 'async' ) ,
2016-10-10 13:21:45 +02:00
BackupsError = require ( '../backups.js' ) . BackupsError ,
2017-04-18 15:32:59 +02:00
crypto = require ( 'crypto' ) ,
2017-04-21 17:21:10 +02:00
config = require ( '../config.js' ) ,
2017-04-11 11:00:55 +02:00
debug = require ( 'debug' ) ( 'box:storage/filesystem' ) ,
2016-09-16 11:55:59 +02:00
fs = require ( 'fs' ) ,
2017-04-11 11:00:55 +02:00
mkdirp = require ( 'mkdirp' ) ,
once = require ( 'once' ) ,
2017-04-18 15:32:59 +02:00
path = require ( 'path' ) ,
2017-04-20 16:59:17 -07:00
safe = require ( 'safetydance' ) ,
2017-04-21 18:56:09 +02:00
spawn = require ( 'child_process' ) . spawn ,
2017-04-11 11:00:55 +02:00
tar = require ( 'tar-fs' ) ,
2017-04-18 15:32:59 +02:00
zlib = require ( 'zlib' ) ;
2016-09-16 11:55:59 +02:00
2016-09-16 14:10:34 +02:00
var FALLBACK _BACKUP _FOLDER = '/var/backups' ;
2017-04-20 16:40:35 +02:00
var FILE _TYPE = '.tar.gz.enc' ;
2017-04-21 17:21:10 +02:00
var BACKUP _USER = config . TEST ? process . env . USER : 'yellowtent' ;
2016-09-15 11:29:45 +02:00
2017-04-17 14:46:19 +02:00
// internal only
2017-04-18 14:39:48 +02:00
function getBackupFilePath ( apiConfig , backupId ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
return path . join ( apiConfig . backupFolder || FALLBACK _BACKUP _FOLDER , backupId . endsWith ( FILE _TYPE ) ? backupId : backupId + FILE _TYPE ) ;
}
2017-04-18 15:32:59 +02:00
// storage api
2017-04-20 14:11:26 -07:00
function backup ( apiConfig , backupId , sourceDirectories , callback ) {
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-11 11:00:55 +02:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-04-20 14:11:26 -07:00
assert ( Array . isArray ( sourceDirectories ) ) ;
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-11 11:00:55 +02:00
callback = once ( callback ) ;
2016-09-16 10:58:34 +02:00
2017-04-18 14:39:48 +02:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
2016-09-16 10:58:34 +02:00
2017-04-20 14:11:26 -07:00
debug ( '[%s] backup: %j -> %s' , backupId , sourceDirectories , backupFilePath ) ;
2016-09-15 11:29:45 +02:00
2017-04-20 16:59:17 -07:00
mkdirp ( path . dirname ( backupFilePath ) , function ( error ) {
2017-04-20 19:00:12 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2016-09-16 11:21:08 +02:00
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:15:49 +02:00
var gzip = zlib . createGzip ( { } ) ;
2017-04-20 15:35:52 +02:00
var encrypt = crypto . createCipher ( 'aes-256-cbc' , apiConfig . key || '' ) ;
2017-04-20 16:59:17 -07:00
var fileStream = fs . createWriteStream ( backupFilePath ) ;
2016-09-16 11:21:08 +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:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-14 17:46:25 +02:00
} ) ;
2017-04-20 15:15:49 +02:00
gzip . on ( 'error' , function ( error ) {
console . error ( '[%s] backup: gzip stream error.' , backupId , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-11 11:00:55 +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:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
fileStream . on ( 'error' , function ( error ) {
console . error ( '[%s] backup: out stream error.' , backupId , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:35:52 +02:00
} ) ;
2017-04-11 11:00:55 +02:00
fileStream . on ( 'close' , function ( ) {
2017-04-20 16:59:17 -07:00
debug ( '[%s] backup: changing ownership.' , backupId ) ;
2017-04-21 17:21:10 +02:00
if ( ! safe . child _process . execSync ( 'chown -R ' + BACKUP _USER + ':' + BACKUP _USER + ' ' + path . dirname ( backupFilePath ) ) ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , safe . error . message ) ) ;
2017-04-20 16:59:17 -07:00
2017-04-11 11:00:55 +02:00
debug ( '[%s] backup: done.' , backupId ) ;
2017-04-20 16:59:17 -07:00
2017-04-17 14:46:19 +02:00
callback ( null ) ;
2017-04-11 11:00:55 +02:00
} ) ;
2017-04-20 15:35:52 +02:00
pack . pipe ( gzip ) . pipe ( encrypt ) . pipe ( fileStream ) ;
2017-04-11 11:00:55 +02:00
} ) ;
2016-09-16 11:21:08 +02:00
}
2017-04-20 15:15:49 +02:00
function restore ( apiConfig , backupId , destination , callback ) {
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
2017-04-11 11:00:55 +02:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-04-20 15:15:49 +02:00
assert . strictEqual ( typeof destination , 'string' ) ;
2016-09-15 11:29:45 +02:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-21 18:56:09 +02:00
var isOldFormat = backupId . endsWith ( '.tar.gz' ) ;
var sourceFilePath = isOldFormat ? path . join ( apiConfig . backupFolder || FALLBACK _BACKUP _FOLDER , backupId ) : getBackupFilePath ( apiConfig , backupId ) ;
2017-04-11 11:00:55 +02:00
2017-04-20 15:15:49 +02:00
debug ( '[%s] restore: %s -> %s' , backupId , sourceFilePath , destination ) ;
2017-04-11 11:00:55 +02:00
if ( ! fs . existsSync ( sourceFilePath ) ) return callback ( new BackupsError ( BackupsError . NOT _FOUND , 'backup file does not exist' ) ) ;
2017-04-20 15:15:49 +02:00
mkdirp ( destination , function ( error ) {
2017-04-20 19:00:12 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-11 11:00:55 +02:00
2017-04-20 15:15:49 +02:00
var fileStream = fs . createReadStream ( sourceFilePath ) ;
2017-04-21 18:56:09 +02:00
var decrypt ;
if ( isOldFormat ) {
let args = [ 'aes-256-cbc' , '-d' , '-pass' , 'pass:' + apiConfig . key ] ;
decrypt = spawn ( 'openssl' , args , { stdio : [ 'pipe' , 'pipe' , process . stderr ] } ) ;
} else {
decrypt = crypto . createDecipher ( 'aes-256-cbc' , apiConfig . key || '' ) ;
}
2017-04-20 15:15:49 +02:00
var gunzip = zlib . createGunzip ( { } ) ;
var extract = tar . extract ( destination ) ;
2017-04-14 17:46:25 +02:00
2017-04-20 15:15:49 +02:00
fileStream . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: file stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:15:49 +02:00
} ) ;
2017-04-14 15:15:50 +02:00
2017-04-21 18:56:09 +02:00
decrypt . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: decrypt stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:15:49 +02:00
} ) ;
2016-09-16 12:00:20 +02:00
2017-04-20 15:15:49 +02:00
gunzip . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: gunzip stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:15:49 +02:00
} ) ;
2016-10-10 18:11:23 +02:00
2017-04-20 15:15:49 +02:00
extract . on ( 'error' , function ( error ) {
console . error ( '[%s] restore: extract stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-11 11:00:55 +02:00
} ) ;
2017-04-20 15:15:49 +02:00
extract . on ( 'finish' , function ( ) {
debug ( '[%s] restore: %s done.' , backupId ) ;
2017-04-21 17:21:10 +02:00
callback ( null ) ;
2017-04-20 15:15:49 +02:00
} ) ;
2017-04-11 11:00:55 +02:00
2017-04-21 18:56:09 +02:00
if ( isOldFormat ) {
fileStream . pipe ( decrypt . stdin ) ;
decrypt . stdout . pipe ( gunzip ) . pipe ( extract ) ;
} else {
fileStream . pipe ( decrypt ) . pipe ( gunzip ) . pipe ( extract ) ;
}
2017-04-17 14:46:19 +02:00
} ) ;
}
function copyBackup ( apiConfig , oldBackupId , newBackupId , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof oldBackupId , 'string' ) ;
assert . strictEqual ( typeof newBackupId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
callback = once ( callback ) ;
2017-04-18 14:39:48 +02:00
var oldFilePath = getBackupFilePath ( apiConfig , oldBackupId ) ;
var newFilePath = getBackupFilePath ( apiConfig , newBackupId ) ;
2017-04-17 14:46:19 +02:00
2017-04-20 15:15:49 +02:00
debug ( 'copyBackup: %s -> %s' , oldFilePath , newFilePath ) ;
2017-04-20 16:59:17 -07:00
mkdirp ( path . dirname ( newFilePath ) , function ( error ) {
2017-04-20 19:00:12 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-17 14:46:19 +02:00
2017-04-20 15:41:25 +02:00
var readStream = fs . createReadStream ( oldFilePath ) ;
var writeStream = fs . createWriteStream ( newFilePath ) ;
readStream . on ( 'error' , function ( error ) {
console . error ( 'copyBackup: read stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:41:25 +02:00
} ) ;
2017-04-20 15:15:49 +02:00
2017-04-20 15:41:25 +02:00
writeStream . on ( 'error' , function ( error ) {
console . error ( 'copyBackup: write stream error.' , error ) ;
2017-04-20 19:00:12 -07:00
callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-20 15:41:25 +02:00
} ) ;
2017-04-20 15:15:49 +02:00
2017-04-20 16:59:17 -07:00
writeStream . on ( 'close' , function ( ) {
2017-04-21 17:21:10 +02:00
if ( ! safe . child _process . execSync ( 'chown -R ' + BACKUP _USER + ':' + BACKUP _USER + ' ' + path . dirname ( newFilePath ) ) ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , safe . error . message ) ) ;
2017-04-20 16:59:17 -07:00
callback ( ) ;
} ) ;
2017-04-20 15:41:25 +02:00
readStream . pipe ( writeStream ) ;
} ) ;
2016-09-15 11:29:45 +02:00
}
2017-04-17 15:45:58 +02:00
function removeBackup ( apiConfig , backupId , appBackupIds , callback ) {
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert ( Array . isArray ( appBackupIds ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
async . each ( [ backupId ] . concat ( appBackupIds ) , function ( id , callback ) {
2017-04-18 14:39:48 +02:00
var filePath = getBackupFilePath ( apiConfig , id ) ;
2017-04-17 15:45:58 +02:00
fs . unlink ( filePath , function ( error ) {
2017-04-18 17:33:17 +02:00
if ( error ) console . error ( 'Unable to remove %s. Not fatal.' , filePath , error ) ;
2017-04-17 15:45:58 +02:00
callback ( ) ;
} ) ;
} , callback ) ;
}
2017-04-11 11:00:55 +02:00
function getDownloadStream ( apiConfig , backupId , callback ) {
2016-09-19 15:03:38 +02:00
assert . strictEqual ( typeof apiConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-18 14:39:48 +02:00
var backupFilePath = getBackupFilePath ( apiConfig , backupId ) ;
2016-09-19 15:03:38 +02:00
2017-04-11 11:00:55 +02:00
debug ( '[%s] getDownloadStream: %s %s' , backupId , backupId , backupFilePath ) ;
2016-09-19 15:03:38 +02:00
2017-04-11 11:00:55 +02:00
if ( ! fs . existsSync ( backupFilePath ) ) return callback ( new BackupsError ( BackupsError . NOT _FOUND , 'backup file does not exist' ) ) ;
var stream = fs . createReadStream ( backupFilePath ) ;
callback ( null , stream ) ;
2016-09-19 15:03:38 +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-21 15:37:57 +02:00
if ( 'backupFolder' in apiConfig && typeof apiConfig . backupFolder !== 'string' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'backupFolder must be string' ) ) ;
2016-10-11 11:36:25 +02:00
2017-04-21 15:37:57 +02:00
// default value will be used
if ( ! apiConfig . backupFolder ) return callback ( ) ;
fs . stat ( apiConfig . backupFolder , function ( error , result ) {
if ( error ) {
debug ( 'testConfig: %s' , apiConfig . backupFolder , error ) ;
return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'Directory does not exist or cannot be accessed' ) ) ;
}
if ( ! result . isDirectory ( ) ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'Backup location is not a directory' ) ) ;
callback ( null ) ;
} ) ;
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 ( ) ;
}