2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2016-10-11 11:36:25 +02:00
testConfig : testConfig ,
2019-12-05 11:55:51 -08:00
testProviderConfig : testProviderConfig ,
2016-10-11 11:36:25 +02:00
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 ,
2019-12-05 10:40:32 -08:00
downloadApp : downloadApp ,
2016-04-10 19:17:44 -07:00
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 ,
2020-01-31 13:37:07 -08:00
configureCollectd : configureCollectd ,
2020-05-12 14:00:05 -07:00
generateEncryptionKeysSync : generateEncryptionKeysSync ,
2017-09-20 09:57:16 -07:00
// for testing
_getBackupFilePath : getBackupFilePath ,
2017-10-12 16:02:09 -07:00
_restoreFsMetadata : restoreFsMetadata ,
2020-05-21 14:30:21 -07:00
_saveFsMetadata : saveFsMetadata ,
_applyBackupRetentionPolicy : applyBackupRetentionPolicy
2015-07-20 00:09:47 -07:00
} ;
2016-04-10 19:17:44 -07:00
var addons = require ( './addons.js' ) ,
apps = require ( './apps.js' ) ,
async = require ( 'async' ) ,
assert = require ( 'assert' ) ,
2016-03-07 19:44:38 -08:00
backupdb = require ( './backupdb.js' ) ,
2019-10-22 20:36:20 -07:00
BoxError = require ( './boxerror.js' ) ,
2020-01-31 13:37:07 -08:00
collectd = require ( './collectd.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' ) ,
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' ) ,
2020-01-31 13:37:07 -08:00
ejs = require ( 'ejs' ) ,
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' ) ,
2020-05-14 20:05:27 -07:00
moment = require ( 'moment' ) ,
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' ) ,
2020-05-10 21:40:25 -07:00
TransformStream = require ( 'stream' ) . Transform ,
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' ) ;
2020-01-31 13:37:07 -08:00
const COLLECTD _CONFIG _EJS = fs . readFileSync ( _ _dirname + '/collectd/cloudron-backup.ejs' , { encoding : 'utf8' } ) ;
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-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' ) ;
2020-02-26 09:08:30 -08:00
case 'linode-objectstorage' : return require ( './storage/s3.js' ) ;
2020-04-29 12:47:48 -07:00
case 'ovh-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 ) {
2020-05-14 11:18:41 -07:00
if ( 'password' in newConfig ) {
2020-05-14 23:01:44 +02:00
if ( newConfig . password === constants . SECRET _PLACEHOLDER ) {
2020-05-14 11:18:41 -07:00
delete newConfig . password ;
}
2020-05-14 23:35:03 +02:00
newConfig . encryption = currentConfig . encryption || null ;
} else {
newConfig . encryption = null ;
2020-05-12 14:00:05 -07:00
}
2019-02-09 18:08:10 -08:00
if ( newConfig . provider === currentConfig . provider ) api ( newConfig . provider ) . injectPrivateFields ( newConfig , currentConfig ) ;
}
function removePrivateFields ( backupConfig ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2020-05-12 14:00:05 -07:00
if ( backupConfig . encryption ) {
delete backupConfig . encryption ;
2020-05-14 23:01:44 +02:00
backupConfig . password = constants . SECRET _PLACEHOLDER ;
2020-05-12 14:00:05 -07:00
}
2019-02-09 18:08:10 -08:00
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 ) ;
2019-10-22 20:36:20 -07:00
if ( ! func ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'unknown storage provider' , { field : 'provider' } ) ) ;
2017-09-25 23:49:49 -07:00
2019-10-22 20:36:20 -07:00
if ( backupConfig . format !== 'tgz' && backupConfig . format !== 'rsync' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'unknown format' , { field : '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
2020-05-14 16:45:26 -07:00
if ( backupConfig . intervalSecs < 6 * 60 * 60 ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Interval must be atleast 6 hours' , { field : 'intervalSecs' } ) ) ;
2018-08-13 22:31:35 -07:00
2020-05-12 10:31:51 -07:00
if ( 'password' in backupConfig ) {
if ( typeof backupConfig . password !== 'string' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'password must be a string' , { field : 'password' } ) ) ;
if ( backupConfig . password . length < 8 ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'password must be atleast 8 characters' , { field : 'password' } ) ) ;
2020-05-11 12:01:20 -07:00
}
2020-05-14 16:45:26 -07:00
const policy = backupConfig . retentionPolicy ;
2020-05-14 20:05:27 -07:00
if ( ! policy ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'retentionPolicy is required' , { field : 'retentionPolicy' } ) ) ;
2020-05-14 16:45:26 -07:00
if ( 'keepWithinSecs' in policy && typeof policy . keepWithinSecs !== 'number' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'keepWithinSecs must be a number' , { field : 'retentionPolicy' } ) ) ;
2020-05-14 20:05:27 -07:00
if ( 'keepDaily' in policy && typeof policy . keepDaily !== 'number' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'keepDaily must be a number' , { field : 'retentionPolicy' } ) ) ;
if ( 'keepWeekly' in policy && typeof policy . keepWeekly !== 'number' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'keepWeekly must be a number' , { field : 'retentionPolicy' } ) ) ;
if ( 'keepMonthly' in policy && typeof policy . keepMonthly !== 'number' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'keepMonthly must be a number' , { field : 'retentionPolicy' } ) ) ;
if ( 'keepYearly' in policy && typeof policy . keepYearly !== 'number' ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'keepYearly must be a number' , { field : 'retentionPolicy' } ) ) ;
2020-05-14 16:45:26 -07:00
2019-12-05 11:55:51 -08:00
api ( backupConfig . provider ) . testConfig ( backupConfig , callback ) ;
}
2020-05-12 15:49:43 -07:00
// this skips password check since that policy is only at creation time
function testProviderConfig ( backupConfig , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var func = api ( backupConfig . provider ) ;
if ( ! func ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'unknown storage provider' , { field : 'provider' } ) ) ;
api ( backupConfig . provider ) . testConfig ( backupConfig , callback ) ;
}
2020-05-12 14:00:05 -07:00
function generateEncryptionKeysSync ( password ) {
assert . strictEqual ( typeof password , 'string' ) ;
const aesKeys = crypto . scryptSync ( password , Buffer . from ( 'CLOUDRONSCRYPTSALT' , 'utf8' ) , 128 ) ;
return {
dataKey : aesKeys . slice ( 0 , 32 ) . toString ( 'hex' ) ,
dataHmacKey : aesKeys . slice ( 32 , 64 ) . toString ( 'hex' ) ,
filenameKey : aesKeys . slice ( 64 , 96 ) . toString ( 'hex' ) ,
filenameHmacKey : aesKeys . slice ( 96 ) . toString ( 'hex' )
} ;
}
2019-12-05 11:55:51 -08:00
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 ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2015-11-07 22:06:09 -08:00
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 ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( 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 ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2016-06-13 13:42:25 -07:00
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' ) {
2020-05-12 14:00:05 -07:00
const fileType = backupConfig . encryption ? '.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
}
2020-05-13 09:36:19 -07:00
function encryptFilePath ( filePath , encryption ) {
2018-07-27 06:55:54 -07:00
assert . strictEqual ( typeof filePath , 'string' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
2018-07-27 06:55:54 -07:00
var encryptedParts = filePath . split ( '/' ) . map ( function ( part ) {
2020-05-13 09:36:19 -07:00
let hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . filenameHmacKey , 'hex' ) ) ;
2020-05-12 14:00:05 -07:00
const iv = hmac . update ( part ) . digest ( ) . slice ( 0 , 16 ) ; // iv has to be deterministic, for our sync (copy) logic to work
2020-05-13 09:36:19 -07:00
const cipher = crypto . createCipheriv ( 'aes-256-cbc' , Buffer . from ( encryption . filenameKey , 'hex' ) , iv ) ;
2018-07-27 06:55:54 -07:00
let crypt = cipher . update ( part ) ;
2020-05-10 21:40:25 -07:00
crypt = Buffer . concat ( [ iv , 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 ( '/' ) ;
}
2020-05-13 09:36:19 -07:00
function decryptFilePath ( filePath , encryption ) {
2018-07-27 11:46:42 -07:00
assert . strictEqual ( typeof filePath , 'string' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
2018-07-27 11:46:42 -07:00
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 {
2020-05-10 21:40:25 -07:00
const buffer = Buffer . from ( part , 'base64' ) ;
const iv = buffer . slice ( 0 , 16 ) ;
2020-05-13 09:36:19 -07:00
let decrypt = crypto . createDecipheriv ( 'aes-256-cbc' , Buffer . from ( encryption . filenameKey , 'hex' ) , iv ) ;
const plainText = decrypt . update ( buffer . slice ( 16 ) ) ;
const plainTextString = Buffer . concat ( [ plainText , decrypt . final ( ) ] ) . toString ( 'utf8' ) ;
const hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . filenameHmacKey , 'hex' ) ) ;
if ( ! hmac . update ( plainTextString ) . digest ( ) . slice ( 0 , 16 ) . equals ( iv ) ) return { error : new BoxError ( BoxError . CRYPTO _ERROR , ` mac error decrypting part ${ part } of path ${ filePath } ` ) } ;
decryptedParts . push ( plainTextString ) ;
2018-07-29 21:01:20 -07:00
} catch ( error ) {
2020-05-13 09:36:19 -07:00
debug ( ` Error decrypting part ${ part } of path ${ filePath } : ` , error ) ;
return { error : new BoxError ( BoxError . CRYPTO _ERROR , ` Error decrypting part ${ part } of path ${ filePath } : ${ error . message } ` ) } ;
2018-07-29 21:01:20 -07:00
}
}
2018-07-27 11:46:42 -07:00
2020-05-13 09:36:19 -07:00
return { result : decryptedParts . join ( '/' ) } ;
2018-07-27 11:46:42 -07:00
}
2020-05-10 21:40:25 -07:00
class EncryptStream extends TransformStream {
2020-05-12 16:17:57 -07:00
constructor ( encryption ) {
2020-05-10 21:40:25 -07:00
super ( ) ;
2020-05-20 22:27:28 -07:00
this . _headerPushed = false ;
2020-05-10 21:40:25 -07:00
this . _iv = crypto . randomBytes ( 16 ) ;
2020-05-12 16:17:57 -07:00
this . _cipher = crypto . createCipheriv ( 'aes-256-cbc' , Buffer . from ( encryption . dataKey , 'hex' ) , this . _iv ) ;
this . _hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . dataHmacKey , 'hex' ) ) ;
2020-05-10 21:40:25 -07:00
}
2020-05-20 22:27:28 -07:00
pushHeaderIfNeeded ( ) {
if ( ! this . _headerPushed ) {
const magic = Buffer . from ( 'CBV2' ) ;
this . push ( magic ) ;
this . _hmac . update ( magic ) ;
2020-05-10 21:40:25 -07:00
this . push ( this . _iv ) ;
2020-05-12 16:17:57 -07:00
this . _hmac . update ( this . _iv ) ;
2020-05-20 22:27:28 -07:00
this . _headerPushed = true ;
2020-05-10 21:40:25 -07:00
}
2020-05-15 16:05:12 -07:00
}
_transform ( chunk , ignoredEncoding , callback ) {
2020-05-20 22:27:28 -07:00
this . pushHeaderIfNeeded ( ) ;
2020-05-15 16:05:12 -07:00
2020-05-10 21:40:25 -07:00
try {
const crypt = this . _cipher . update ( chunk ) ;
2020-05-12 16:17:57 -07:00
this . _hmac . update ( crypt ) ;
2020-05-10 21:40:25 -07:00
callback ( null , crypt ) ;
} catch ( error ) {
callback ( error ) ;
}
}
_flush ( callback ) {
try {
2020-05-20 22:27:28 -07:00
this . pushHeaderIfNeeded ( ) ; // for 0-length files
2020-05-10 21:40:25 -07:00
const crypt = this . _cipher . final ( ) ;
2020-05-12 16:17:57 -07:00
this . push ( crypt ) ;
this . _hmac . update ( crypt ) ;
2020-05-15 14:35:19 -07:00
callback ( null , this . _hmac . digest ( ) ) ; // +32 bytes
2020-05-10 21:40:25 -07:00
} catch ( error ) {
callback ( error ) ;
}
}
}
class DecryptStream extends TransformStream {
2020-05-12 16:17:57 -07:00
constructor ( encryption ) {
2020-05-10 21:40:25 -07:00
super ( ) ;
2020-05-12 16:17:57 -07:00
this . _key = Buffer . from ( encryption . dataKey , 'hex' ) ;
2020-05-20 22:27:28 -07:00
this . _header = Buffer . alloc ( 0 ) ;
2020-05-10 21:40:25 -07:00
this . _decipher = null ;
2020-05-12 16:17:57 -07:00
this . _hmac = crypto . createHmac ( 'sha256' , Buffer . from ( encryption . dataHmacKey , 'hex' ) ) ;
this . _buffer = Buffer . alloc ( 0 ) ;
2020-05-10 21:40:25 -07:00
}
_transform ( chunk , ignoredEncoding , callback ) {
2020-05-20 22:27:28 -07:00
const needed = 20 - this . _header . length ; // 4 for magic, 16 for iv
2020-05-12 16:17:57 -07:00
2020-05-20 22:27:28 -07:00
if ( this . _header . length !== 20 ) { // not gotten header yet
this . _header = Buffer . concat ( [ this . _header , chunk . slice ( 0 , needed ) ] ) ;
if ( this . _header . length !== 20 ) return callback ( ) ;
2020-05-10 21:40:25 -07:00
2020-05-20 22:27:28 -07:00
if ( ! this . _header . slice ( 0 , 4 ) . equals ( new Buffer . from ( 'CBV2' ) ) ) return callback ( new BoxError ( BoxError . CRYPTO _ERROR , 'Invalid magic in header' ) ) ;
const iv = this . _header . slice ( 4 ) ;
this . _decipher = crypto . createDecipheriv ( 'aes-256-cbc' , this . _key , iv ) ;
this . _hmac . update ( this . _header ) ;
2020-05-10 21:40:25 -07:00
}
2020-05-12 16:17:57 -07:00
this . _buffer = Buffer . concat ( [ this . _buffer , chunk . slice ( needed ) ] ) ;
2020-05-20 22:27:28 -07:00
if ( this . _buffer . length < 32 ) return callback ( ) ; // hmac trailer length is 32
2020-05-12 16:17:57 -07:00
2020-05-10 21:40:25 -07:00
try {
2020-05-12 16:17:57 -07:00
const cipherText = this . _buffer . slice ( 0 , - 32 ) ;
this . _hmac . update ( cipherText ) ;
const plainText = this . _decipher . update ( cipherText ) ;
this . _buffer = this . _buffer . slice ( - 32 ) ;
2020-05-10 21:40:25 -07:00
callback ( null , plainText ) ;
} catch ( error ) {
callback ( error ) ;
}
}
_flush ( callback ) {
2020-05-12 16:17:57 -07:00
if ( this . _buffer . length !== 32 ) return callback ( new BoxError ( BoxError . CRYPTO _ERROR , 'Invalid password or tampered file (not enough data)' ) ) ;
2020-05-10 21:40:25 -07:00
try {
2020-05-12 16:17:57 -07:00
if ( ! this . _hmac . digest ( ) . equals ( this . _buffer ) ) return callback ( new BoxError ( BoxError . CRYPTO _ERROR , 'Invalid password or tampered file (mac mismatch)' ) ) ;
2020-05-10 21:40:25 -07:00
const plainText = this . _decipher . final ( ) ;
callback ( null , plainText ) ;
} catch ( error ) {
callback ( error ) ;
}
}
}
2020-05-13 09:36:19 -07:00
function createReadStream ( sourceFile , encryption ) {
2018-07-27 06:55:54 -07:00
assert . strictEqual ( typeof sourceFile , 'string' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
2018-07-27 06:55:54 -07:00
var stream = fs . createReadStream ( sourceFile ) ;
var ps = progressStream ( { time : 10000 } ) ; // display a progress every 10 seconds
stream . on ( 'error' , function ( error ) {
2020-05-15 14:35:19 -07:00
debug ( ` createReadStream: read stream error at ${ sourceFile } ` , error ) ;
ps . emit ( 'error' , new BoxError ( BoxError . FS _ERROR , ` Error reading ${ sourceFile } : ${ error . message } ` ) ) ;
2018-07-27 06:55:54 -07:00
} ) ;
2020-05-13 09:36:19 -07:00
if ( encryption ) {
let encryptStream = new EncryptStream ( encryption ) ;
2020-05-10 21:40:25 -07:00
encryptStream . on ( 'error' , function ( error ) {
2020-05-15 14:35:19 -07:00
debug ( ` createReadStream: encrypt stream error ${ sourceFile } ` , error ) ;
ps . emit ( 'error' , new BoxError ( BoxError . CRYPTO _ERROR , ` Encryption error at ${ sourceFile } : ${ error . message } ` ) ) ;
2018-07-27 06:55:54 -07:00
} ) ;
2020-05-10 21:40:25 -07:00
return stream . pipe ( encryptStream ) . pipe ( ps ) ;
2018-07-27 06:55:54 -07:00
} else {
return stream . pipe ( ps ) ;
}
}
2020-05-13 09:36:19 -07:00
function createWriteStream ( destFile , encryption ) {
2018-07-27 11:46:42 -07:00
assert . strictEqual ( typeof destFile , 'string' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
2018-07-27 11:46:42 -07:00
var stream = fs . createWriteStream ( destFile ) ;
2019-12-05 14:51:15 -08:00
var ps = progressStream ( { time : 10000 } ) ; // display a progress every 10 seconds
stream . on ( 'error' , function ( error ) {
2020-05-15 14:35:19 -07:00
debug ( ` createWriteStream: write stream error ${ destFile } ` , error ) ;
ps . emit ( 'error' , new BoxError ( BoxError . FS _ERROR , ` Write error ${ destFile } : ${ error . message } ` ) ) ;
2019-12-05 14:51:15 -08:00
} ) ;
2018-07-27 11:46:42 -07:00
2020-05-15 15:24:12 -07:00
stream . on ( 'finish' , function ( ) {
debug ( 'createWriteStream: done.' ) ;
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not write
ps . emit ( 'done' ) ;
} ) ;
2020-05-13 09:36:19 -07:00
if ( encryption ) {
let decrypt = new DecryptStream ( encryption ) ;
2018-07-27 11:46:42 -07:00
decrypt . on ( 'error' , function ( error ) {
2020-05-15 14:35:19 -07:00
debug ( ` createWriteStream: decrypt stream error ${ destFile } ` , error ) ;
ps . emit ( 'error' , new BoxError ( BoxError . CRYPTO _ERROR , ` Decryption error at ${ destFile } : ${ error . message } ` ) ) ;
2018-07-27 11:46:42 -07:00
} ) ;
2020-05-10 21:40:25 -07:00
2019-12-05 14:51:15 -08:00
ps . pipe ( decrypt ) . pipe ( stream ) ;
2018-07-27 11:46:42 -07:00
} else {
2019-12-05 14:51:15 -08:00
ps . pipe ( stream ) ;
2018-07-27 11:46:42 -07:00
}
2019-12-05 14:51:15 -08:00
return ps ;
2018-07-27 11:46:42 -07:00
}
2020-05-13 09:36:19 -07:00
function tarPack ( dataLayout , encryption , callback ) {
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
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 ( { } ) ;
2020-05-10 21:40:25 -07:00
var ps = progressStream ( { time : 10000 } ) ; // emit 'progress' 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 ) ;
2019-10-22 20:36:20 -07:00
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
gzip . on ( 'error' , function ( error ) {
2018-12-20 11:41:38 -08:00
debug ( 'tarPack: gzip stream error.' , error ) ;
2019-10-22 20:36:20 -07:00
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
2020-05-13 09:36:19 -07:00
if ( encryption ) {
const encryptStream = new EncryptStream ( encryption ) ;
2020-05-10 21:40:25 -07:00
encryptStream . on ( 'error' , function ( error ) {
2018-12-20 11:41:38 -08:00
debug ( 'tarPack: encrypt stream error.' , error ) ;
2019-10-22 20:36:20 -07:00
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-20 09:57:16 -07:00
} ) ;
2020-05-10 21:40:25 -07:00
pack . pipe ( gzip ) . pipe ( encryptStream ) . 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
2020-05-10 21:40:25 -07:00
return callback ( null , ps ) ;
}
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
2020-05-13 09:36:19 -07:00
const destPath = task . path && backupConfig . encryption ? encryptFilePath ( task . path , backupConfig . encryption ) : 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 } ` ) ;
2020-05-13 09:36:19 -07:00
var stream = createReadStream ( dataLayout . toLocalPath ( './' + task . path ) , backupConfig . encryption ) ;
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
2019-12-03 15:09:59 -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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , error . message ) ) ;
2017-09-26 11:59:45 -07:00
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' } ) ;
2019-10-22 20:36:20 -07:00
if ( ! result ) return callback ( new BoxError ( BoxError . FS _ERROR , safe . error ) ) ;
2019-08-12 21:56:41 -07:00
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
2019-10-22 20:36:20 -07:00
if ( diskUsage . available <= needed ) return callback ( new BoxError ( BoxError . FS _ERROR , ` Not enough disk space for backup. Needed: ${ prettyBytes ( needed ) } Available: ${ prettyBytes ( diskUsage . available ) } ` ) ) ;
2019-08-12 21:56:41 -07:00
callback ( null ) ;
} ) . catch ( function ( error ) {
2019-10-22 20:36:20 -07:00
callback ( new BoxError ( BoxError . FS _ERROR , error ) ) ;
2019-08-12 21:56:41 -07:00
} ) ;
}
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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( error ) ;
2017-09-19 20:27:36 -07:00
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
2020-05-13 09:36:19 -07:00
tarPack ( dataLayout , backupConfig . encryption , function ( error , tarStream ) {
2019-08-12 21:56:41 -07:00
if ( error ) return retryCallback ( error ) ;
2018-12-20 11:41:38 -08:00
2019-12-03 15:09:59 -08:00
tarStream . on ( 'progress' , function ( progress ) {
2019-08-12 21:56:41 -07:00
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 ` } ) ;
} ) ;
2019-10-22 20:36:20 -07:00
tarStream . on ( 'error' , retryCallback ) ; // already returns BoxError
2019-08-12 21:56:41 -07:00
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
} ) ;
}
2020-05-13 09:36:19 -07:00
function tarExtract ( inStream , dataLayout , encryption , 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' ) ;
2020-05-13 09:36:19 -07:00
assert . strictEqual ( typeof encryption , 'object' ) ;
2017-09-20 09:57:16 -07:00
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
2020-05-15 15:21:24 -07:00
const emitError = once ( ( error ) => {
inStream . destroy ( ) ;
ps . emit ( 'error' , error ) ;
} ) ;
2018-12-20 11:41:38 -08:00
2017-09-28 14:26:39 -07:00
inStream . on ( 'error' , function ( error ) {
debug ( 'tarExtract: input stream error.' , error ) ;
2019-10-22 20:36:20 -07:00
emitError ( new BoxError ( BoxError . 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 ) ;
2019-10-22 20:36:20 -07:00
emitError ( new BoxError ( BoxError . 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 ) ;
2019-10-22 20:36:20 -07:00
emitError ( new BoxError ( BoxError . 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
} ) ;
2020-05-13 09:36:19 -07:00
if ( encryption ) {
let decrypt = new DecryptStream ( encryption ) ;
2017-09-20 09:57:16 -07:00
decrypt . on ( 'error' , function ( error ) {
2017-09-28 14:26:39 -07:00
debug ( 'tarExtract: decrypt stream error.' , error ) ;
2019-10-22 20:36:20 -07:00
emitError ( new BoxError ( BoxError . 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-10-22 20:36:20 -07:00
if ( metadataJson === null ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Error loading fsmetadata.json:' + safe . error . message ) ) ;
2017-11-22 22:37:27 -08:00
var metadata = safe . JSON . parse ( metadataJson ) ;
2019-10-22 20:36:20 -07:00
if ( metadata === null ) return callback ( new BoxError ( BoxError . 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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . 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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , ` unable to chmod: ${ error . message } ` ) ) ;
2017-10-12 16:02:09 -07:00
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
2019-12-05 21:50:44 -08:00
function downloadFile ( entry , done ) {
2018-07-29 21:01:20 -07:00
let relativePath = path . relative ( backupFilePath , entry . fullPath ) ;
2020-05-12 14:00:05 -07:00
if ( backupConfig . encryption ) {
2020-05-13 09:36:19 -07:00
const { error , result } = decryptFilePath ( relativePath , backupConfig . encryption ) ;
if ( error ) return done ( new BoxError ( BoxError . CRYPTO _ERROR , 'Unable to decrypt file' ) ) ;
relativePath = result ;
2018-07-29 21:01:20 -07:00
}
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 ) {
2019-12-05 21:50:44 -08:00
if ( error ) return done ( new BoxError ( BoxError . FS _ERROR , error . message ) ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
async . retry ( { times : 5 , interval : 20000 } , function ( retryCallback ) {
api ( backupConfig . provider ) . download ( backupConfig , entry . fullPath , function ( error , sourceStream ) {
2020-05-15 14:54:02 -07:00
if ( error ) {
progressCallback ( { message : ` Download ${ entry . fullPath } to ${ destFilePath } errored: ${ error . message } ` } ) ;
return retryCallback ( error ) ;
}
let destStream = createWriteStream ( destFilePath , backupConfig . encryption ) ;
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once ( ( error ) => {
if ( error ) progressCallback ( { message : ` Download ${ entry . fullPath } to ${ destFilePath } errored: ${ error . message } ` } ) ;
else progressCallback ( { message : ` Download ${ entry . fullPath } to ${ destFilePath } finished ` } ) ;
sourceStream . destroy ( ) ;
destStream . destroy ( ) ;
retryCallback ( error ) ;
} ) ;
destStream . 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 ${ entry . fullPath } ` } ) ; // 0M@0Mbps looks wrong
progressCallback ( { message : ` Downloading ${ entry . fullPath } : ${ transferred } M@ ${ speed } Mbps ` } ) ;
} ) ;
destStream . on ( 'error' , closeAndRetry ) ;
2018-07-27 15:22:54 -07:00
2019-01-14 11:48:47 -08:00
sourceStream . 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 } ` } ) ;
2020-05-15 15:24:12 -07:00
sourceStream . pipe ( destStream , { end : true } ) . on ( 'done' , closeAndRetry ) ;
2019-01-14 11:48:47 -08:00
} ) ;
2019-12-05 21:50:44 -08:00
} , done ) ;
2018-07-27 15:22:54 -07:00
} ) ;
}
2019-12-05 21:50:44 -08:00
api ( backupConfig . provider ) . listDir ( backupConfig , backupFilePath , 1000 , function ( entries , iteratorDone ) {
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
2019-12-05 21:50:44 -08:00
async . eachLimit ( entries , concurrency , downloadFile , iteratorDone ) ;
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-12-05 21:50:44 -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
2020-05-13 09:36:19 -07:00
tarExtract ( sourceStream , dataLayout , backupConfig . encryption , 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 ) ;
2019-11-11 17:09:46 -08:00
if ( ! transferred && ! speed ) return progressCallback ( { message : 'Downloading backup' } ) ; // 0M@0Mbps looks wrong
2019-01-14 11:36:11 -08:00
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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DATABASE _ERROR , error ) ) ;
2017-11-22 10:58:07 -08:00
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
} ) ;
}
2019-12-05 10:40:32 -08:00
function downloadApp ( app , restoreConfig , progressCallback , callback ) {
2017-10-10 20:23:04 -07:00
assert . strictEqual ( typeof app , '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
2019-12-04 18:54:25 -08:00
const startTime = new Date ( ) ;
const getBackupConfigFunc = restoreConfig . backupConfig ? ( next ) => next ( null , restoreConfig . backupConfig ) : settings . getBackupConfig ;
2017-10-10 20:23:04 -07:00
2019-12-04 18:54:25 -08:00
getBackupConfigFunc ( function ( error , backupConfig ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( error ) ;
2017-10-10 20:23:04 -07:00
2019-12-05 10:40:32 -08:00
download ( backupConfig , restoreConfig . backupId , restoreConfig . backupFormat , dataLayout , progressCallback , function ( error ) {
debug ( 'downloadApp: time: %s' , ( new Date ( ) - startTime ) / 1000 ) ;
2017-10-10 20:23:04 -07:00
callback ( error ) ;
} ) ;
} ) ;
}
2019-12-04 15:22:42 -08:00
function runBackupUpload ( uploadConfig , progressCallback , callback ) {
assert . strictEqual ( typeof uploadConfig , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const { backupId , format , dataLayout , progressTag } = uploadConfig ;
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-12-04 15:22:42 -08:00
assert . strictEqual ( typeof progressTag , 'string' ) ;
2019-01-18 14:50:04 -08:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
2017-09-19 08:19:01 -07:00
2019-12-04 15:22:42 -08:00
let result = '' ; // the script communicates error result as a string
2018-11-26 15:21:48 -08:00
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
2019-10-22 20:36:20 -07:00
return callback ( new BoxError ( BoxError . INTERNAL _ERROR , 'Backuptask crashed' ) ) ;
2017-09-19 08:19:01 -07:00
} else if ( error && error . code === 50 ) { // exited with error
2019-10-22 20:36:20 -07:00
return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , result ) ) ;
2017-09-19 08:19:01 -07:00
}
callback ( ) ;
2020-02-11 10:03:26 -08:00
} ) . on ( 'message' , function ( progress ) { // this is { message } or { result }
if ( 'message' in progress ) return progressCallback ( { message : ` ${ progress . message } ( ${ progressTag } ) ` } ) ;
2019-12-04 15:22:42 -08:00
debug ( ` runBackupUpload: result - ${ JSON . stringify ( progress ) } ` ) ;
result = progress . result ;
2018-11-26 15:21:48 -08:00
} ) ;
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 ] ;
2019-10-22 20:36:20 -07:00
if ( ! safe . fs . writeFileSync ( paths . SNAPSHOT _INFO _FILE , JSON . stringify ( data , null , 4 ) , 'utf8' ) ) {
return callback ( new BoxError ( BoxError . FS _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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DATABASE _ERROR , error ) ) ;
2017-09-17 21:30:16 -07:00
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-12-04 15:22:42 -08:00
const uploadConfig = {
backupId : 'snapshot/box' ,
format : backupConfig . format ,
dataLayout : new DataLayout ( boxDataDir , [ ] ) ,
progressTag : 'box'
} ;
runBackupUpload ( uploadConfig , 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' ) ;
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
2020-05-13 22:09:33 -07:00
const data = {
encryptionVersion : backupConfig . encryption ? 2 : null ,
packageVersion : constants . VERSION ,
type : backupdb . BACKUP _TYPE _BOX ,
dependsOn : appBackupIds ,
manifest : null ,
format : format
} ;
backupdb . add ( backupId , data , function ( error ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( 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 ) ) ;
2019-12-04 15:22:42 -08:00
copy . on ( 'progress' , ( message ) => progressCallback ( { message : ` box: ${ 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 ) ;
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( 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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( error ) ;
2017-09-17 23:45:06 -07:00
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 ) {
2020-04-29 12:04:43 -07:00
// only backup apps that are installed or specific pending states
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app . installationState === apps . ISTATE _INSTALLED ||
2019-08-30 13:12:49 -07:00
app . installationState === apps . ISTATE _PENDING _CONFIGURE ||
app . installationState === apps . ISTATE _PENDING _BACKUP || // called from apptask
app . installationState === apps . ISTATE _PENDING _UPDATE ; // called from apptask
2016-04-10 19:17:44 -07:00
}
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
2019-09-10 15:26:58 -07:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( app ) ) ) {
2019-10-22 20:36:20 -07:00
return callback ( new BoxError ( BoxError . FS _ERROR , 'Error creating config.json: ' + safe . error . message ) ) ;
2017-04-21 14:07:10 -07:00
}
2016-04-10 21:41:53 -07:00
2017-11-19 17:53:17 -08:00
addons . backupAddons ( app , app . manifest . addons , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . 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 ) ;
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
2020-05-13 22:09:33 -07:00
const data = {
encryptionVersion : backupConfig . encryption ? 2 : null ,
packageVersion : manifest . version ,
type : backupdb . BACKUP _TYPE _APP ,
dependsOn : [ ] ,
manifest ,
format : format
} ;
backupdb . add ( backupId , data , function ( error ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( 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 ) ) ;
2019-12-05 11:15:21 -08:00
copy . on ( 'progress' , ( message ) => progressCallback ( { message : ` ${ message } ( ${ app . fqdn } ) ` } ) ) ;
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 ) ;
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2017-09-17 23:45:06 -07:00
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' } ] : [ ] ) ;
2019-12-04 15:22:42 -08:00
const uploadConfig = {
backupId ,
format : backupConfig . format ,
dataLayout ,
progressTag : app . fqdn
} ;
runBackupUpload ( uploadConfig , 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 ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( 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 ) {
2019-10-24 18:32:33 -07:00
if ( error ) return callback ( error ) ;
2016-04-10 22:24:01 -07:00
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 ) {
2019-10-22 20:36:20 -07:00
if ( error && error . reason !== BoxError . BAD _STATE ) {
2016-04-10 22:24:01 -07:00
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-10-22 20:36:20 -07:00
if ( error ) return callback ( new BoxError ( BoxError . BAD _STATE , ` Cannot backup now: ${ error . message } ` ) ) ;
2018-12-09 03:20:00 -08:00
2019-08-27 22:39:59 -07:00
tasks . add ( tasks . TASK _BACKUP , [ ] , function ( error , taskId ) {
2019-10-24 18:32:33 -07:00
if ( error ) return callback ( error ) ;
2019-08-27 22:39:59 -07:00
2018-12-09 03:20:00 -08:00
eventlog . add ( eventlog . ACTION _BACKUP _START , auditSource , { taskId } ) ;
2019-10-11 19:35:21 -07:00
tasks . startTask ( taskId , { timeout : 12 * 60 * 60 * 1000 /* 12 hours */ } , function ( error , backupId ) {
2019-08-27 22:39:59 -07:00
locker . unlock ( locker . OP _FULL _BACKUP ) ;
const errorMessage = error ? error . message : '' ;
2019-10-11 19:35:21 -07:00
const timedOut = error ? error . code === tasks . ETIMEOUT : false ;
2018-12-09 03:20:00 -08:00
2019-10-11 19:35:21 -07:00
eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId , errorMessage , timedOut , backupId } ) ;
2019-08-27 22:39:59 -07:00
} ) ;
callback ( null , taskId ) ;
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
2020-05-21 14:30:21 -07:00
// backups must be descending in creationTime
2020-05-14 20:05:27 -07:00
function applyBackupRetentionPolicy ( backups , policy ) {
assert ( Array . isArray ( backups ) ) ;
assert . strictEqual ( typeof policy , 'object' ) ;
const now = new Date ( ) ;
for ( const backup of backups ) {
if ( backup . keepReason ) continue ; // already kept for some other reason
if ( ( now - backup . creationTime ) < ( backup . preserveSecs * 1000 ) ) {
backup . keepReason = 'preserveSecs' ;
2020-05-21 14:30:21 -07:00
} else if ( ( now - backup . creationTime < policy . keepWithinSecs * 1000 ) || policy . keepWithinSecs < 0 ) {
backup . keepReason = 'keepWithinSecs' ;
2020-05-14 20:05:27 -07:00
}
}
const KEEP _FORMATS = {
keepDaily : 'Y-M-D' ,
keepWeekly : 'Y-W' ,
keepMonthly : 'Y-M' ,
keepYearly : 'Y'
} ;
for ( const format of [ 'keepDaily' , 'keepWeekly' , 'keepMonthly' , 'keepYearly' ] ) {
if ( ! ( format in policy ) ) continue ;
const n = policy [ format ] ; // we want to keep "n" backups of format
if ( ! n ) continue ; // disabled rule
let lastPeriod = null , keptSoFar = 0 ;
for ( const backup of backups ) {
if ( backup . keepReason ) continue ; // already kept for some other reason
const period = moment ( backup . creationTime ) . format ( KEEP _FORMATS [ format ] ) ;
if ( period === lastPeriod ) continue ; // already kept for this period
lastPeriod = period ;
backup . keepReason = format ;
if ( ++ keptSoFar === n ) break ;
}
}
for ( const backup of backups ) {
if ( backup . keepReason ) debug ( ` applyBackupRetentionPolicy: ${ backup . id } ${ backup . type } ${ backup . keepReason } ` ) ;
}
}
2020-05-21 13:41:01 -07:00
function cleanupBackup ( backupConfig , backup , progressCallback , callback ) {
2017-10-03 00:36:09 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof backup , 'object' ) ;
2020-05-21 13:41:01 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-10-03 00:36:09 -07:00
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' ) {
2020-05-21 13:41:01 -07:00
progressCallback ( { message : ` ${ backup . id } : Removing ${ backupFilePath } ` } ) ;
2017-10-10 20:23:04 -07:00
api ( backupConfig . provider ) . remove ( backupConfig , backupFilePath , done ) ;
} else {
var events = api ( backupConfig . provider ) . removeDir ( backupConfig , backupFilePath ) ;
2020-05-21 13:41:01 -07:00
events . on ( 'progress' , ( message ) => progressCallback ( { message : ` ${ backup . id } : ${ message } ` } ) ) ;
2017-10-10 20:23:04 -07:00
events . on ( 'done' , done ) ;
}
2017-10-03 00:36:09 -07:00
}
2020-05-21 13:41:01 -07:00
function cleanupAppBackups ( backupConfig , referencedAppBackupIds , progressCallback , callback ) {
2017-05-30 13:18:58 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2020-05-14 20:05:27 -07:00
assert ( Array . isArray ( referencedAppBackupIds ) ) ;
2020-05-21 13:41:01 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2017-05-30 13:18:58 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-04-23 11:34:46 -07:00
2020-05-14 20:05:27 -07:00
let removedAppBackupIds = [ ] ;
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 ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2017-04-24 13:41:23 +02:00
2020-05-14 20:05:27 -07:00
for ( const appBackup of appBackups ) { // set the reason so that policy filter can skip it
if ( referencedAppBackupIds . includes ( appBackup . id ) ) appBackup . keepReason = 'reference' ;
}
applyBackupRetentionPolicy ( appBackups , backupConfig . retentionPolicy ) ;
2019-01-11 12:48:40 -08:00
async . eachSeries ( appBackups , function iterator ( appBackup , iteratorDone ) {
2020-05-14 20:05:27 -07:00
if ( appBackup . keepReason ) return iteratorDone ( ) ;
2017-04-24 12:01:50 +02:00
2020-05-21 13:41:01 -07:00
progressCallback ( { message : ` Removing app backup ${ appBackup . id } ` } ) ;
2017-04-23 11:34:46 -07:00
2020-05-14 20:05:27 -07:00
removedAppBackupIds . push ( appBackup . id ) ;
2020-05-21 13:41:01 -07:00
cleanupBackup ( backupConfig , appBackup , progressCallback , 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
2020-05-14 20:05:27 -07:00
callback ( null , removedAppBackupIds ) ;
2017-05-30 13:18:58 -07:00
} ) ;
} ) ;
}
2017-04-24 13:41:23 +02:00
2020-05-21 13:41:01 -07:00
function cleanupBoxBackups ( backupConfig , progressCallback , auditSource , callback ) {
2017-05-30 13:18:58 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2020-05-21 13:41:01 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
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
2020-05-14 20:05:27 -07:00
let referencedAppBackupIds = [ ] , removedBoxBackupIds = [ ] ;
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
2020-05-14 20:05:27 -07:00
if ( boxBackups . length === 0 ) return callback ( null , { removedBoxBackupIds , referencedAppBackupIds } ) ;
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 ) ;
2020-05-14 20:05:27 -07:00
referencedAppBackupIds = boxBackups [ i ] . dependsOn ;
2017-05-30 15:15:20 -07:00
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
2020-05-14 20:05:27 -07:00
applyBackupRetentionPolicy ( boxBackups , backupConfig . retentionPolicy ) ;
2019-01-11 12:48:40 -08:00
async . eachSeries ( boxBackups , function iterator ( boxBackup , iteratorNext ) {
2020-05-14 20:05:27 -07:00
if ( boxBackup . keepReason ) {
referencedAppBackupIds = referencedAppBackupIds . concat ( boxBackup . dependsOn ) ;
2019-01-11 12:48:40 -08:00
return iteratorNext ( ) ;
2017-10-03 00:36:09 -07:00
}
2017-04-24 13:41:23 +02:00
2020-05-21 13:41:01 -07:00
progressCallback ( { message : ` Removing box backup ${ boxBackup . id } ` } ) ;
2017-05-30 13:18:58 -07:00
2020-05-14 20:05:27 -07:00
removedBoxBackupIds . push ( boxBackup . id ) ;
2020-05-21 13:41:01 -07:00
cleanupBackup ( backupConfig , boxBackup , progressCallback , iteratorNext ) ;
2017-05-30 13:18:58 -07:00
} , function ( ) {
2017-09-23 11:09:36 -07:00
debug ( 'cleanupBoxBackups: done' ) ;
2020-05-14 20:05:27 -07:00
callback ( null , { removedBoxBackupIds , referencedAppBackupIds } ) ;
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 */ ) {
2019-10-24 10:39:47 -07:00
if ( ! error || error . reason !== BoxError . NOT _FOUND ) return iteratorDone ( ) ;
2017-09-17 23:45:06 -07:00
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 ) ;
2020-05-14 16:45:26 -07:00
if ( backupConfig . retentionPolicy . keepWithinSecs < 0 ) {
2017-05-30 13:18:58 -07:00
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' } ) ;
2020-05-21 13:41:01 -07:00
cleanupBoxBackups ( backupConfig , progressCallback , auditSource , function ( error , { removedBoxBackupIds , referencedAppBackupIds } ) {
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' } ) ;
2020-05-21 13:41:01 -07:00
cleanupAppBackups ( backupConfig , referencedAppBackupIds , progressCallback , function ( error , removedAppBackupIds ) {
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 ) ;
2020-05-14 20:05:27 -07:00
callback ( null , { removedBoxBackupIds , removedAppBackupIds } ) ;
2019-01-11 12:48:40 -08:00
} ) ;
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 ) {
2019-08-27 22:39:59 -07:00
tasks . add ( tasks . TASK _CLEAN _BACKUPS , [ auditSource ] , function ( error , taskId ) {
2019-10-22 20:36:20 -07:00
if ( error ) return callback ( error ) ;
2019-08-27 22:39:59 -07:00
tasks . startTask ( taskId , { } , ( error , result ) => { // result is { removedBoxBackups, removedAppBackups }
eventlog . add ( eventlog . ACTION _BACKUP _CLEANUP _FINISH , auditSource , {
2019-09-27 09:43:40 -07:00
taskId ,
2019-08-27 22:39:59 -07:00
errorMessage : error ? error . message : null ,
removedBoxBackups : result ? result . removedBoxBackups : [ ] ,
removedAppBackups : result ? result . removedAppBackups : [ ]
} ) ;
2019-01-12 10:08:52 -08:00
} ) ;
2019-08-27 22:39:59 -07:00
callback ( null , taskId ) ;
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 ) ;
} ) ;
}
2020-01-31 13:37:07 -08:00
function configureCollectd ( backupConfig , callback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( backupConfig . provider === 'filesystem' ) {
const collectdConf = ejs . render ( COLLECTD _CONFIG _EJS , { backupDir : backupConfig . backupFolder } ) ;
collectd . addProfile ( 'cloudron-backup' , collectdConf , callback ) ;
} else {
collectd . removeProfile ( 'cloudron-backup' , callback ) ;
}
2020-02-26 09:08:30 -08:00
}