2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
BackupsError : BackupsError ,
2016-10-11 11:36:25 +02:00
testConfig : testConfig ,
2017-05-30 14:09:55 -07:00
getByStatePaged : getByStatePaged ,
2016-03-08 08:52:20 -08:00
getByAppIdPaged : getByAppIdPaged ,
2015-07-20 00:09:47 -07:00
2017-11-16 11:22:09 -08:00
get : get ,
2015-09-21 14:14:21 -07:00
2018-12-09 03:20:00 -08:00
startBackupTask : startBackupTask ,
2016-04-10 19:17:44 -07:00
ensureBackup : ensureBackup ,
2017-11-22 10:58:07 -08:00
restore : restore ,
2016-04-10 19:17:44 -07:00
backupApp : backupApp ,
restoreApp : restoreApp ,
2016-09-16 18:14:36 +02:00
backupBoxAndApps : backupBoxAndApps ,
2017-09-19 20:27:36 -07:00
upload : upload ,
2019-01-10 16:00:49 -08:00
startCleanupTask : startCleanupTask ,
2017-09-20 09:57:16 -07:00
cleanup : cleanup ,
2017-09-27 20:52:36 -07:00
cleanupCacheFilesSync : cleanupCacheFilesSync ,
2017-09-20 09:57:16 -07:00
2019-02-09 18:08:10 -08:00
injectPrivateFields : injectPrivateFields ,
removePrivateFields : removePrivateFields ,
2019-02-28 16:46:30 -08:00
checkConfiguration : checkConfiguration ,
2019-02-09 18:08:10 -08:00
SECRET _PLACEHOLDER : String . fromCharCode ( 0x25CF ) . repeat ( 8 ) ,
2017-09-20 09:57:16 -07:00
// for testing
_getBackupFilePath : getBackupFilePath ,
2017-10-12 16:02:09 -07:00
_restoreFsMetadata : restoreFsMetadata ,
_saveFsMetadata : saveFsMetadata
2015-07-20 00:09:47 -07:00
} ;
2016-04-10 19:17:44 -07:00
var addons = require ( './addons.js' ) ,
appdb = require ( './appdb.js' ) ,
apps = require ( './apps.js' ) ,
2017-09-19 20:40:38 -07:00
AppsError = require ( './apps.js' ) . AppsError ,
2016-04-10 19:17:44 -07:00
async = require ( 'async' ) ,
assert = require ( 'assert' ) ,
2016-03-07 19:44:38 -08:00
backupdb = require ( './backupdb.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2017-09-20 09:57:16 -07:00
crypto = require ( 'crypto' ) ,
2017-11-22 10:58:07 -08:00
database = require ( './database.js' ) ,
2016-04-10 19:17:44 -07:00
DatabaseError = require ( './databaseerror.js' ) ,
2019-01-22 17:35:36 -08:00
DataLayout = require ( './datalayout.js' ) ,
2015-07-20 00:09:47 -07:00
debug = require ( 'debug' ) ( 'box:backups' ) ,
2019-08-12 21:56:41 -07:00
df = require ( '@sindresorhus/df' ) ,
2018-12-09 03:20:00 -08:00
eventlog = require ( './eventlog.js' ) ,
2017-09-22 14:40:37 -07:00
fs = require ( 'fs' ) ,
2018-12-09 03:20:00 -08:00
locker = require ( './locker.js' ) ,
2017-09-20 09:57:16 -07:00
mkdirp = require ( 'mkdirp' ) ,
once = require ( 'once' ) ,
2016-04-10 19:17:44 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2017-09-20 09:57:16 -07:00
progressStream = require ( 'progress-stream' ) ,
2019-08-12 21:56:41 -07:00
prettyBytes = require ( 'pretty-bytes' ) ,
2017-04-21 14:07:10 -07:00
safe = require ( 'safetydance' ) ,
2016-04-10 19:17:44 -07:00
shell = require ( './shell.js' ) ,
2015-11-07 22:06:09 -08:00
settings = require ( './settings.js' ) ,
2017-09-22 14:40:37 -07:00
syncer = require ( './syncer.js' ) ,
2017-09-20 09:57:16 -07:00
tar = require ( 'tar-fs' ) ,
2018-11-16 11:13:03 -08:00
tasks = require ( './tasks.js' ) ,
2017-09-20 09:57:16 -07:00
util = require ( 'util' ) ,
zlib = require ( 'zlib' ) ;
2016-04-10 19:17:44 -07:00
2018-11-29 23:30:54 -08:00
const BACKUP _UPLOAD _CMD = path . join ( _ _dirname , 'scripts/backupupload.js' ) ;
2018-11-17 19:53:15 -08:00
2017-09-19 20:40:38 -07:00
function debugApp ( app ) {
2018-02-08 15:07:49 +01:00
assert ( typeof app === 'object' ) ;
2016-04-10 19:17:44 -07:00
2018-02-08 15:07:49 +01:00
debug ( app . fqdn + ' ' + util . format . apply ( util , Array . prototype . slice . call ( arguments , 1 ) ) ) ;
2016-04-10 19:17:44 -07:00
}
2015-07-20 00:09:47 -07:00
function BackupsError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( BackupsError , Error ) ;
BackupsError . EXTERNAL _ERROR = 'external error' ;
BackupsError . INTERNAL _ERROR = 'internal error' ;
2016-04-10 19:17:44 -07:00
BackupsError . BAD _STATE = 'bad state' ;
2017-04-20 17:23:31 -07:00
BackupsError . BAD _FIELD = 'bad field' ;
2016-10-10 13:21:45 +02:00
BackupsError . NOT _FOUND = 'not found' ;
2015-07-20 00:09:47 -07:00
2015-11-06 18:14:59 -08:00
// choose which storage backend we use for test purpose we use s3
2015-11-07 22:06:09 -08:00
function api ( provider ) {
switch ( provider ) {
2017-09-17 23:46:53 -07:00
case 's3' : return require ( './storage/s3.js' ) ;
2017-09-17 17:51:00 +02:00
case 'gcs' : return require ( './storage/gcs.js' ) ;
2017-09-17 23:46:53 -07:00
case 'filesystem' : return require ( './storage/filesystem.js' ) ;
case 'minio' : return require ( './storage/s3.js' ) ;
2017-09-21 12:13:43 -07:00
case 's3-v4-compat' : return require ( './storage/s3.js' ) ;
2017-09-21 12:25:39 -07:00
case 'digitalocean-spaces' : return require ( './storage/s3.js' ) ;
2017-09-17 23:46:53 -07:00
case 'exoscale-sos' : return require ( './storage/s3.js' ) ;
2019-07-22 16:44:56 -07:00
case 'wasabi' : return require ( './storage/s3.js' ) ;
2019-04-12 10:10:43 -07:00
case 'scaleway-objectstorage' : return require ( './storage/s3.js' ) ;
2017-09-17 23:46:53 -07:00
case 'noop' : return require ( './storage/noop.js' ) ;
2017-09-17 18:50:23 -07:00
default : return null ;
2015-11-07 22:06:09 -08:00
}
2015-11-06 18:14:59 -08:00
}
2019-02-09 18:08:10 -08:00
function injectPrivateFields ( newConfig , currentConfig ) {
if ( newConfig . key === exports . SECRET _PLACEHOLDER ) newConfig . key = currentConfig . key ;
if ( newConfig . provider === currentConfig . provider ) api ( newConfig . provider ) . injectPrivateFields ( newConfig , currentConfig ) ;
}
function removePrivateFields ( backupConfig ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
if ( backupConfig . key ) backupConfig . key = exports . SECRET _PLACEHOLDER ;
return api ( backupConfig . provider ) . removePrivateFields ( backupConfig ) ;
}
2016-10-11 11:36:25 +02:00
function testConfig ( backupConfig , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var func = api ( backupConfig . provider ) ;
2017-09-25 23:49:49 -07:00
if ( ! func ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'unknown storage provider' ) ) ;
2017-09-30 14:19:17 -07:00
if ( backupConfig . format !== 'tgz' && backupConfig . format !== 'rsync' ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'unknown format' ) ) ;
2016-10-11 11:36:25 +02:00
2018-09-04 10:48:54 -07:00
// remember to adjust the cron ensureBackup task interval accordingly
2018-08-13 22:31:35 -07:00
if ( backupConfig . intervalSecs < 6 * 60 * 60 ) return callback ( new BackupsError ( BackupsError . BAD _FIELD , 'Interval must be atleast 6 hours' ) ) ;
2016-10-11 11:36:25 +02:00
api ( backupConfig . provider ) . testConfig ( backupConfig , callback ) ;
}
2017-05-30 14:09:55 -07:00
function getByStatePaged ( state , page , perPage , callback ) {
assert . strictEqual ( typeof state , 'string' ) ;
2016-03-08 08:52:20 -08:00
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-05-30 14:09:55 -07:00
backupdb . getByTypeAndStatePaged ( backupdb . BACKUP _TYPE _BOX , state , page , perPage , function ( error , results ) {
2015-11-07 22:06:09 -08:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2016-03-08 08:52:20 -08:00
callback ( null , results ) ;
} ) ;
}
function getByAppIdPaged ( page , perPage , appId , callback ) {
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
backupdb . getByAppIdPaged ( page , perPage , appId , function ( error , results ) {
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-03-08 08:52:20 -08:00
callback ( null , results ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2017-11-16 11:22:09 -08:00
function get ( backupId , callback ) {
2016-06-13 13:42:25 -07:00
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-18 12:08:26 +02:00
backupdb . get ( backupId , function ( error , result ) {
2018-07-30 07:39:34 -07:00
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new BackupsError ( BackupsError . NOT _FOUND ) ) ;
2016-06-13 13:42:25 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2017-11-16 11:22:09 -08:00
callback ( null , result ) ;
2016-06-13 13:42:25 -07:00
} ) ;
}
2017-09-27 17:34:49 -07:00
function getBackupFilePath ( backupConfig , backupId , format ) {
2017-09-19 20:40:38 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
2017-09-19 20:40:38 -07:00
2017-09-27 17:34:49 -07:00
if ( format === 'tgz' ) {
2017-09-22 14:40:37 -07:00
const fileType = backupConfig . key ? '.tar.gz.enc' : '.tar.gz' ;
2017-10-05 10:57:46 -07:00
return path . join ( backupConfig . prefix || backupConfig . backupFolder || '' , backupId + fileType ) ;
2017-09-22 14:40:37 -07:00
} else {
2017-10-05 10:57:46 -07:00
return path . join ( backupConfig . prefix || backupConfig . backupFolder || '' , backupId ) ;
2017-09-22 14:40:37 -07:00
}
2017-09-19 20:40:38 -07:00
}
2018-07-27 06:55:54 -07:00
function encryptFilePath ( filePath , key ) {
assert . strictEqual ( typeof filePath , 'string' ) ;
assert . strictEqual ( typeof key , 'string' ) ;
var encryptedParts = filePath . split ( '/' ) . map ( function ( part ) {
const cipher = crypto . createCipher ( 'aes-256-cbc' , key ) ;
let crypt = cipher . update ( part ) ;
crypt = Buffer . concat ( [ crypt , cipher . final ( ) ] ) ;
2018-07-27 11:46:42 -07:00
return crypt . toString ( 'base64' ) // ensures path is valid
. replace ( /\//g , '-' ) // replace '/' of base64 since it conflicts with path separator
. replace ( /=/g , '' ) ; // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
2018-07-27 06:55:54 -07:00
} ) ;
return encryptedParts . join ( '/' ) ;
}
2018-07-27 11:46:42 -07:00
function decryptFilePath ( filePath , key ) {
assert . strictEqual ( typeof filePath , 'string' ) ;
assert . strictEqual ( typeof key , 'string' ) ;
2018-07-29 21:01:20 -07:00
let decryptedParts = [ ] ;
for ( let part of filePath . split ( '/' ) ) {
2018-07-27 11:46:42 -07:00
part = part + Array ( part . length % 4 ) . join ( '=' ) ; // add back = padding
part = part . replace ( /-/g , '/' ) ; // replace with '/'
2018-07-29 21:01:20 -07:00
try {
let decrypt = crypto . createDecipher ( 'aes-256-cbc' , key ) ;
let text = decrypt . update ( Buffer . from ( part , 'base64' ) ) ;
text = Buffer . concat ( [ text , decrypt . final ( ) ] ) ;
decryptedParts . push ( text . toString ( 'utf8' ) ) ;
} catch ( error ) {
debug ( ` Error decrypting file ${ filePath } part ${ part } : ` , error ) ;
return null ;
}
}
2018-07-27 11:46:42 -07:00
return decryptedParts . join ( '/' ) ;
}
2018-07-27 06:55:54 -07:00
function createReadStream ( sourceFile , key ) {
assert . strictEqual ( typeof sourceFile , 'string' ) ;
assert ( key === null || typeof key === 'string' ) ;
var stream = fs . createReadStream ( sourceFile ) ;
var ps = progressStream ( { time : 10000 } ) ; // display a progress every 10 seconds
stream . on ( 'error' , function ( error ) {
2019-01-18 14:50:04 -08:00
debug ( 'createReadStream: read stream error.' , error ) ;
2018-07-27 06:55:54 -07:00
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
if ( key !== null ) {
var encrypt = crypto . createCipher ( 'aes-256-cbc' , key ) ;
encrypt . on ( 'error' , function ( error ) {
debug ( 'createReadStream: encrypt stream error.' , error ) ;
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
return stream . pipe ( encrypt ) . pipe ( ps ) ;
} else {
return stream . pipe ( ps ) ;
}
}
2018-07-27 11:46:42 -07:00
function createWriteStream ( destFile , key ) {
assert . strictEqual ( typeof destFile , 'string' ) ;
assert ( key === null || typeof key === 'string' ) ;
var stream = fs . createWriteStream ( destFile ) ;
if ( key !== null ) {
var decrypt = crypto . createDecipher ( 'aes-256-cbc' , key ) ;
decrypt . on ( 'error' , function ( error ) {
debug ( 'createWriteStream: decrypt stream error.' , error ) ;
} ) ;
decrypt . pipe ( stream ) ;
return decrypt ;
} else {
return stream ;
}
}
2018-12-20 14:33:29 -08:00
function tarPack ( dataLayout , key , callback ) {
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2017-09-20 09:57:16 -07:00
assert ( key === null || typeof key === 'string' ) ;
2018-12-20 11:41:38 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-20 09:57:16 -07:00
var pack = tar . pack ( '/' , {
dereference : false , // pack the symlink and not what it points to
2019-01-19 10:28:02 -08:00
entries : dataLayout . localPaths ( ) ,
2019-04-01 11:56:20 -07:00
ignoreStatError : ( path , err ) => {
debug ( ` tarPack: error stat'ing ${ path } - ${ err . code } ` ) ;
return err . code === 'ENOENT' ; // ignore if file or dir got removed (probably some temporary file)
2019-03-31 17:30:58 -07:00
} ,
2017-09-20 09:57:16 -07:00
map : function ( header ) {
2019-01-19 10:28:02 -08:00
header . name = dataLayout . toRemotePath ( header . name ) ;
2019-07-10 14:35:47 -07:00
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
// https://www.systutorials.com/docs/linux/man/5-star/
if ( header . size > 8589934590 || header . name > 99 ) header . pax = { size : header . size } ;
2017-09-20 09:57:16 -07:00
return header ;
} ,
strict : false // do not error for unknown types (skip fifo, char/block devices)
} ) ;
var gzip = zlib . createGzip ( { } ) ;
2018-11-27 10:24:54 -08:00
var ps = progressStream ( { time : 10000 } ) ; // emit 'pgoress' every 10 seconds
2017-09-20 09:57:16 -07:00
pack . on ( 'error' , function ( error ) {
2018-12-20 11:41:38 -08:00
debug ( 'tarPack: tar stream error.' , error ) ;
2017-09-20 09:57:16 -07:00
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
gzip . on ( 'error' , function ( error ) {
2018-12-20 11:41:38 -08:00
debug ( 'tarPack: gzip stream error.' , error ) ;
2017-09-20 09:57:16 -07:00
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
if ( key !== null ) {
var encrypt = crypto . createCipher ( 'aes-256-cbc' , key ) ;
encrypt . on ( 'error' , function ( error ) {
2018-12-20 11:41:38 -08:00
debug ( 'tarPack: encrypt stream error.' , error ) ;
2017-09-20 09:57:16 -07:00
ps . emit ( 'error' , new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
} ) ;
2018-12-20 11:41:38 -08:00
pack . pipe ( gzip ) . pipe ( encrypt ) . pipe ( ps ) ;
2017-09-20 09:57:16 -07:00
} else {
2018-12-20 11:41:38 -08:00
pack . pipe ( gzip ) . pipe ( ps ) ;
2017-09-20 09:57:16 -07:00
}
2018-12-20 11:41:38 -08:00
callback ( null , ps ) ;
2017-09-20 09:57:16 -07:00
}
2019-01-18 14:50:04 -08:00
function sync ( backupConfig , backupId , dataLayout , progressCallback , callback ) {
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-15 20:44:09 -08:00
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
const concurrency = backupConfig . syncConcurrency || ( backupConfig . provider === 's3' ? 20 : 10 ) ;
2019-01-14 13:17:51 -08:00
2019-01-19 10:28:02 -08:00
syncer . sync ( dataLayout , function processTask ( task , iteratorCallback ) {
2017-09-28 14:26:39 -07:00
debug ( 'sync: processing task: %j' , task ) ;
2018-07-31 19:41:03 -07:00
// the empty task.path is special to signify the directory
const destPath = task . path && backupConfig . key ? encryptFilePath ( task . path , backupConfig . key ) : task . path ;
2018-07-27 06:55:54 -07:00
const backupFilePath = path . join ( getBackupFilePath ( backupConfig , backupId , backupConfig . format ) , destPath ) ;
2017-09-27 17:34:49 -07:00
2018-02-22 10:58:56 -08:00
if ( task . operation === 'removedir' ) {
2018-11-16 19:24:56 -08:00
debug ( ` Removing directory ${ backupFilePath } ` ) ;
2018-02-22 10:58:56 -08:00
return api ( backupConfig . provider ) . removeDir ( backupConfig , backupFilePath )
2018-11-27 10:24:54 -08:00
. on ( 'progress' , ( message ) => progressCallback ( { message } ) )
2018-02-22 10:58:56 -08:00
. on ( 'done' , iteratorCallback ) ;
} else if ( task . operation === 'remove' ) {
2018-11-16 19:24:56 -08:00
debug ( ` Removing ${ backupFilePath } ` ) ;
2018-02-22 10:58:56 -08:00
return api ( backupConfig . provider ) . remove ( backupConfig , backupFilePath , iteratorCallback ) ;
}
2017-10-10 20:23:04 -07:00
var retryCount = 0 ;
async . retry ( { times : 5 , interval : 20000 } , function ( retryCallback ) {
2018-02-27 19:16:03 -08:00
retryCallback = once ( retryCallback ) ; // protect again upload() erroring much later after read stream error
2017-10-10 20:23:04 -07:00
++ retryCount ;
2017-10-10 14:25:03 -07:00
if ( task . operation === 'add' ) {
2019-01-11 11:06:32 -08:00
progressCallback ( { message : ` Adding ${ task . path } ` + ( retryCount > 1 ? ` (Try ${ retryCount } ) ` : '' ) } ) ;
2018-11-16 19:24:56 -08:00
debug ( ` Adding ${ task . path } position ${ task . position } try ${ retryCount } ` ) ;
2019-01-19 10:28:02 -08:00
var stream = createReadStream ( dataLayout . toLocalPath ( './' + task . path ) , backupConfig . key || null ) ;
2018-03-20 16:41:32 -07:00
stream . on ( 'error' , function ( error ) {
2018-11-16 19:24:56 -08:00
debug ( ` read stream error for ${ task . path } : ${ error . message } ` ) ;
2018-03-20 16:41:32 -07:00
retryCallback ( ) ;
} ) ; // ignore error if file disappears
2018-11-27 11:55:53 -08:00
stream . on ( 'progress' , function ( progress ) {
2019-01-14 12:23:03 -08:00
const transferred = Math . round ( progress . transferred / 1024 / 1024 ) , speed = Math . round ( progress . speed / 1024 / 1024 ) ;
if ( ! transferred && ! speed ) return progressCallback ( { message : ` Uploading ${ task . path } ` } ) ; // 0M@0Mbps looks wrong
progressCallback ( { message : ` Uploading ${ task . path } : ${ transferred } M@ ${ speed } Mbps ` } ) ; // 0M@0Mbps looks wrong
2018-11-27 11:55:53 -08:00
} ) ;
2018-03-20 16:41:32 -07:00
api ( backupConfig . provider ) . upload ( backupConfig , backupFilePath , stream , function ( error ) {
2018-11-16 19:24:56 -08:00
debug ( error ? ` Error uploading ${ task . path } try ${ retryCount } : ${ error . message } ` : ` Uploaded ${ task . path } ` ) ;
2018-03-20 16:41:32 -07:00
retryCallback ( error ) ;
} ) ;
2017-10-10 14:25:03 -07:00
}
} , iteratorCallback ) ;
2019-01-14 13:17:51 -08:00
} , concurrency , function ( error ) {
2017-09-26 11:59:45 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
callback ( ) ;
} ) ;
2017-09-22 14:40:37 -07:00
}
2018-12-20 15:11:11 -08:00
// this is not part of 'snapshotting' because we need root access to traverse
2019-01-18 14:50:04 -08:00
function saveFsMetadata ( dataLayout , metadataFile , callback ) {
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof metadataFile , 'string' ) ;
2017-09-22 14:40:37 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-17 09:53:51 -08:00
// contains paths prefixed with './'
let metadata = {
emptyDirs : [ ] ,
execFiles : [ ]
} ;
2017-09-22 14:40:37 -07:00
2019-01-19 10:28:02 -08:00
for ( let lp of dataLayout . localPaths ( ) ) {
2019-01-19 21:45:54 -08:00
var emptyDirs = safe . child _process . execSync ( ` find ${ lp } -type d -empty \n ` , { encoding : 'utf8' } ) ;
2019-01-17 09:53:51 -08:00
if ( emptyDirs === null ) return callback ( safe . error ) ;
2019-01-19 10:28:02 -08:00
if ( emptyDirs . length ) metadata . emptyDirs = metadata . emptyDirs . concat ( emptyDirs . trim ( ) . split ( '\n' ) . map ( ( ed ) => dataLayout . toRemotePath ( ed ) ) ) ;
2019-01-17 09:53:51 -08:00
2019-01-19 21:45:54 -08:00
var execFiles = safe . child _process . execSync ( ` find ${ lp } -type f -executable \n ` , { encoding : 'utf8' } ) ;
2019-01-17 09:53:51 -08:00
if ( execFiles === null ) return callback ( safe . error ) ;
2019-01-19 10:28:02 -08:00
if ( execFiles . length ) metadata . execFiles = metadata . execFiles . concat ( execFiles . trim ( ) . split ( '\n' ) . map ( ( ef ) => dataLayout . toRemotePath ( ef ) ) ) ;
2019-01-17 09:53:51 -08:00
}
2017-10-12 16:02:09 -07:00
2019-01-18 14:50:04 -08:00
if ( ! safe . fs . writeFileSync ( metadataFile , JSON . stringify ( metadata , null , 4 ) ) ) return callback ( safe . error ) ;
2017-10-12 16:02:09 -07:00
2017-09-22 14:40:37 -07:00
callback ( ) ;
}
2019-08-12 21:56:41 -07:00
// the du call in the function below requires root
function checkFreeDiskSpace ( backupConfig , dataLayout , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( backupConfig . provider !== 'filesystem' ) return callback ( ) ;
let used = 0 ;
for ( let localPath of dataLayout . localPaths ( ) ) {
debug ( ` checkFreeDiskSpace: getting disk usage of ${ localPath } ` ) ;
let result = safe . child _process . execSync ( ` du -Dsb ${ localPath } ` , { encoding : 'utf8' } ) ;
if ( ! result ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , safe . error ) ) ;
used += parseInt ( result , 10 ) ;
}
debug ( ` checkFreeDiskSpace: ${ used } bytes ` ) ;
df . file ( backupConfig . backupFolder ) . then ( function ( diskUsage ) {
const needed = used + ( 1024 * 1024 * 1024 ) ; // check if there is atleast 1GB left afterwards
if ( diskUsage . available <= needed ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` Not enough disk space for backup. Needed: ${ prettyBytes ( needed ) } Available: ${ prettyBytes ( diskUsage . available ) } ` ) ) ;
callback ( null ) ;
} ) . catch ( function ( error ) {
callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
} ) ;
}
2018-11-16 11:48:02 -08:00
// this function is called via backupupload (since it needs root to traverse app's directory)
2019-01-18 14:50:04 -08:00
function upload ( backupId , format , dataLayoutString , progressCallback , callback ) {
2017-09-19 20:27:36 -07:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert . strictEqual ( typeof dataLayoutString , 'string' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-19 20:27:36 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
debug ( ` upload: id ${ backupId } format ${ format } dataLayout ${ dataLayoutString } ` ) ;
const dataLayout = DataLayout . fromString ( dataLayoutString ) ;
2017-09-19 20:27:36 -07:00
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2019-08-12 21:56:41 -07:00
checkFreeDiskSpace ( backupConfig , dataLayout , function ( error ) {
if ( error ) return callback ( error ) ;
2018-02-27 19:16:03 -08:00
2019-08-12 21:56:41 -07:00
if ( format === 'tgz' ) {
async . retry ( { times : 5 , interval : 20000 } , function ( retryCallback ) {
retryCallback = once ( retryCallback ) ; // protect again upload() erroring much later after tar stream error
2017-10-10 14:25:03 -07:00
2019-08-12 21:56:41 -07:00
tarPack ( dataLayout , backupConfig . key || null , function ( error , tarStream ) {
if ( error ) return retryCallback ( error ) ;
2018-12-20 11:41:38 -08:00
2019-08-12 21:56:41 -07:00
tarStream . on ( 'progress' , function ( progress ) {
const transferred = Math . round ( progress . transferred / 1024 / 1024 ) , speed = Math . round ( progress . speed / 1024 / 1024 ) ;
if ( ! transferred && ! speed ) return progressCallback ( { message : 'Uploading backup' } ) ; // 0M@0Mbps looks wrong
progressCallback ( { message : ` Uploading backup ${ transferred } M@ ${ speed } Mbps ` } ) ;
} ) ;
tarStream . on ( 'error' , retryCallback ) ; // already returns BackupsError
api ( backupConfig . provider ) . upload ( backupConfig , getBackupFilePath ( backupConfig , backupId , format ) , tarStream , retryCallback ) ;
} ) ;
} , callback ) ;
} else {
async . series ( [
saveFsMetadata . bind ( null , dataLayout , ` ${ dataLayout . localRoot ( ) } /fsmetadata.json ` ) ,
sync . bind ( null , backupConfig , backupId , dataLayout , progressCallback )
] , callback ) ;
}
} ) ;
2017-09-20 09:57:16 -07:00
} ) ;
}
2018-12-20 14:33:29 -08:00
function tarExtract ( inStream , dataLayout , key , callback ) {
2017-09-20 09:57:16 -07:00
assert . strictEqual ( typeof inStream , 'object' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2017-09-20 09:57:16 -07:00
assert ( key === null || typeof key === 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var gunzip = zlib . createGunzip ( { } ) ;
var ps = progressStream ( { time : 10000 } ) ; // display a progress every 10 seconds
2018-12-20 14:33:29 -08:00
var extract = tar . extract ( '/' , {
map : function ( header ) {
2019-01-19 10:28:02 -08:00
header . name = dataLayout . toLocalPath ( header . name ) ;
2018-12-20 14:33:29 -08:00
return header ;
}
} ) ;
2017-09-20 09:57:16 -07:00
2018-12-20 11:41:38 -08:00
const emitError = once ( ( error ) => ps . emit ( 'error' , error ) ) ;
2017-09-28 14:26:39 -07:00
inStream . on ( 'error' , function ( error ) {
debug ( 'tarExtract: input stream error.' , error ) ;
2018-12-20 11:41:38 -08:00
emitError ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-28 14:26:39 -07:00
} ) ;
2017-09-20 09:57:16 -07:00
gunzip . on ( 'error' , function ( error ) {
2017-09-28 14:26:39 -07:00
debug ( 'tarExtract: gunzip stream error.' , error ) ;
2018-12-20 11:41:38 -08:00
emitError ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
extract . on ( 'error' , function ( error ) {
2017-09-28 14:26:39 -07:00
debug ( 'tarExtract: extract stream error.' , error ) ;
2018-12-20 11:41:38 -08:00
emitError ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
extract . on ( 'finish' , function ( ) {
2017-09-28 14:26:39 -07:00
debug ( 'tarExtract: done.' ) ;
2018-12-22 21:08:07 -08:00
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not extract
ps . emit ( 'done' ) ;
2017-09-20 09:57:16 -07:00
} ) ;
if ( key !== null ) {
var decrypt = crypto . createDecipher ( 'aes-256-cbc' , key ) ;
decrypt . on ( 'error' , function ( error ) {
2017-09-28 14:26:39 -07:00
debug ( 'tarExtract: decrypt stream error.' , error ) ;
2018-12-20 11:41:38 -08:00
emitError ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` Failed to decrypt: ${ error . message } ` ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
inStream . pipe ( ps ) . pipe ( decrypt ) . pipe ( gunzip ) . pipe ( extract ) ;
} else {
inStream . pipe ( ps ) . pipe ( gunzip ) . pipe ( extract ) ;
}
2018-11-27 11:55:53 -08:00
2018-12-20 11:41:38 -08:00
callback ( null , ps ) ;
2017-09-20 09:57:16 -07:00
}
2019-01-18 14:50:04 -08:00
function restoreFsMetadata ( dataLayout , metadataFile , callback ) {
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof metadataFile , 'string' ) ;
2017-09-23 14:27:35 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
debug ( ` Recreating empty directories in ${ dataLayout . toString ( ) } ` ) ;
2017-09-23 14:27:35 -07:00
2019-01-18 14:50:04 -08:00
var metadataJson = safe . fs . readFileSync ( metadataFile , 'utf8' ) ;
2019-03-17 16:09:21 -07:00
if ( metadataJson === null ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , 'Error loading fsmetadata.json:' + safe . error . message ) ) ;
2017-11-22 22:37:27 -08:00
var metadata = safe . JSON . parse ( metadataJson ) ;
2019-03-17 16:09:21 -07:00
if ( metadata === null ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , 'Error parsing fsmetadata.json:' + safe . error . message ) ) ;
2017-09-23 14:27:35 -07:00
2017-10-12 16:02:09 -07:00
async . eachSeries ( metadata . emptyDirs , function createPath ( emptyDir , iteratorDone ) {
2019-01-19 10:28:02 -08:00
mkdirp ( dataLayout . toLocalPath ( emptyDir ) , iteratorDone ) ;
2017-10-10 20:23:04 -07:00
} , function ( error ) {
2017-10-12 16:02:09 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` unable to create path: ${ error . message } ` ) ) ;
2017-10-10 20:23:04 -07:00
2017-10-12 16:02:09 -07:00
async . eachSeries ( metadata . execFiles , function createPath ( execFile , iteratorDone ) {
2019-01-19 10:28:02 -08:00
fs . chmod ( dataLayout . toLocalPath ( execFile ) , parseInt ( '0755' , 8 ) , iteratorDone ) ;
2017-10-12 16:02:09 -07:00
} , function ( error ) {
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , ` unable to chmod: ${ error . message } ` ) ) ;
callback ( ) ;
} ) ;
2017-10-10 20:23:04 -07:00
} ) ;
2017-09-23 14:27:35 -07:00
}
2019-01-18 14:50:04 -08:00
function downloadDir ( backupConfig , backupFilePath , dataLayout , progressCallback , callback ) {
2018-07-27 15:22:54 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backupFilePath , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2018-11-27 11:55:53 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2018-07-27 15:22:54 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
debug ( ` downloadDir: ${ backupFilePath } to ${ dataLayout . toString ( ) } ` ) ;
2018-07-27 15:22:54 -07:00
function downloadFile ( entry , callback ) {
2018-07-29 21:01:20 -07:00
let relativePath = path . relative ( backupFilePath , entry . fullPath ) ;
if ( backupConfig . key ) {
relativePath = decryptFilePath ( relativePath , backupConfig . key ) ;
if ( ! relativePath ) return callback ( new BackupsError ( BackupsError . BAD _STATE , 'Unable to decrypt file' ) ) ;
}
2019-01-19 10:28:02 -08:00
const destFilePath = dataLayout . toLocalPath ( './' + relativePath ) ;
2018-07-27 15:22:54 -07:00
mkdirp ( path . dirname ( destFilePath ) , function ( error ) {
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2019-01-14 11:48:47 -08:00
async . retry ( { times : 5 , interval : 20000 } , function ( retryCallback ) {
let destStream = createWriteStream ( destFilePath , backupConfig . key || null ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
2019-01-15 12:01:02 -08:00
let closeAndRetry = once ( ( error ) => {
if ( error ) progressCallback ( { message : ` Download ${ entry . fullPath } errored: ${ error . message } ` } ) ;
else progressCallback ( { message : ` Download ${ entry . fullPath } finished ` } ) ;
destStream . destroy ( ) ;
retryCallback ( error ) ;
} ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
api ( backupConfig . provider ) . download ( backupConfig , entry . fullPath , function ( error , sourceStream ) {
if ( error ) return closeAndRetry ( error ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
sourceStream . on ( 'error' , closeAndRetry ) ;
destStream . on ( 'error' , closeAndRetry ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
progressCallback ( { message : ` Downloading ${ entry . fullPath } to ${ destFilePath } ` } ) ;
sourceStream . pipe ( destStream , { end : true } ) . on ( 'finish' , closeAndRetry ) ;
} ) ;
} , callback ) ;
2018-07-27 15:22:54 -07:00
} ) ;
}
api ( backupConfig . provider ) . listDir ( backupConfig , backupFilePath , 1000 , function ( entries , done ) {
2019-01-09 15:01:51 -08:00
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
2018-12-20 14:33:29 -08:00
const concurrency = backupConfig . downloadConcurrency || ( backupConfig . provider === 's3' ? 30 : 10 ) ;
2019-01-14 13:17:51 -08:00
async . eachLimit ( entries , concurrency , downloadFile , done ) ;
2018-07-27 15:22:54 -07:00
} , callback ) ;
}
2018-12-20 14:33:29 -08:00
function download ( backupConfig , backupId , format , dataLayout , progressCallback , callback ) {
2017-11-22 10:29:40 -08:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2017-09-20 09:57:16 -07:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2018-11-27 11:55:53 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-20 09:57:16 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
debug ( ` download - Downloading ${ backupId } of format ${ format } to ${ dataLayout . toString ( ) } ` ) ;
2017-09-20 09:57:16 -07:00
2019-01-14 11:36:11 -08:00
const backupFilePath = getBackupFilePath ( backupConfig , backupId , format ) ;
2017-11-22 10:29:40 -08:00
if ( format === 'tgz' ) {
2019-01-18 14:10:28 -08:00
async . retry ( { times : 5 , interval : 20000 } , function ( retryCallback ) {
api ( backupConfig . provider ) . download ( backupConfig , backupFilePath , function ( error , sourceStream ) {
if ( error ) return retryCallback ( error ) ;
2017-09-20 09:57:16 -07:00
2018-12-20 14:33:29 -08:00
tarExtract ( sourceStream , dataLayout , backupConfig . key || null , function ( error , ps ) {
2019-01-14 11:36:11 -08:00
if ( error ) return retryCallback ( error ) ;
2018-12-20 11:41:38 -08:00
2019-01-14 11:36:11 -08:00
ps . on ( 'progress' , function ( progress ) {
const transferred = Math . round ( progress . transferred / 1024 / 1024 ) , speed = Math . round ( progress . speed / 1024 / 1024 ) ;
if ( ! transferred && ! speed ) return progressCallback ( { message : 'Downloading' } ) ; // 0M@0Mbps looks wrong
progressCallback ( { message : ` Downloading ${ transferred } M@ ${ speed } Mbps ` } ) ;
} ) ;
ps . on ( 'error' , retryCallback ) ;
ps . on ( 'done' , retryCallback ) ;
2018-12-20 11:41:38 -08:00
} ) ;
2019-01-18 15:00:52 -08:00
} ) ;
} , callback ) ;
2017-11-22 10:29:40 -08:00
} else {
2019-01-18 14:50:04 -08:00
downloadDir ( backupConfig , backupFilePath , dataLayout , progressCallback , function ( error ) {
2017-11-22 10:29:40 -08:00
if ( error ) return callback ( error ) ;
2017-09-26 11:14:56 -07:00
2019-01-19 10:28:02 -08:00
restoreFsMetadata ( dataLayout , ` ${ dataLayout . localRoot ( ) } /fsmetadata.json ` , callback ) ;
2017-11-22 10:29:40 -08:00
} ) ;
}
2017-09-19 20:27:36 -07:00
}
2017-10-10 20:23:04 -07:00
2018-11-27 11:55:53 -08:00
function restore ( backupConfig , backupId , progressCallback , callback ) {
2017-11-22 10:58:07 -08:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2018-11-27 11:55:53 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-11-22 10:58:07 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
const dataLayout = new DataLayout ( paths . BOX _DATA _DIR , [ ] ) ;
2018-12-20 14:33:29 -08:00
download ( backupConfig , backupId , backupConfig . format , dataLayout , progressCallback , function ( error ) {
2017-11-22 10:58:07 -08:00
if ( error ) return callback ( error ) ;
2018-09-26 12:39:33 -07:00
debug ( 'restore: download completed, importing database' ) ;
2019-01-19 10:28:02 -08:00
database . importFromFile ( ` ${ dataLayout . localRoot ( ) } /box.mysqldump ` , function ( error ) {
2017-11-22 10:58:07 -08:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2018-09-26 12:39:33 -07:00
debug ( 'restore: database imported' ) ;
2019-07-27 19:09:09 -07:00
settings . initCache ( callback ) ;
2017-11-22 10:58:07 -08:00
} ) ;
2017-09-19 20:27:36 -07:00
} ) ;
}
2018-11-27 11:55:53 -08:00
function restoreApp ( app , addonsToRestore , restoreConfig , progressCallback , callback ) {
2017-10-10 20:23:04 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof addonsToRestore , 'object' ) ;
2017-11-16 14:47:05 -08:00
assert . strictEqual ( typeof restoreConfig , 'object' ) ;
2018-11-30 09:58:00 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-10-10 20:23:04 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-18 14:50:04 -08:00
const appDataDir = safe . fs . realpathSync ( path . join ( paths . APPS _DATA _DIR , app . id ) ) ;
if ( ! appDataDir ) return callback ( safe . error ) ;
const dataLayout = new DataLayout ( appDataDir , app . dataDir ? [ { localDir : app . dataDir , remoteDir : 'data' } ] : [ ] ) ;
2017-10-10 20:23:04 -07:00
var startTime = new Date ( ) ;
2017-11-22 10:29:40 -08:00
settings . getBackupConfig ( function ( error , backupConfig ) {
2017-10-10 20:23:04 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
async . series ( [
2018-12-20 14:33:29 -08:00
download . bind ( null , backupConfig , restoreConfig . backupId , restoreConfig . backupFormat , dataLayout , progressCallback ) ,
2017-10-10 20:23:04 -07:00
addons . restoreAddons . bind ( null , app , addonsToRestore )
] , function ( error ) {
debug ( 'restoreApp: time: %s' , ( new Date ( ) - startTime ) / 1000 ) ;
callback ( error ) ;
} ) ;
} ) ;
}
2018-12-20 14:33:29 -08:00
function runBackupUpload ( backupId , format , dataLayout , progressCallback , callback ) {
2017-09-19 08:19:01 -07:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2017-09-27 17:34:49 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-19 08:19:01 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-26 15:21:48 -08:00
let result = '' ;
2019-01-18 14:50:04 -08:00
shell . sudo ( ` backup- ${ backupId } ` , [ BACKUP _UPLOAD _CMD , backupId , format , dataLayout . toString ( ) ] , { preserveEnv : true , ipc : true } , function ( error ) {
2017-09-19 08:19:01 -07:00
if ( error && ( error . code === null /* signal */ || ( error . code !== 0 && error . code !== 50 ) ) ) { // backuptask crashed
2017-09-27 12:14:22 -07:00
return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , 'Backuptask crashed' ) ) ;
2017-09-19 08:19:01 -07:00
} else if ( error && error . code === 50 ) { // exited with error
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , result ) ) ;
}
callback ( ) ;
2018-11-27 10:24:54 -08:00
} ) . on ( 'message' , function ( message ) {
if ( ! message . result ) return progressCallback ( message ) ;
2019-08-12 21:56:41 -07:00
debug ( ` runBackupUpload: result - ${ JSON . stringify ( message ) } ` ) ;
2018-11-26 15:21:48 -08:00
result = message . result ;
} ) ;
2017-09-19 08:19:01 -07:00
}
2017-09-17 23:45:06 -07:00
function getSnapshotInfo ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2016-04-20 19:40:58 -07:00
2017-09-17 23:45:06 -07:00
var contents = safe . fs . readFileSync ( paths . SNAPSHOT _INFO _FILE , 'utf8' ) ;
var info = safe . JSON . parse ( contents ) ;
if ( ! info ) return { } ;
return info [ id ] || { } ;
}
2017-06-01 14:08:51 -07:00
2017-09-17 23:45:06 -07:00
function setSnapshotInfo ( id , info , callback ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof info , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-01 14:08:51 -07:00
2017-09-17 23:45:06 -07:00
var contents = safe . fs . readFileSync ( paths . SNAPSHOT _INFO _FILE , 'utf8' ) ;
var data = safe . JSON . parse ( contents ) || { } ;
if ( info ) data [ id ] = info ; else delete data [ id ] ;
if ( ! safe . fs . writeFileSync ( paths . SNAPSHOT _INFO _FILE , JSON . stringify ( data , null , 4 ) , 'utf8' ) ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , safe . error . message ) ) ;
2017-06-01 14:08:51 -07:00
2017-09-17 23:45:06 -07:00
callback ( ) ;
2015-09-21 14:14:21 -07:00
}
2016-04-10 19:17:44 -07:00
2018-11-27 11:03:58 -08:00
function snapshotBox ( progressCallback , callback ) {
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-17 21:30:16 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-27 11:03:58 -08:00
progressCallback ( { message : 'Snapshotting box' } ) ;
2017-10-10 20:23:04 -07:00
2017-11-24 15:29:23 -08:00
database . exportToFile ( ` ${ paths . BOX _DATA _DIR } /box.mysqldump ` , function ( error ) {
2017-09-17 21:30:16 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
return callback ( ) ;
} ) ;
}
2018-11-27 10:24:54 -08:00
function uploadBoxSnapshot ( backupConfig , progressCallback , callback ) {
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
var startTime = new Date ( ) ;
2018-11-27 11:03:58 -08:00
snapshotBox ( progressCallback , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( error ) ;
2019-01-13 15:17:02 -08:00
const boxDataDir = safe . fs . realpathSync ( paths . BOX _DATA _DIR ) ;
if ( ! boxDataDir ) return callback ( safe . error ) ;
2019-01-18 14:50:04 -08:00
const dataLayout = new DataLayout ( boxDataDir , [ ] ) ;
2018-12-20 14:33:29 -08:00
runBackupUpload ( 'snapshot/box' , backupConfig . format , dataLayout , progressCallback , function ( error ) {
2017-09-19 08:19:01 -07:00
if ( error ) return callback ( error ) ;
debug ( 'uploadBoxSnapshot: time: %s secs' , ( new Date ( ) - startTime ) / 1000 ) ;
2017-09-17 23:45:06 -07:00
2017-10-11 13:25:10 -07:00
setSnapshotInfo ( 'box' , { timestamp : new Date ( ) . toISOString ( ) , format : backupConfig . format } , callback ) ;
2017-09-17 23:45:06 -07:00
} ) ;
} ) ;
}
2019-04-14 11:33:09 -07:00
function rotateBoxBackup ( backupConfig , tag , appBackupIds , progressCallback , callback ) {
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2019-04-14 11:33:09 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
2017-04-21 10:31:43 +02:00
assert ( Array . isArray ( appBackupIds ) ) ;
2018-11-27 10:51:35 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-17 21:30:16 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-04-10 19:17:44 -07:00
2017-09-17 23:45:06 -07:00
var snapshotInfo = getSnapshotInfo ( 'box' ) ;
if ( ! snapshotInfo ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , 'Snapshot info missing or corrupt' ) ) ;
2016-09-16 10:58:34 +02:00
2019-04-14 11:33:09 -07:00
const snapshotTime = snapshotInfo . timestamp . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ; // add this to filename to make it unique, so it's easy to download them
2019-07-25 14:40:52 -07:00
const backupId = util . format ( '%s/box_%s_v%s' , tag , snapshotTime , constants . VERSION ) ;
2017-10-11 13:25:10 -07:00
const format = backupConfig . format ;
2016-04-10 19:17:44 -07:00
2018-11-27 10:51:35 -08:00
debug ( ` Rotating box backup to id ${ backupId } ` ) ;
2017-09-22 14:40:37 -07:00
2019-07-25 14:40:52 -07:00
backupdb . add ( backupId , { version : constants . VERSION , type : backupdb . BACKUP _TYPE _BOX , dependsOn : appBackupIds , manifest : null , format : format } , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2017-09-17 21:30:16 -07:00
2017-10-10 23:42:24 -07:00
var copy = api ( backupConfig . provider ) . copy ( backupConfig , getBackupFilePath ( backupConfig , 'snapshot/box' , format ) , getBackupFilePath ( backupConfig , backupId , format ) ) ;
2018-11-27 10:51:35 -08:00
copy . on ( 'progress' , ( message ) => progressCallback ( { message } ) ) ;
2017-10-10 23:42:24 -07:00
copy . on ( 'done' , function ( copyBackupError ) {
2017-09-17 23:45:06 -07:00
const state = copyBackupError ? backupdb . BACKUP _STATE _ERROR : backupdb . BACKUP _STATE _NORMAL ;
2016-04-10 19:17:44 -07:00
2017-09-17 23:45:06 -07:00
backupdb . update ( backupId , { state : state } , function ( error ) {
2017-10-11 13:57:05 -07:00
if ( copyBackupError ) return callback ( copyBackupError ) ;
2017-05-28 17:02:36 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2016-04-10 19:17:44 -07:00
2018-11-27 10:51:35 -08:00
debug ( ` Rotated box backup successfully as id ${ backupId } ` ) ;
2017-09-22 14:40:37 -07:00
2019-05-08 15:28:22 -07:00
callback ( null , backupId ) ;
2016-04-10 19:17:44 -07:00
} ) ;
} ) ;
} ) ;
}
2019-04-14 11:33:09 -07:00
function backupBoxWithAppBackupIds ( appBackupIds , tag , progressCallback , callback ) {
2017-09-17 23:45:06 -07:00
assert ( Array . isArray ( appBackupIds ) ) ;
2019-04-14 11:33:09 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2018-11-27 10:24:54 -08:00
uploadBoxSnapshot ( backupConfig , progressCallback , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( error ) ;
2019-04-14 11:33:09 -07:00
rotateBoxBackup ( backupConfig , tag , appBackupIds , progressCallback , callback ) ;
2017-09-17 23:45:06 -07:00
} ) ;
} ) ;
}
2016-04-10 19:17:44 -07:00
function canBackupApp ( app ) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return ( app . installationState === appdb . ISTATE _INSTALLED && app . health === appdb . HEALTH _HEALTHY ) ||
app . installationState === appdb . ISTATE _PENDING _CONFIGURE ||
app . installationState === appdb . ISTATE _PENDING _BACKUP || // called from apptask
app . installationState === appdb . ISTATE _PENDING _UPDATE ; // called from apptask
}
2018-11-27 11:03:58 -08:00
function snapshotApp ( app , progressCallback , callback ) {
2016-04-10 19:17:44 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2018-11-27 11:03:58 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-04-10 19:17:44 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-14 23:20:32 -08:00
progressCallback ( { message : ` Snapshotting app ${ app . fqdn } ` } ) ;
2017-10-10 20:23:04 -07:00
2017-11-16 11:22:09 -08:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( apps . getAppConfig ( app ) ) ) ) {
2017-04-21 14:07:10 -07:00
return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , 'Error creating config.json: ' + safe . error . message ) ) ;
}
2016-04-10 21:41:53 -07:00
2017-11-19 17:53:17 -08:00
addons . backupAddons ( app , app . manifest . addons , function ( error ) {
2017-04-21 14:07:10 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . EXTERNAL _ERROR , error . message ) ) ;
2016-09-16 11:21:08 +02:00
2017-09-17 23:45:06 -07:00
return callback ( null ) ;
} ) ;
}
2017-09-17 21:30:16 -07:00
2019-04-14 11:33:09 -07:00
function rotateAppBackup ( backupConfig , app , tag , options , progressCallback , callback ) {
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
2019-04-14 11:33:09 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
2019-04-13 17:09:15 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-11-27 10:51:35 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-17 21:30:16 -07:00
2017-09-17 23:45:06 -07:00
var snapshotInfo = getSnapshotInfo ( app . id ) ;
if ( ! snapshotInfo ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , 'Snapshot info missing or corrupt' ) ) ;
2017-04-21 14:07:10 -07:00
2017-11-16 11:22:09 -08:00
var manifest = snapshotInfo . restoreConfig ? snapshotInfo . restoreConfig . manifest : snapshotInfo . manifest ; // compat
2019-04-14 11:33:09 -07:00
const snapshotTime = snapshotInfo . timestamp . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ; // add this for unique filename which helps when downloading them
const backupId = util . format ( '%s/app_%s_%s_v%s' , tag , app . id , snapshotTime , manifest . version ) ;
2017-09-28 10:22:10 -07:00
const format = backupConfig . format ;
2016-09-16 11:21:08 +02:00
2018-11-27 10:51:35 -08:00
debug ( ` Rotating app backup of ${ app . id } to id ${ backupId } ` ) ;
2017-09-22 14:40:37 -07:00
2019-04-13 17:14:04 -07:00
backupdb . add ( backupId , { version : manifest . version , type : backupdb . BACKUP _TYPE _APP , dependsOn : [ ] , manifest : manifest , format : format } , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2017-04-21 14:07:10 -07:00
2017-10-10 23:42:24 -07:00
var copy = api ( backupConfig . provider ) . copy ( backupConfig , getBackupFilePath ( backupConfig , ` snapshot/app_ ${ app . id } ` , format ) , getBackupFilePath ( backupConfig , backupId , format ) ) ;
2018-11-27 10:51:35 -08:00
copy . on ( 'progress' , ( message ) => progressCallback ( { message } ) ) ;
2017-10-10 23:42:24 -07:00
copy . on ( 'done' , function ( copyBackupError ) {
2017-09-17 23:45:06 -07:00
const state = copyBackupError ? backupdb . BACKUP _STATE _ERROR : backupdb . BACKUP _STATE _NORMAL ;
2017-05-28 17:02:36 -07:00
2019-04-13 17:09:15 -07:00
backupdb . update ( backupId , { preserveSecs : options . preserveSecs || 0 , state : state } , function ( error ) {
2017-10-11 13:57:05 -07:00
if ( copyBackupError ) return callback ( copyBackupError ) ;
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2018-11-27 10:51:35 -08:00
debug ( ` Rotated app backup of ${ app . id } successfully to id ${ backupId } ` ) ;
2017-10-11 13:57:05 -07:00
2017-11-16 14:47:05 -08:00
callback ( null , backupId ) ;
2017-04-21 14:07:10 -07:00
} ) ;
2016-04-10 19:17:44 -07:00
} ) ;
} ) ;
}
2018-11-27 10:24:54 -08:00
function uploadAppSnapshot ( backupConfig , app , progressCallback , callback ) {
2017-09-17 23:45:06 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-04-10 19:17:44 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-17 23:45:06 -07:00
if ( ! canBackupApp ( app ) ) return callback ( ) ; // nothing to do
2016-04-10 19:17:44 -07:00
2017-09-17 23:45:06 -07:00
var startTime = new Date ( ) ;
2018-11-27 11:03:58 -08:00
snapshotApp ( app , progressCallback , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( error ) ;
2019-01-13 15:17:02 -08:00
const backupId = util . format ( 'snapshot/app_%s' , app . id ) ;
const appDataDir = safe . fs . realpathSync ( path . join ( paths . APPS _DATA _DIR , app . id ) ) ;
if ( ! appDataDir ) return callback ( safe . error ) ;
2019-01-18 14:50:04 -08:00
const dataLayout = new DataLayout ( appDataDir , app . dataDir ? [ { localDir : app . dataDir , remoteDir : 'data' } ] : [ ] ) ;
2018-12-20 14:33:29 -08:00
runBackupUpload ( backupId , backupConfig . format , dataLayout , progressCallback , function ( error ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( error ) ;
debugApp ( app , 'uploadAppSnapshot: %s done time: %s secs' , backupId , ( new Date ( ) - startTime ) / 1000 ) ;
2017-11-19 17:53:17 -08:00
setSnapshotInfo ( app . id , { timestamp : new Date ( ) . toISOString ( ) , manifest : app . manifest , format : backupConfig . format } , callback ) ;
2017-09-17 23:45:06 -07:00
} ) ;
2016-04-10 19:17:44 -07:00
} ) ;
}
2019-04-14 11:33:09 -07:00
function backupAppWithTag ( app , tag , options , progressCallback , callback ) {
2016-04-10 19:17:44 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-04-14 11:33:09 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
2019-04-13 17:09:15 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-04-10 19:17:44 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-17 23:45:06 -07:00
if ( ! canBackupApp ( app ) ) return callback ( ) ; // nothing to do
2016-04-10 19:17:44 -07:00
2017-09-17 18:50:26 -07:00
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2016-04-10 19:17:44 -07:00
2018-11-27 10:24:54 -08:00
uploadAppSnapshot ( backupConfig , app , progressCallback , function ( error ) {
2017-09-17 18:50:26 -07:00
if ( error ) return callback ( error ) ;
2016-04-10 19:17:44 -07:00
2019-04-14 11:33:09 -07:00
rotateAppBackup ( backupConfig , app , tag , options , progressCallback , callback ) ;
2016-04-10 19:17:44 -07:00
} ) ;
} ) ;
}
2019-04-13 17:09:15 -07:00
function backupApp ( app , options , progressCallback , callback ) {
2017-09-28 11:12:12 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-04-13 17:09:15 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-11-27 10:24:54 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-09-28 11:12:12 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-04-14 11:33:09 -07:00
const tag = ( new Date ( ) ) . toISOString ( ) . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ;
2017-09-28 11:12:12 -07:00
2019-04-14 11:33:09 -07:00
debug ( ` backupApp - Backing up ${ app . fqdn } with tag ${ tag } ` ) ;
2017-09-28 11:12:12 -07:00
2019-04-14 11:33:09 -07:00
backupAppWithTag ( app , tag , options , progressCallback , callback ) ;
2017-09-28 11:12:12 -07:00
}
2018-11-27 11:42:44 -08:00
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
2018-11-29 15:16:31 -08:00
function backupBoxAndApps ( progressCallback , callback ) {
2018-11-27 11:42:44 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2016-04-10 22:24:01 -07:00
2019-04-14 11:33:09 -07:00
const tag = ( new Date ( ) ) . toISOString ( ) . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ;
2017-01-04 19:41:33 -08:00
2016-04-10 22:24:01 -07:00
apps . getAll ( function ( error , allApps ) {
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2018-11-27 11:42:44 -08:00
let percent = 1 ;
let step = 100 / ( allApps . length + 2 ) ;
2016-04-10 22:24:01 -07:00
async . mapSeries ( allApps , function iterator ( app , iteratorCallback ) {
2018-11-27 11:42:44 -08:00
progressCallback ( { percent : percent , message : ` Backing up ${ app . fqdn } ` } ) ;
percent += step ;
2016-04-10 22:24:01 -07:00
2017-08-16 14:12:07 -07:00
if ( ! app . enableBackup ) {
2018-11-27 11:42:44 -08:00
debug ( ` Skipped backup ${ app . fqdn } ` ) ;
2017-11-16 14:47:05 -08:00
return iteratorCallback ( null , null ) ; // nothing to backup
2017-08-16 14:12:07 -07:00
}
2019-04-14 11:33:09 -07:00
backupAppWithTag ( app , tag , { /* options */ } , ( progress ) => progressCallback ( { percent : percent , message : progress . message } ) , function ( error , backupId ) {
2016-04-10 22:24:01 -07:00
if ( error && error . reason !== BackupsError . BAD _STATE ) {
debugApp ( app , 'Unable to backup' , error ) ;
return iteratorCallback ( error ) ;
}
2018-11-27 11:42:44 -08:00
debugApp ( app , 'Backed up' ) ;
2016-04-10 22:24:01 -07:00
iteratorCallback ( null , backupId || null ) ; // clear backupId if is in BAD_STATE and never backed up
} ) ;
} , function appsBackedUp ( error , backupIds ) {
2018-11-27 11:42:44 -08:00
if ( error ) return callback ( error ) ;
2016-04-10 22:24:01 -07:00
backupIds = backupIds . filter ( function ( id ) { return id !== null ; } ) ; // remove apps in bad state that were never backed up
2018-11-27 11:42:44 -08:00
progressCallback ( { percent : percent , message : 'Backing up system data' } ) ;
percent += step ;
2016-04-10 22:24:01 -07:00
2019-04-14 11:33:09 -07:00
backupBoxWithAppBackupIds ( backupIds , tag , ( progress ) => progressCallback ( { percent : percent , message : progress . message } ) , callback ) ;
2016-04-10 22:24:01 -07:00
} ) ;
} ) ;
}
2018-12-09 03:20:00 -08:00
function startBackupTask ( auditSource , callback ) {
let error = locker . lock ( locker . OP _FULL _BACKUP ) ;
2019-01-17 10:58:34 -08:00
if ( error ) return callback ( new BackupsError ( BackupsError . BAD _STATE , ` Cannot backup now: ${ error . message } ` ) ) ;
2018-12-09 03:20:00 -08:00
2019-01-10 16:02:10 -08:00
let task = tasks . startTask ( tasks . TASK _BACKUP , [ ] ) ;
2018-12-09 03:20:00 -08:00
task . on ( 'error' , ( error ) => callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ) ;
task . on ( 'start' , ( taskId ) => {
eventlog . add ( eventlog . ACTION _BACKUP _START , auditSource , { taskId } ) ;
callback ( null , taskId ) ;
} ) ;
task . on ( 'finish' , ( error , result ) => {
locker . unlock ( locker . OP _FULL _BACKUP ) ;
2019-03-04 17:52:31 -08:00
const errorMessage = error ? error . message : '' ;
2018-12-09 03:20:00 -08:00
2019-03-04 17:52:31 -08:00
eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId : task . id , errorMessage : errorMessage , backupId : result } ) ;
2018-12-09 03:20:00 -08:00
} ) ;
}
2016-06-02 18:51:50 -07:00
function ensureBackup ( auditSource , callback ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
2016-04-10 22:24:01 -07:00
2017-01-26 12:46:41 -08:00
debug ( 'ensureBackup: %j' , auditSource ) ;
2017-05-30 14:09:55 -07:00
getByStatePaged ( backupdb . BACKUP _STATE _NORMAL , 1 , 1 , function ( error , backups ) {
2016-04-10 22:24:01 -07:00
if ( error ) {
debug ( 'Unable to list backups' , error ) ;
2018-09-04 10:48:54 -07:00
return callback ( error ) ;
2016-04-10 22:24:01 -07:00
}
2018-08-13 22:31:35 -07:00
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( error ) ;
2016-04-10 22:24:01 -07:00
2018-08-13 22:31:35 -07:00
if ( backups . length !== 0 && ( new Date ( ) - new Date ( backups [ 0 ] . creationTime ) < ( backupConfig . intervalSecs - 3600 ) * 1000 ) ) { // adjust 1 hour
debug ( 'Previous backup was %j, no need to backup now' , backups [ 0 ] ) ;
return callback ( null ) ;
}
2018-12-09 03:20:00 -08:00
startBackupTask ( auditSource , callback ) ;
2018-08-13 22:31:35 -07:00
} ) ;
2016-04-10 22:24:01 -07:00
} ) ;
}
2016-04-10 19:17:44 -07:00
2017-10-03 00:36:09 -07:00
function cleanupBackup ( backupConfig , backup , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backup , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var backupFilePath = getBackupFilePath ( backupConfig , backup . id , backup . format ) ;
2017-10-10 20:23:04 -07:00
function done ( error ) {
2017-10-03 00:36:09 -07:00
if ( error ) {
debug ( 'cleanupBackup: error removing backup %j : %s' , backup , error . message ) ;
2018-01-15 20:08:55 -08:00
return callback ( ) ;
2017-10-03 00:36:09 -07:00
}
// prune empty directory if possible
api ( backupConfig . provider ) . remove ( backupConfig , path . dirname ( backupFilePath ) , function ( error ) {
if ( error ) debug ( 'cleanupBackup: unable to prune backup directory %s : %s' , path . dirname ( backupFilePath ) , error . message ) ;
backupdb . del ( backup . id , function ( error ) {
if ( error ) debug ( 'cleanupBackup: error removing from database' , error ) ;
else debug ( 'cleanupBackup: removed %s' , backup . id ) ;
callback ( ) ;
} ) ;
} ) ;
2017-10-10 20:23:04 -07:00
}
if ( backup . format === 'tgz' ) {
api ( backupConfig . provider ) . remove ( backupConfig , backupFilePath , done ) ;
} else {
var events = api ( backupConfig . provider ) . removeDir ( backupConfig , backupFilePath ) ;
events . on ( 'progress' , function ( detail ) { debug ( ` cleanupBackup: ${ detail } ` ) ; } ) ;
events . on ( 'done' , done ) ;
}
2017-10-03 00:36:09 -07:00
}
2017-05-30 13:18:58 -07:00
function cleanupAppBackups ( backupConfig , referencedAppBackups , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert ( Array . isArray ( referencedAppBackups ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-23 11:34:46 -07:00
2017-05-30 13:18:58 -07:00
const now = new Date ( ) ;
2019-01-11 12:48:40 -08:00
let removedAppBackups = [ ] ;
2016-10-10 15:04:28 +02:00
2017-06-01 10:39:07 -07:00
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb . getByTypePaged ( backupdb . BACKUP _TYPE _APP , 1 , 1000 , function ( error , appBackups ) {
2017-05-30 13:18:58 -07:00
if ( error ) return callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ;
2017-04-24 13:41:23 +02:00
2019-01-11 12:48:40 -08:00
async . eachSeries ( appBackups , function iterator ( appBackup , iteratorDone ) {
if ( referencedAppBackups . indexOf ( appBackup . id ) !== - 1 ) return iteratorDone ( ) ;
2019-04-13 17:09:15 -07:00
if ( ( now - appBackup . creationTime ) < ( appBackup . preserveSecs * 1000 ) ) return iteratorDone ( ) ;
2019-01-11 12:48:40 -08:00
if ( ( now - appBackup . creationTime ) < ( backupConfig . retentionSecs * 1000 ) ) return iteratorDone ( ) ;
2017-04-24 12:01:50 +02:00
2019-01-11 12:48:40 -08:00
debug ( 'cleanupAppBackups: removing %s' , appBackup . id ) ;
2017-04-23 11:34:46 -07:00
2019-01-11 12:48:40 -08:00
removedAppBackups . push ( appBackup . id ) ;
cleanupBackup ( backupConfig , appBackup , iteratorDone ) ;
2017-06-01 10:39:07 -07:00
} , function ( ) {
2017-09-23 11:09:36 -07:00
debug ( 'cleanupAppBackups: done' ) ;
2017-06-01 10:39:07 -07:00
2019-01-11 12:48:40 -08:00
callback ( null , removedAppBackups ) ;
2017-05-30 13:18:58 -07:00
} ) ;
} ) ;
}
2017-04-24 13:41:23 +02:00
2017-10-01 09:29:42 -07:00
function cleanupBoxBackups ( backupConfig , auditSource , callback ) {
2017-05-30 13:18:58 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2017-10-01 09:29:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2017-05-30 13:18:58 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-24 13:50:46 +02:00
2017-05-30 13:18:58 -07:00
const now = new Date ( ) ;
2019-01-11 12:48:40 -08:00
let referencedAppBackups = [ ] , removedBoxBackups = [ ] ;
2017-04-24 13:50:46 +02:00
2017-05-30 14:09:55 -07:00
backupdb . getByTypePaged ( backupdb . BACKUP _TYPE _BOX , 1 , 1000 , function ( error , boxBackups ) {
2017-05-30 13:18:58 -07:00
if ( error ) return callback ( error ) ;
2017-04-24 13:41:23 +02:00
2019-01-11 12:48:40 -08:00
if ( boxBackups . length === 0 ) return callback ( null , { removedBoxBackups , referencedAppBackups } ) ;
2017-04-24 13:41:23 +02:00
2017-05-30 15:15:20 -07:00
// search for the first valid backup
var i ;
for ( i = 0 ; i < boxBackups . length ; i ++ ) {
if ( boxBackups [ i ] . state === backupdb . BACKUP _STATE _NORMAL ) break ;
}
// keep the first valid backup
if ( i !== boxBackups . length ) {
2017-09-30 17:28:35 -07:00
debug ( 'cleanupBoxBackups: preserving box backup %s (%j)' , boxBackups [ i ] . id , boxBackups [ i ] . dependsOn ) ;
2017-05-30 15:15:20 -07:00
referencedAppBackups = boxBackups [ i ] . dependsOn ;
boxBackups . splice ( i , 1 ) ;
2017-06-01 09:38:39 -07:00
} else {
2017-09-23 11:09:36 -07:00
debug ( 'cleanupBoxBackups: no box backup to preserve' ) ;
2017-05-30 15:15:20 -07:00
}
2017-04-24 13:41:23 +02:00
2019-01-11 12:48:40 -08:00
async . eachSeries ( boxBackups , function iterator ( boxBackup , iteratorNext ) {
2017-06-01 10:33:49 -07:00
// TODO: errored backups should probably be cleaned up before retention time, but we will
// have to be careful not to remove any backup currently being created
2019-01-11 12:48:40 -08:00
if ( ( now - boxBackup . creationTime ) < ( backupConfig . retentionSecs * 1000 ) ) {
referencedAppBackups = referencedAppBackups . concat ( boxBackup . dependsOn ) ;
return iteratorNext ( ) ;
2017-10-03 00:36:09 -07:00
}
2017-04-24 13:41:23 +02:00
2019-01-11 12:48:40 -08:00
debug ( 'cleanupBoxBackups: removing %s' , boxBackup . id ) ;
2017-05-30 13:18:58 -07:00
2019-01-11 12:48:40 -08:00
removedBoxBackups . push ( boxBackup . id ) ;
cleanupBackup ( backupConfig , boxBackup , iteratorNext ) ;
2017-05-30 13:18:58 -07:00
} , function ( ) {
2017-09-23 11:09:36 -07:00
debug ( 'cleanupBoxBackups: done' ) ;
2019-01-11 12:48:40 -08:00
callback ( null , { removedBoxBackups , referencedAppBackups } ) ;
2017-05-30 13:18:58 -07:00
} ) ;
} ) ;
}
2017-09-27 20:52:36 -07:00
function cleanupCacheFilesSync ( ) {
var files = safe . fs . readdirSync ( path . join ( paths . BACKUP _INFO _DIR ) ) ;
if ( ! files ) return ;
files . filter ( function ( f ) { return f . endsWith ( '.sync.cache' ) ; } ) . forEach ( function ( f ) {
safe . fs . unlinkSync ( path . join ( paths . BACKUP _INFO _DIR , f ) ) ;
} ) ;
}
2017-09-17 23:45:06 -07:00
// removes the snapshots of apps that have been uninstalled
function cleanupSnapshots ( backupConfig , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var contents = safe . fs . readFileSync ( paths . SNAPSHOT _INFO _FILE , 'utf8' ) ;
var info = safe . JSON . parse ( contents ) ;
if ( ! info ) return callback ( ) ;
delete info . box ;
async . eachSeries ( Object . keys ( info ) , function ( appId , iteratorDone ) {
2017-09-19 20:40:38 -07:00
apps . get ( appId , function ( error /*, app */ ) {
2017-09-17 23:45:06 -07:00
if ( ! error || error . reason !== AppsError . NOT _FOUND ) return iteratorDone ( ) ;
2017-10-10 20:23:04 -07:00
function done ( /* ignoredError */ ) {
2017-09-26 16:31:08 -07:00
safe . fs . unlinkSync ( path . join ( paths . BACKUP _INFO _DIR , ` ${ appId } .sync.cache ` ) ) ;
safe . fs . unlinkSync ( path . join ( paths . BACKUP _INFO _DIR , ` ${ appId } .sync.cache.new ` ) ) ;
2017-09-30 19:39:44 -07:00
setSnapshotInfo ( appId , null , function ( /* ignoredError */ ) {
2017-09-30 20:36:08 -07:00
debug ( 'cleanupSnapshots: cleaned up snapshot of app id %s' , appId ) ;
2017-09-30 19:39:44 -07:00
iteratorDone ( ) ;
} ) ;
2017-10-10 20:23:04 -07:00
}
if ( info [ appId ] . format === 'tgz' ) {
api ( backupConfig . provider ) . remove ( backupConfig , getBackupFilePath ( backupConfig , ` snapshot/app_ ${ appId } ` , info [ appId ] . format ) , done ) ;
} else {
var events = api ( backupConfig . provider ) . removeDir ( backupConfig , getBackupFilePath ( backupConfig , ` snapshot/app_ ${ appId } ` , info [ appId ] . format ) ) ;
events . on ( 'progress' , function ( detail ) { debug ( ` cleanupSnapshots: ${ detail } ` ) ; } ) ;
events . on ( 'done' , done ) ;
}
2017-09-17 23:45:06 -07:00
} ) ;
2017-09-30 20:36:08 -07:00
} , function ( ) {
debug ( 'cleanupSnapshots: done' ) ;
callback ( ) ;
} ) ;
2017-09-17 23:45:06 -07:00
}
2019-01-10 16:00:49 -08:00
function cleanup ( auditSource , progressCallback , callback ) {
2017-10-01 09:29:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-01-10 16:00:49 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-05-30 13:18:58 -07:00
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( error ) ;
if ( backupConfig . retentionSecs < 0 ) {
debug ( 'cleanup: keeping all backups' ) ;
2019-01-11 12:48:40 -08:00
return callback ( null , { } ) ;
2017-05-30 13:18:58 -07:00
}
2019-01-10 16:00:49 -08:00
progressCallback ( { percent : 10 , message : 'Cleaning box backups' } ) ;
2019-01-11 12:48:40 -08:00
cleanupBoxBackups ( backupConfig , auditSource , function ( error , result ) {
2017-05-30 13:18:58 -07:00
if ( error ) return callback ( error ) ;
2019-01-10 16:00:49 -08:00
progressCallback ( { percent : 40 , message : 'Cleaning app backups' } ) ;
2019-01-11 12:48:40 -08:00
cleanupAppBackups ( backupConfig , result . referencedAppBackups , function ( error , removedAppBackups ) {
2017-09-17 23:45:06 -07:00
if ( error ) return callback ( error ) ;
2019-01-10 16:00:49 -08:00
progressCallback ( { percent : 90 , message : 'Cleaning snapshots' } ) ;
2019-01-11 12:48:40 -08:00
cleanupSnapshots ( backupConfig , function ( error ) {
if ( error ) return callback ( error ) ;
callback ( null , { removedBoxBackups : result . removedBoxBackups , removedAppBackups : removedAppBackups } ) ;
} ) ;
2017-09-17 23:45:06 -07:00
} ) ;
2016-10-10 15:04:28 +02:00
} ) ;
} ) ;
2016-10-10 16:10:51 +02:00
}
2017-11-22 10:58:07 -08:00
2019-01-10 16:00:49 -08:00
function startCleanupTask ( auditSource , callback ) {
let task = tasks . startTask ( tasks . TASK _CLEAN _BACKUPS , [ auditSource ] ) ;
task . on ( 'error' , ( error ) => callback ( new BackupsError ( BackupsError . INTERNAL _ERROR , error ) ) ) ;
task . on ( 'start' , ( taskId ) => {
2019-01-11 12:48:40 -08:00
eventlog . add ( eventlog . ACTION _BACKUP _CLEANUP _START , auditSource , { taskId } ) ;
2019-01-10 16:00:49 -08:00
callback ( null , taskId ) ;
} ) ;
2019-01-11 12:48:40 -08:00
task . on ( 'finish' , ( error , result ) => { // result is { removedBoxBackups, removedAppBackups }
2019-01-12 10:08:52 -08:00
eventlog . add ( eventlog . ACTION _BACKUP _CLEANUP _FINISH , auditSource , {
errorMessage : error ? error . message : null ,
removedBoxBackups : result ? result . removedBoxBackups : [ ] ,
removedAppBackups : result ? result . removedAppBackups : [ ]
} ) ;
2019-01-11 12:48:40 -08:00
} ) ;
2019-01-10 16:00:49 -08:00
}
2019-02-28 16:46:30 -08:00
function checkConfiguration ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( error ) ;
let message = '' ;
if ( backupConfig . provider === 'noop' ) {
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.' ;
} else if ( backupConfig . provider === 'filesystem' && ! backupConfig . externalDisk ) {
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.' ;
}
callback ( null , message ) ;
} ) ;
}