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
|
|
|
|
2016-06-13 13:42:25 -07:00
|
|
|
getRestoreConfig: getRestoreConfig,
|
2015-09-21 14:14:21 -07:00
|
|
|
|
2016-04-10 19:17:44 -07:00
|
|
|
ensureBackup: ensureBackup,
|
|
|
|
|
|
|
|
|
|
backup: backup,
|
|
|
|
|
backupApp: backupApp,
|
|
|
|
|
restoreApp: restoreApp,
|
|
|
|
|
|
2016-09-16 18:14:36 +02:00
|
|
|
backupBoxAndApps: backupBoxAndApps,
|
|
|
|
|
|
2017-09-19 20:27:36 -07:00
|
|
|
upload: upload,
|
2017-09-20 09:57:16 -07:00
|
|
|
download: download,
|
2017-09-19 20:27:36 -07:00
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
cleanup: cleanup,
|
|
|
|
|
|
|
|
|
|
// for testing
|
|
|
|
|
_getBackupFilePath: getBackupFilePath,
|
|
|
|
|
_createTarPackStream: createTarPackStream,
|
|
|
|
|
_tarExtract: tarExtract
|
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'),
|
2015-07-20 00:09:47 -07:00
|
|
|
config = require('./config.js'),
|
2017-09-20 09:57:16 -07:00
|
|
|
crypto = require('crypto'),
|
2016-04-10 19:17:44 -07:00
|
|
|
DatabaseError = require('./databaseerror.js'),
|
2015-07-20 00:09:47 -07:00
|
|
|
debug = require('debug')('box:backups'),
|
2016-05-01 11:42:12 -07:00
|
|
|
eventlog = require('./eventlog.js'),
|
2017-09-22 14:40:37 -07:00
|
|
|
fs = require('fs'),
|
2016-10-14 14:46:34 -07:00
|
|
|
locker = require('./locker.js'),
|
|
|
|
|
mailer = require('./mailer.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'),
|
|
|
|
|
progress = require('./progress.js'),
|
2017-09-20 09:57:16 -07:00
|
|
|
progressStream = require('progress-stream'),
|
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'),
|
|
|
|
|
util = require('util'),
|
|
|
|
|
zlib = require('zlib');
|
2016-04-10 19:17:44 -07:00
|
|
|
|
|
|
|
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
|
|
|
|
|
2017-09-19 08:19:01 -07:00
|
|
|
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
|
|
|
|
|
|
2017-09-19 20:40:38 -07:00
|
|
|
function debugApp(app) {
|
2016-04-10 19:17:44 -07:00
|
|
|
assert(!app || typeof app === 'object');
|
|
|
|
|
|
|
|
|
|
var prefix = app ? app.location : '(no app)';
|
|
|
|
|
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
|
|
|
|
}
|
|
|
|
|
|
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 'caas': return require('./storage/caas.js');
|
|
|
|
|
case 's3': return require('./storage/s3.js');
|
|
|
|
|
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');
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
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'));
|
|
|
|
|
|
|
|
|
|
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'flat-file') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
|
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
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-13 13:42:25 -07:00
|
|
|
function getRestoreConfig(backupId, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-04-18 12:08:26 +02:00
|
|
|
backupdb.get(backupId, function (error, result) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
2016-06-13 13:42:25 -07:00
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
2017-04-18 12:08:26 +02:00
|
|
|
if (!result.restoreConfig) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
2016-06-13 13:42:25 -07:00
|
|
|
|
2017-04-18 12:08:26 +02:00
|
|
|
callback(null, result.restoreConfig);
|
2016-06-13 13:42:25 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
function getBackupFilePath(backupConfig, backupId, subpath) {
|
2017-09-19 20:40:38 -07:00
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
if (backupConfig.format === 'tgz') {
|
|
|
|
|
const fileType = backupConfig.key ? '.tar.gz.enc' : '.tar.gz';
|
|
|
|
|
return path.join(backupConfig.prefix || backupConfig.backupFolder, backupId+fileType);
|
|
|
|
|
} else {
|
|
|
|
|
return path.join(backupConfig.prefix || backupConfig.backupFolder, backupId, subpath || '');
|
|
|
|
|
}
|
2017-09-19 20:40:38 -07:00
|
|
|
}
|
|
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
function createTarPackStream(sourceDir, key) {
|
|
|
|
|
assert.strictEqual(typeof sourceDir, 'string');
|
|
|
|
|
assert(key === null || typeof key === 'string');
|
|
|
|
|
|
|
|
|
|
var pack = tar.pack('/', {
|
|
|
|
|
dereference: false, // pack the symlink and not what it points to
|
|
|
|
|
entries: [ sourceDir ],
|
|
|
|
|
map: function(header) {
|
|
|
|
|
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
|
|
|
|
|
return header;
|
|
|
|
|
},
|
|
|
|
|
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var gzip = zlib.createGzip({});
|
|
|
|
|
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
|
|
|
|
|
|
|
|
|
pack.on('error', function (error) {
|
|
|
|
|
debug('backup: tar stream error.', error);
|
|
|
|
|
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gzip.on('error', function (error) {
|
|
|
|
|
debug('backup: gzip stream error.', error);
|
|
|
|
|
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ps.on('progress', function(progress) {
|
|
|
|
|
debug('backup: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (key !== null) {
|
|
|
|
|
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
|
|
|
|
encrypt.on('error', function (error) {
|
|
|
|
|
debug('backup: encrypt stream error.', error);
|
|
|
|
|
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
return pack.pipe(gzip).pipe(encrypt).pipe(ps);
|
|
|
|
|
} else {
|
|
|
|
|
return pack.pipe(gzip).pipe(ps);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
function sync(backupConfig, backupId, dataDir, callback) {
|
|
|
|
|
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
|
|
|
|
debug('syncer task: %j', task);
|
|
|
|
|
if (task.operation === 'add') {
|
|
|
|
|
var stream = fs.createReadStream(path.join(dataDir, task.path));
|
|
|
|
|
stream.on('error', function () { return iteratorCallback(); }); // ignore error if file disappears
|
|
|
|
|
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, task.path), stream, iteratorCallback);
|
|
|
|
|
} else if (task.operation === 'remove') {
|
|
|
|
|
api(backupConfig.provider).remove(backupConfig, getBackupFilePath(backupConfig, backupId, task.path), iteratorCallback);
|
|
|
|
|
}
|
2017-09-26 11:59:45 -07:00
|
|
|
}, function (error) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
});
|
2017-09-22 14:40:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveEmptyDirs(appDataDir, callback) {
|
|
|
|
|
assert.strictEqual(typeof appDataDir, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}` });
|
|
|
|
|
|
|
|
|
|
if (emptyDirs === null) return callback(safe.error);
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.writeFileSync(`${appDataDir}/emptydirs.txt`, emptyDirs)) return callback(safe.error);
|
|
|
|
|
callback();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-19 20:27:36 -07:00
|
|
|
// this function is called via backuptask (since it needs root to traverse app's directory)
|
|
|
|
|
function upload(backupId, dataDir, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
callback = once(callback);
|
|
|
|
|
|
2017-09-19 20:27:36 -07:00
|
|
|
debug('Start box backup with id %s', backupId);
|
|
|
|
|
|
|
|
|
|
settings.getBackupConfig(function (error, backupConfig) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
if (backupConfig.format === 'tgz') {
|
|
|
|
|
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
|
|
|
|
|
tarStream.on('error', callback); // already returns BackupsError
|
|
|
|
|
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId), tarStream, callback);
|
|
|
|
|
} else {
|
|
|
|
|
async.series([
|
|
|
|
|
saveEmptyDirs.bind(null, dataDir),
|
|
|
|
|
sync.bind(null, backupConfig, backupId, dataDir)
|
|
|
|
|
], callback);
|
|
|
|
|
}
|
2017-09-20 09:57:16 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tarExtract(inStream, destination, key, callback) {
|
|
|
|
|
assert.strictEqual(typeof inStream, 'object');
|
|
|
|
|
assert.strictEqual(typeof destination, 'string');
|
|
|
|
|
assert(key === null || typeof key === 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
callback = once(callback);
|
|
|
|
|
|
|
|
|
|
var gunzip = zlib.createGunzip({});
|
|
|
|
|
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
|
|
|
|
var extract = tar.extract(destination);
|
|
|
|
|
|
|
|
|
|
ps.on('progress', function(progress) {
|
|
|
|
|
debug('restore: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gunzip.on('error', function (error) {
|
|
|
|
|
debug('restore: gunzip stream error.', error);
|
|
|
|
|
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
extract.on('error', function (error) {
|
|
|
|
|
debug('restore: extract stream error.', error);
|
|
|
|
|
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
extract.on('finish', function () {
|
|
|
|
|
debug('restore: done.');
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (key !== null) {
|
|
|
|
|
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
|
|
|
|
decrypt.on('error', function (error) {
|
|
|
|
|
debug('restore: decrypt stream error.', error);
|
|
|
|
|
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
|
|
|
|
|
} else {
|
|
|
|
|
inStream.pipe(ps).pipe(gunzip).pipe(extract);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-23 14:27:35 -07:00
|
|
|
function createEmptyDirs(appDataDir, callback) {
|
|
|
|
|
assert.strictEqual(typeof appDataDir, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debugApp('createEmptyDirs: recreating empty directories');
|
|
|
|
|
|
|
|
|
|
var emptyDirs = safe.fs.readFileSync(path.join(appDataDir, 'emptydirs.txt'), 'utf8');
|
|
|
|
|
if (!emptyDirs) return callback(new Error('emptydirs.txt was not found:' + safe.fs.error));
|
|
|
|
|
|
|
|
|
|
async.eachSeries(emptyDirs.trim().split('\n'), function createPath(emptyDir, iteratorDone) {
|
|
|
|
|
mkdirp(path.join(appDataDir, 'data', emptyDir), iteratorDone);
|
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
function download(backupId, dataDir, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
assert.strictEqual(typeof dataDir, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Start download of id %s', backupId);
|
|
|
|
|
|
|
|
|
|
settings.getBackupConfig(function (error, backupConfig) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2017-09-26 11:14:56 -07:00
|
|
|
if (backupConfig.format === 'tgz') {
|
|
|
|
|
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId), function (error, sourceStream) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
async.series([
|
|
|
|
|
api(backupConfig.provider).downloadDir.bind(null, backupConfig, getBackupFilePath(backupConfig, backupId), dataDir),
|
|
|
|
|
createEmptyDirs.bind(null, dataDir)
|
|
|
|
|
], callback);
|
|
|
|
|
}
|
2017-09-19 20:27:36 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-19 08:19:01 -07:00
|
|
|
function runBackupTask(backupId, dataDir, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
assert.strictEqual(typeof dataDir, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var killTimerId = null;
|
|
|
|
|
|
2017-09-21 14:41:34 -07:00
|
|
|
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, dataDir ], { env: process.env }, function (error) {
|
2017-09-19 08:19:01 -07:00
|
|
|
clearTimeout(killTimerId);
|
|
|
|
|
cp = null;
|
|
|
|
|
|
|
|
|
|
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
|
|
|
|
|
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'backuptask crashed'));
|
|
|
|
|
} else if (error && error.code === 50) { // exited with error
|
|
|
|
|
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8') || safe.error.message;
|
|
|
|
|
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
killTimerId = setTimeout(function () {
|
|
|
|
|
debug('runBackupTask: backup task taking too long. killing');
|
|
|
|
|
cp.kill();
|
|
|
|
|
}, 4 * 60 * 60 * 1000); // 4 hours
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2017-09-17 21:30:16 -07:00
|
|
|
function snapshotBox(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
|
|
|
|
|
var mysqlDumpArgs = [
|
|
|
|
|
'-c',
|
|
|
|
|
`/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
|
|
|
|
|
--triggers ${config.database().name} > "${paths.BOX_DATA_DIR}/box.mysqldump"`
|
|
|
|
|
];
|
|
|
|
|
shell.exec('backupBox', '/bin/bash', mysqlDumpArgs, { }, function (error) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
return callback();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function uploadBoxSnapshot(backupConfig, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var startTime = new Date();
|
|
|
|
|
|
|
|
|
|
snapshotBox(function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-09-19 08:19:01 -07:00
|
|
|
runBackupTask('snapshot/box', paths.BOX_DATA_DIR, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
|
2017-09-17 23:45:06 -07:00
|
|
|
|
|
|
|
|
setSnapshotInfo('box', { timestamp: new Date().toISOString() }, callback);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof timestamp, 'string');
|
2017-04-21 10:31:43 +02:00
|
|
|
assert(Array.isArray(appBackupIds));
|
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
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
|
|
|
|
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
|
2016-04-10 19:17:44 -07:00
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
debug('rotateBoxBackup: rotating to id:%s', backupId);
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null }, function (error) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
2017-09-17 21:30:16 -07:00
|
|
|
|
2017-09-19 20:40:38 -07:00
|
|
|
api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box'), getBackupFilePath(backupConfig, backupId), 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) {
|
|
|
|
|
if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message));
|
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
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
debug('rotateBoxBackup: successful id:%s', backupId);
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
// FIXME this is only needed for caas, hopefully we can remove that in the future
|
|
|
|
|
api(backupConfig.provider).backupDone(backupId, appBackupIds, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2017-04-17 15:10:29 +02:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
callback(null, backupId);
|
2016-04-10 21:55:08 -07:00
|
|
|
});
|
2016-04-10 19:17:44 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function backupBoxWithAppBackupIds(appBackupIds, timestamp, callback) {
|
|
|
|
|
assert(Array.isArray(appBackupIds));
|
|
|
|
|
assert.strictEqual(typeof timestamp, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
settings.getBackupConfig(function (error, backupConfig) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
uploadBoxSnapshot(backupConfig, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 21:30:16 -07:00
|
|
|
function snapshotApp(app, manifest, callback) {
|
2016-04-10 19:17:44 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
2016-06-13 21:17:51 -07:00
|
|
|
assert(manifest && typeof manifest === 'object');
|
2016-04-10 19:17:44 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-04-18 12:16:32 +02:00
|
|
|
var restoreConfig = apps.getAppConfig(app);
|
|
|
|
|
restoreConfig.manifest = manifest;
|
|
|
|
|
|
2017-04-21 14:07:10 -07:00
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(restoreConfig))) {
|
|
|
|
|
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
|
|
|
|
}
|
2016-04-10 21:41:53 -07:00
|
|
|
|
2017-04-21 14:07:10 -07:00
|
|
|
addons.backupAddons(app, manifest.addons, function (error) {
|
|
|
|
|
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, restoreConfig);
|
2017-09-17 21:30:16 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function setRestorePoint(appId, lastBackupId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof lastBackupId, 'string');
|
2017-09-17 21:30:16 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
2017-09-17 21:30:16 -07:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
return callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-09-17 21:30:16 -07:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof timestamp, 'string');
|
|
|
|
|
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-09-17 23:45:06 -07:00
|
|
|
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
|
|
|
|
var restoreConfig = snapshotInfo.restoreConfig;
|
|
|
|
|
var manifest = restoreConfig.manifest;
|
|
|
|
|
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
|
2016-09-16 11:21:08 +02:00
|
|
|
|
2017-09-22 14:40:37 -07:00
|
|
|
debugApp(app, 'rotateAppBackup: rotating to id:%s', backupId);
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
2017-04-21 14:07:10 -07:00
|
|
|
|
2017-09-20 09:38:55 -07:00
|
|
|
api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`), getBackupFilePath(backupConfig, backupId), function (copyBackupError) {
|
2017-09-17 23:45:06 -07:00
|
|
|
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
|
|
|
|
|
debugApp(app, 'rotateAppBackup: successful id:%s', backupId);
|
2017-05-28 17:02:36 -07:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
backupdb.update(backupId, { state: state }, function (error) {
|
|
|
|
|
if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message));
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
setRestorePoint(app.id, backupId, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
return callback(null, backupId);
|
2017-05-28 17:02:36 -07:00
|
|
|
});
|
2017-04-21 14:07:10 -07:00
|
|
|
});
|
2016-04-10 19:17:44 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function uploadAppSnapshot(backupConfig, app, manifest, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert(manifest && typeof manifest === 'object');
|
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();
|
|
|
|
|
|
|
|
|
|
snapshotApp(app, manifest, function (error, restoreConfig) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
var backupId = util.format('snapshot/app_%s', app.id);
|
|
|
|
|
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
2017-09-19 08:19:01 -07:00
|
|
|
runBackupTask(backupId, appDataDir, 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);
|
|
|
|
|
|
|
|
|
|
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), restoreConfig: restoreConfig }, callback);
|
|
|
|
|
});
|
2016-04-10 19:17:44 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
function backupApp(app, manifest, timestamp, callback) {
|
2016-04-10 19:17:44 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
2016-06-13 21:17:51 -07:00
|
|
|
assert(manifest && typeof manifest === 'object');
|
2017-09-17 23:45:06 -07:00
|
|
|
assert.strictEqual(typeof timestamp, 'string');
|
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
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
uploadAppSnapshot(backupConfig, app, manifest, function (error) {
|
2017-09-17 18:50:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-04-10 19:17:44 -07:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
rotateAppBackup(backupConfig, app, timestamp, callback);
|
2016-04-10 19:17:44 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-04-10 22:24:01 -07:00
|
|
|
// this function expects you to have a lock
|
2016-05-03 18:36:50 -07:00
|
|
|
function backupBoxAndApps(auditSource, callback) {
|
|
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
|
|
2016-04-10 22:24:01 -07:00
|
|
|
callback = callback || NOOP_CALLBACK;
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
2017-01-04 19:41:33 -08:00
|
|
|
|
2016-05-03 18:36:50 -07:00
|
|
|
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
|
|
|
|
|
|
2016-04-10 22:24:01 -07:00
|
|
|
apps.getAll(function (error, allApps) {
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
var processed = 0;
|
|
|
|
|
var step = 100/(allApps.length+1);
|
|
|
|
|
|
2017-01-12 15:12:41 +01:00
|
|
|
progress.set(progress.BACKUP, step * processed, '');
|
2016-04-10 22:24:01 -07:00
|
|
|
|
|
|
|
|
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
2017-01-12 15:39:52 +01:00
|
|
|
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
|
2017-01-12 13:42:55 +01:00
|
|
|
|
2016-04-10 22:24:01 -07:00
|
|
|
++processed;
|
|
|
|
|
|
2017-08-16 14:12:07 -07:00
|
|
|
if (!app.enableBackup) {
|
|
|
|
|
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location)));
|
|
|
|
|
return iteratorCallback(null, app.lastBackupId); // just use the last backup
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
backupApp(app, app.manifest, timestamp, 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);
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-12 15:39:52 +01:00
|
|
|
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location)));
|
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) {
|
|
|
|
|
if (error) {
|
|
|
|
|
progress.set(progress.BACKUP, 100, error.message);
|
|
|
|
|
return callback(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
|
|
|
|
|
|
2017-01-12 15:12:41 +01:00
|
|
|
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
|
2017-01-12 13:42:55 +01:00
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, filename) {
|
2016-04-10 22:24:01 -07:00
|
|
|
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
|
|
|
|
|
2016-05-03 18:36:50 -07:00
|
|
|
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
|
|
|
|
|
|
2016-04-10 22:24:01 -07:00
|
|
|
callback(error, filename);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-03 18:36:50 -07:00
|
|
|
function backup(auditSource, callback) {
|
|
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2016-04-10 22:24:01 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var error = locker.lock(locker.OP_FULL_BACKUP);
|
|
|
|
|
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
|
|
|
|
|
|
2017-09-12 14:51:58 -07:00
|
|
|
var startTime = new Date();
|
2016-05-01 11:42:12 -07:00
|
|
|
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
|
|
|
|
|
|
2016-05-03 18:36:50 -07:00
|
|
|
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
|
2016-10-14 14:46:34 -07:00
|
|
|
if (error) {
|
|
|
|
|
debug('backup failed.', error);
|
2017-01-26 13:03:36 -08:00
|
|
|
mailer.backupFailed(error);
|
2016-10-14 14:46:34 -07:00
|
|
|
}
|
2016-04-10 22:24:01 -07:00
|
|
|
|
|
|
|
|
locker.unlock(locker.OP_FULL_BACKUP);
|
2017-09-12 14:51:58 -07:00
|
|
|
|
|
|
|
|
debug('backup took %s seconds', (new Date() - startTime)/1000);
|
2016-04-10 22:24:01 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
return callback(error); // no point trying to backup if appstore is down
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
|
|
|
|
debug('Previous backup was %j, no need to backup now', backups[0]);
|
|
|
|
|
return callback(null);
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-02 18:51:50 -07:00
|
|
|
backup(auditSource, callback);
|
2016-04-10 22:24:01 -07:00
|
|
|
});
|
|
|
|
|
}
|
2016-04-10 19:17:44 -07:00
|
|
|
|
|
|
|
|
function restoreApp(app, addonsToRestore, backupId, callback) {
|
|
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof addonsToRestore, 'object');
|
|
|
|
|
assert.strictEqual(typeof backupId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
assert(app.lastBackupId);
|
|
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
2017-09-07 20:10:07 -07:00
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
var startTime = new Date();
|
2017-09-12 14:51:58 -07:00
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
async.series([
|
|
|
|
|
download.bind(null, backupId, appDataDir),
|
|
|
|
|
addons.restoreAddons.bind(null, app, addonsToRestore)
|
|
|
|
|
], function (error) {
|
|
|
|
|
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
2017-09-12 14:51:58 -07:00
|
|
|
|
2017-09-20 09:57:16 -07:00
|
|
|
callback(error);
|
2017-04-20 16:30:11 -07:00
|
|
|
});
|
2016-04-10 19:17:44 -07:00
|
|
|
}
|
2016-09-16 18:14:36 +02: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();
|
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
|
|
|
|
2017-06-01 10:39:07 -07:00
|
|
|
async.eachSeries(appBackups, function iterator(backup, iteratorDone) {
|
|
|
|
|
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
|
|
|
|
|
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
|
2017-04-24 12:01:50 +02:00
|
|
|
|
2017-09-23 11:09:36 -07:00
|
|
|
debug('cleanupAppBackups: removing %s', backup.id);
|
2017-04-23 11:34:46 -07:00
|
|
|
|
2017-09-23 11:09:36 -07:00
|
|
|
api(backupConfig.provider).remove(backupConfig, getBackupFilePath(backupConfig, backup.id), function (error) {
|
2017-06-01 10:39:07 -07:00
|
|
|
if (error) {
|
2017-09-23 11:09:36 -07:00
|
|
|
debug('cleanupAppBackups: error removing backup %j : %s', backup, error.message);
|
2017-06-01 10:39:07 -07:00
|
|
|
iteratorDone();
|
|
|
|
|
}
|
2016-10-10 15:04:28 +02:00
|
|
|
|
2017-06-01 10:39:07 -07:00
|
|
|
backupdb.del(backup.id, function (error) {
|
2017-09-23 11:09:36 -07:00
|
|
|
if (error) debug('cleanupAppBackups: error removing from database', error);
|
|
|
|
|
else debug('cleanupAppBackups: removed %s', backup.id);
|
2017-04-23 11:34:46 -07:00
|
|
|
|
2017-06-01 10:39:07 -07:00
|
|
|
iteratorDone();
|
2017-04-23 11:34:46 -07:00
|
|
|
});
|
2017-05-30 13:18:58 -07:00
|
|
|
});
|
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
|
|
|
|
|
|
|
|
callback();
|
2017-05-30 13:18:58 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-04-24 13:41:23 +02:00
|
|
|
|
2017-05-30 13:18:58 -07:00
|
|
|
function cleanupBoxBackups(backupConfig, callback) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
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();
|
|
|
|
|
var referencedAppBackups = [];
|
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
|
|
|
|
2017-05-30 13:18:58 -07:00
|
|
|
if (boxBackups.length === 0) return callback(null, []);
|
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-23 11:09:36 -07:00
|
|
|
debug('cleanupBoxBackups: preserving box backup %j', boxBackups[i]);
|
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
|
|
|
|
2017-05-30 13:18:58 -07:00
|
|
|
async.eachSeries(boxBackups, function iterator(backup, iteratorDone) {
|
|
|
|
|
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
|
2017-04-24 13:41:23 +02:00
|
|
|
|
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
|
2017-05-30 13:18:58 -07:00
|
|
|
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
|
2017-04-24 13:41:23 +02:00
|
|
|
|
2017-09-23 11:09:36 -07:00
|
|
|
debug('cleanupBoxBackups: removing %s', backup.id);
|
2017-05-30 13:18:58 -07:00
|
|
|
|
2017-09-23 11:09:36 -07:00
|
|
|
var filePaths = [].concat(backup.id, backup.dependsOn).map(getBackupFilePath.bind(null, backupConfig));
|
2017-05-30 13:18:58 -07:00
|
|
|
|
2017-09-23 11:09:36 -07:00
|
|
|
async.eachSeries(filePaths, api(backupConfig.provider).remove.bind(null, backupConfig), function (error) {
|
2017-05-30 13:18:58 -07:00
|
|
|
if (error) {
|
2017-09-23 11:09:36 -07:00
|
|
|
debug('cleanupBoxBackups: error removing backup %j : %s', backup, error.message);
|
2017-05-30 13:18:58 -07:00
|
|
|
iteratorDone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backupdb.del(backup.id, function (error) {
|
2017-09-23 11:09:36 -07:00
|
|
|
if (error) debug('cleanupBoxBackups: error removing from database', error);
|
|
|
|
|
else debug('cleanupBoxBackups: removed %j', filePaths);
|
2017-05-30 13:18:58 -07:00
|
|
|
|
|
|
|
|
iteratorDone();
|
2017-04-24 13:41:23 +02:00
|
|
|
});
|
2016-10-10 16:20:37 +02:00
|
|
|
});
|
2017-05-30 13:18:58 -07:00
|
|
|
}, function () {
|
2017-09-23 11:09:36 -07:00
|
|
|
debug('cleanupBoxBackups: done');
|
|
|
|
|
|
2017-05-30 13:18:58 -07:00
|
|
|
return callback(null, referencedAppBackups);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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-09-23 11:09:36 -07:00
|
|
|
api(backupConfig.provider).remove(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${appId}`), function (/* ignoredError */) {
|
2017-09-17 23:45:06 -07:00
|
|
|
setSnapshotInfo(appId, null);
|
|
|
|
|
|
|
|
|
|
iteratorDone();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 13:18:58 -07:00
|
|
|
function cleanup(callback) {
|
|
|
|
|
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
|
|
|
|
|
|
|
|
|
|
callback = callback || NOOP_CALLBACK;
|
|
|
|
|
|
|
|
|
|
settings.getBackupConfig(function (error, backupConfig) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
if (backupConfig.retentionSecs < 0) {
|
|
|
|
|
debug('cleanup: keeping all backups');
|
|
|
|
|
return callback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanupBoxBackups(backupConfig, function (error, referencedAppBackups) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2017-09-17 23:45:06 -07:00
|
|
|
cleanupAppBackups(backupConfig, referencedAppBackups, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
cleanupSnapshots(backupConfig, callback);
|
|
|
|
|
});
|
2016-10-10 15:04:28 +02:00
|
|
|
});
|
|
|
|
|
});
|
2016-10-10 16:10:51 +02:00
|
|
|
}
|