backups: add backup multiple targets
This commit is contained in:
+259
-250
@@ -1,40 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get,
|
||||
list,
|
||||
add,
|
||||
del,
|
||||
|
||||
setConfig,
|
||||
setLimits,
|
||||
setSchedule,
|
||||
setRetention,
|
||||
|
||||
removePrivateFields,
|
||||
|
||||
startBackupTask,
|
||||
|
||||
startCleanupTask,
|
||||
cleanupCacheFilesSync,
|
||||
|
||||
removePrivateFields,
|
||||
|
||||
generateEncryptionKeysSync,
|
||||
|
||||
getSnapshotInfo,
|
||||
setSnapshotInfo,
|
||||
|
||||
validatePolicy,
|
||||
testStorage,
|
||||
validateFormat,
|
||||
|
||||
getPolicy,
|
||||
setPolicy,
|
||||
|
||||
getTarget,
|
||||
|
||||
getConfig,
|
||||
setConfig,
|
||||
setStorage,
|
||||
setLimits,
|
||||
|
||||
getRootPath,
|
||||
setupManagedStorage,
|
||||
|
||||
remount,
|
||||
getMountStatus,
|
||||
ensureMounted,
|
||||
|
||||
_addDefaultTarget: addDefaultTarget,
|
||||
_addDefault: addDefault,
|
||||
_getDefault: getDefault,
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
@@ -43,22 +39,35 @@ const assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
{ CronTime } = require('cron'),
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hush = require('./hush.js'),
|
||||
locks = require('./locks.js'),
|
||||
mounts = require('./mounts.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
storage = require('./storage.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
uuid = require('uuid'),
|
||||
_ = require('./underscore.js');
|
||||
uuid = require('uuid');
|
||||
|
||||
const BACKUP_TARGET_FIELDS = [ 'id', 'label', 'configJson', 'limitsJson', 'retentionJson', 'schedule', 'encryptionJson', 'format', 'priority', 'creationTime', 'ts' ].join(',');
|
||||
const BACKUP_TARGET_FIELDS = [ 'id', 'label', 'provider', 'configJson', 'limitsJson', 'retentionJson', 'schedule', 'encryptionJson', 'format', 'priority', 'creationTime', 'ts' ].join(',');
|
||||
|
||||
function getRootPath(provider, config, mountPath) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof mountPath, 'string');
|
||||
|
||||
if (mounts.isManagedProvider(provider)) {
|
||||
return path.join(mountPath, config.prefix);
|
||||
} else if (provider === 'mountpoint') {
|
||||
return path.join(config.mountPoint, config.prefix);
|
||||
} else if (provider === 'filesystem') {
|
||||
return config.backupFolder;
|
||||
} else {
|
||||
return config.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -66,6 +75,10 @@ function postProcess(result) {
|
||||
result.config = result.configJson ? safe.JSON.parse(result.configJson) : {};
|
||||
delete result.configJson;
|
||||
|
||||
// note: rootPath will be dynamic for managed mount providers during app import . since it's used in api backends it has to be inside config
|
||||
result.config.rootPath = getRootPath(result.provider, result.config, paths.MANAGED_BACKUP_MOUNT_DIR);
|
||||
result.config.provider = result.provider; // this allows api backends to identify the real provider
|
||||
|
||||
result.limits = result.limitsJson ? safe.JSON.parse(result.limitsJson) : {};
|
||||
delete result.limitsJson;
|
||||
|
||||
@@ -80,36 +93,45 @@ function postProcess(result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function removePrivateFields(backupConfig) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
if (backupConfig.encryption) {
|
||||
delete backupConfig.encryption;
|
||||
backupConfig.password = constants.SECRET_PLACEHOLDER;
|
||||
function removePrivateFields(target) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
|
||||
if (target.encryption) {
|
||||
delete target.encryption;
|
||||
target.password = constants.SECRET_PLACEHOLDER;
|
||||
}
|
||||
delete backupConfig.rootPath;
|
||||
return storage.api(backupConfig.provider).removePrivateFields(backupConfig);
|
||||
delete target.rootPath;
|
||||
return storage.api(target.provider).removePrivateFields(target.config);
|
||||
}
|
||||
|
||||
// this function is used in migrations - 20200512172301-settings-backup-encryption.js
|
||||
function generateEncryptionKeysSync(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
function validateFormat(format) {
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
const aesKeys = crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 128);
|
||||
return {
|
||||
dataKey: aesKeys.subarray(0, 32).toString('hex'),
|
||||
dataHmacKey: aesKeys.subarray(32, 64).toString('hex'),
|
||||
filenameKey: aesKeys.subarray(64, 96).toString('hex'),
|
||||
filenameHmacKey: aesKeys.subarray(96).toString('hex')
|
||||
};
|
||||
if (format === 'tgz' || format == 'rsync') return null;
|
||||
|
||||
return new BoxError(BoxError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
async function validatePolicy(policy) {
|
||||
assert.strictEqual(typeof policy, 'object');
|
||||
function validateLabel(label) {
|
||||
assert.strictEqual(typeof label, 'string');
|
||||
|
||||
const job = safe.safeCall(function () { return new CronTime(policy.schedule); });
|
||||
if (label.length > 48) return new BoxError(BoxError.BAD_FIELD, 'Label too long');
|
||||
}
|
||||
|
||||
function validateSchedule(schedule) {
|
||||
assert.strictEqual(typeof schedule, 'string');
|
||||
|
||||
if (schedule === constants.CRON_PATTERN_NEVER) return null;
|
||||
|
||||
const job = safe.safeCall(function () { return new CronTime(schedule); });
|
||||
if (!job) return new BoxError(BoxError.BAD_FIELD, 'Invalid schedule pattern');
|
||||
|
||||
const retention = policy.retention;
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateRetention(retention) {
|
||||
assert.strictEqual(typeof retention, 'object');
|
||||
|
||||
if (!retention) return new BoxError(BoxError.BAD_FIELD, 'retention is required');
|
||||
if (!['keepWithinSecs','keepDaily','keepWeekly','keepMonthly','keepYearly'].find(k => !!retention[k])) return new BoxError(BoxError.BAD_FIELD, 'retention properties missing');
|
||||
if ('keepWithinSecs' in retention && typeof retention.keepWithinSecs !== 'number') return new BoxError(BoxError.BAD_FIELD, 'retention.keepWithinSecs must be a number');
|
||||
@@ -117,17 +139,133 @@ async function validatePolicy(policy) {
|
||||
if ('keepWeekly' in retention && typeof retention.keepWeekly !== 'number') return new BoxError(BoxError.BAD_FIELD, 'retention.keepWeekly must be a number');
|
||||
if ('keepMonthly' in retention && typeof retention.keepMonthly !== 'number') return new BoxError(BoxError.BAD_FIELD, 'retention.keepMonthly must be a number');
|
||||
if ('keepYearly' in retention && typeof retention.keepYearly !== 'number') return new BoxError(BoxError.BAD_FIELD, 'retention.keepYearly must be a number');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function startBackupTask(auditSource) {
|
||||
function validateEncryptionPassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 8) return new BoxError(BoxError.BAD_FIELD, 'password must be atleast 8 characters');
|
||||
}
|
||||
|
||||
async function list(page, perPage) {
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]);
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function getDefault() {
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE priority=? LIMIT 1`, [ true ]);
|
||||
return postProcess(results[0]);
|
||||
}
|
||||
|
||||
async function get(id) {
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE id=?`, [ id ]);
|
||||
if (results.length === 0) return null;
|
||||
return postProcess(results[0]);
|
||||
}
|
||||
|
||||
async function addDefault() {
|
||||
const label = '', priority = true;
|
||||
const limits = null, encryption = null;
|
||||
const retention = { keepWithinSecs: 2 * 24 * 60 * 60 };
|
||||
const schedule = '00 00 23 * * *';;
|
||||
const config = { backupFolder: paths.DEFAULT_BACKUP_DIR };
|
||||
const provider = 'filesystem';
|
||||
const format = 'tgz';
|
||||
|
||||
const id = `bc-${uuid.v4()}`;
|
||||
await database.query('INSERT INTO backupTargets (id, label, provider, configJson, limitsJson, retentionJson, schedule, encryptionJson, format, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, label, provider, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, priority ]);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function update(target, data) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
|
||||
const args = [];
|
||||
const fields = [];
|
||||
for (const k in data) {
|
||||
if (k === 'label' || k === 'schedule' || k === 'priority') { // format, provider cannot be updated
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
} else if (k === 'config' || k === 'limits' || k === 'retention') { // encryption cannot be updated
|
||||
fields.push(`${k}JSON = ?`);
|
||||
args.push(JSON.stringify(data[k]));
|
||||
}
|
||||
}
|
||||
args.push(target.id);
|
||||
|
||||
const [updateError, result] = await safe(database.query('UPDATE backupTargets SET ' + fields.join(', ') + ' WHERE id = ?', args));
|
||||
if (updateError) throw updateError;
|
||||
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found');
|
||||
}
|
||||
|
||||
async function setSchedule(target, schedule) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert.strictEqual(typeof schedule, 'string');
|
||||
|
||||
const error = await validateSchedule(schedule);
|
||||
if (error) throw error;
|
||||
|
||||
await update(target, { schedule });
|
||||
|
||||
await cron.handleBackupScheduleChanged(target);
|
||||
}
|
||||
|
||||
async function setLimits(target, limits) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert.strictEqual(typeof limits, 'object');
|
||||
|
||||
await update(target, { limits });
|
||||
}
|
||||
|
||||
async function setRetention(target, retention) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert.strictEqual(typeof retention, 'object');
|
||||
|
||||
const error = await validateRetention(retention);
|
||||
if (error) throw error;
|
||||
|
||||
await update(target, { retention });
|
||||
}
|
||||
|
||||
async function del(target, auditSource) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert(auditSource && typeof auditSource === 'object');
|
||||
|
||||
if (target.priority) throw new BoxError(BoxError.CONFLICT, 'Cannot delete the primary backup target');
|
||||
|
||||
const queries = [];
|
||||
queries.push({ query: 'DELETE FROM backups WHERE targetId = ?', args: [ target.id ] });
|
||||
queries.push({ query: 'DELETE FROM backupTargets WHERE id=? AND priority=?', args: [ target.id, false ] });
|
||||
|
||||
const [error, result] = await safe(database.transaction(queries));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error);
|
||||
if (error) throw error;
|
||||
if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found');
|
||||
// await eventlog.add(eventlog.ACTION_ARCHIVES_DEL, auditSource, { id: archive.id, backupId: archive.backupId });
|
||||
|
||||
debug('del: clearing backup cache');
|
||||
cleanupCacheFilesSync();
|
||||
}
|
||||
|
||||
async function startBackupTask(target, auditSource) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
|
||||
const [error] = await safe(locks.acquire(locks.TYPE_FULL_BACKUP_TASK));
|
||||
if (error) throw new BoxError(BoxError.BAD_STATE, `Another backup task is in progress: ${error.message}`);
|
||||
|
||||
const backupConfig = await getConfig();
|
||||
const memoryLimit = target.limits?.memoryLimit ? Math.max(target.limits.memoryLimit/1024/1024, 1024) : 1024;
|
||||
|
||||
const memoryLimit = backupConfig.limits?.memoryLimit ? Math.max(backupConfig.limits.memoryLimit/1024/1024, 1024) : 1024;
|
||||
|
||||
const taskId = await tasks.add(tasks.TASK_BACKUP, [ { /* options */ } ]);
|
||||
const taskId = await tasks.add(`${tasks.TASK_FULL_BACKUP_PREFIX}${target.id}`, [ target.id, { /* options */ } ]);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
|
||||
@@ -150,8 +288,8 @@ async function startBackupTask(auditSource) {
|
||||
}
|
||||
|
||||
// this function is used in migrations - 20200512172301-settings-backup-encryption.js
|
||||
function cleanupCacheFilesSync() {
|
||||
const files = safe.fs.readdirSync(path.join(paths.BACKUP_INFO_DIR));
|
||||
function cleanupCacheFilesSync(target) {
|
||||
const files = safe.fs.readdirSync(path.join(paths.BACKUP_INFO_DIR, target.id));
|
||||
if (!files) return;
|
||||
|
||||
files
|
||||
@@ -183,10 +321,11 @@ async function setSnapshotInfo(id, info) {
|
||||
}
|
||||
}
|
||||
|
||||
async function startCleanupTask(auditSource) {
|
||||
async function startCleanupTask(target, auditSource) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const taskId = await tasks.add(tasks.TASK_CLEAN_BACKUPS, []);
|
||||
const taskId = await tasks.add(`${tasks.TASK_CLEAN_BACKUPS_PREFIX}${target.id}`, [ target.id ]);
|
||||
|
||||
// background
|
||||
tasks.startTask(taskId, {})
|
||||
@@ -201,239 +340,109 @@ async function startCleanupTask(auditSource) {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function testStorage(storageConfig) {
|
||||
assert.strictEqual(typeof storageConfig, 'object');
|
||||
|
||||
const func = storage.api(storageConfig.provider);
|
||||
if (!func) return new BoxError(BoxError.BAD_FIELD, 'unknown storage provider');
|
||||
|
||||
await storage.api(storageConfig.provider).testConfig(storageConfig);
|
||||
}
|
||||
|
||||
function validateEncryptionPassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 8) return new BoxError(BoxError.BAD_FIELD, 'password must be atleast 8 characters');
|
||||
}
|
||||
|
||||
function managedBackupMountObject(backupConfig) {
|
||||
assert(mounts.isManagedProvider(backupConfig.provider));
|
||||
function managedBackupMountObject(config) {
|
||||
assert(mounts.isManagedProvider(config.provider));
|
||||
|
||||
return {
|
||||
name: 'backup',
|
||||
hostPath: paths.MANAGED_BACKUP_MOUNT_DIR,
|
||||
mountType: backupConfig.provider,
|
||||
mountOptions: backupConfig.mountOptions
|
||||
mountType: config.provider,
|
||||
mountOptions: config.mountOptions
|
||||
};
|
||||
}
|
||||
|
||||
async function remount() {
|
||||
const backupConfig = await getConfig();
|
||||
async function remount(target) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
|
||||
if (mounts.isManagedProvider(backupConfig.provider)) {
|
||||
await mounts.remount(managedBackupMountObject(backupConfig));
|
||||
if (mounts.isManagedProvider(target.provider)) {
|
||||
await mounts.remount(managedBackupMountObject(target.config));
|
||||
}
|
||||
}
|
||||
|
||||
async function getMountStatus() {
|
||||
const backupConfig = await getConfig();
|
||||
async function getMountStatus(target) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
|
||||
let hostPath;
|
||||
if (mounts.isManagedProvider(backupConfig.provider)) {
|
||||
if (mounts.isManagedProvider(target.provider)) {
|
||||
hostPath = paths.MANAGED_BACKUP_MOUNT_DIR;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
hostPath = backupConfig.mountPoint;
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
hostPath = backupConfig.backupFolder;
|
||||
} else if (target.provider === 'mountpoint') {
|
||||
hostPath = target.config.mountPoint;
|
||||
} else if (target.provider === 'filesystem') {
|
||||
hostPath = target.config.backupFolder;
|
||||
} else {
|
||||
return { state: 'active' };
|
||||
}
|
||||
|
||||
return await mounts.getStatus(backupConfig.provider, hostPath); // { state, message }
|
||||
return await mounts.getStatus(target.provider, hostPath); // { state, message }
|
||||
}
|
||||
|
||||
async function ensureMounted() {
|
||||
const status = await getMountStatus();
|
||||
async function ensureMounted(target) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
|
||||
const status = await getMountStatus(target);
|
||||
if (status.state === 'active') return status;
|
||||
|
||||
await remount();
|
||||
return await getMountStatus();
|
||||
return await getMountStatus(target);
|
||||
}
|
||||
|
||||
async function getPolicy() {
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE priority=?`, [ true ]);
|
||||
const result = postProcess(results[0]);
|
||||
return { retention: result.retention, schedule: result.schedule };
|
||||
}
|
||||
|
||||
async function setPolicy(policy) {
|
||||
assert.strictEqual(typeof policy, 'object');
|
||||
|
||||
const error = await validatePolicy(policy);
|
||||
if (error) throw error;
|
||||
|
||||
await updateTarget(policy);
|
||||
|
||||
await cron.handleBackupPolicyChanged(policy);
|
||||
}
|
||||
|
||||
function getRootPath(storageConfig, mountPath) {
|
||||
assert.strictEqual(typeof storageConfig, 'object');
|
||||
assert.strictEqual(typeof mountPath, 'string');
|
||||
|
||||
if (mounts.isManagedProvider(storageConfig.provider)) {
|
||||
return path.join(mountPath, storageConfig.prefix);
|
||||
} else if (storageConfig.provider === 'mountpoint') {
|
||||
return path.join(storageConfig.mountPoint, storageConfig.prefix);
|
||||
} else if (storageConfig.provider === 'filesystem') {
|
||||
return storageConfig.backupFolder;
|
||||
} else {
|
||||
return storageConfig.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTarget(data) {
|
||||
assert(data && typeof data === 'object');
|
||||
|
||||
const args = [];
|
||||
const fields = [];
|
||||
for (const k in data) {
|
||||
if (k === 'label' || k === 'schedule' || k === 'format' || k === 'priority') {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
} else if (k === 'config' || k === 'limits' || k === 'retention' || k === 'encryption') {
|
||||
fields.push(`${k}JSON = ?`);
|
||||
args.push(JSON.stringify(data[k]));
|
||||
}
|
||||
}
|
||||
args.push(true); // primary flag
|
||||
|
||||
const [updateError, result] = await safe(database.query('UPDATE backupTargets SET ' + fields.join(', ') + ' WHERE priority = ?', args));
|
||||
if (updateError) throw updateError;
|
||||
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found');
|
||||
}
|
||||
|
||||
async function addDefaultTarget() {
|
||||
const label = '', priority = true;
|
||||
const limits = null, encryption = null;
|
||||
const retention = { keepWithinSecs: 2 * 24 * 60 * 60 };
|
||||
const schedule = '00 00 23 * * *';;
|
||||
const config = { provider: 'filesystem', backupFolder: paths.DEFAULT_BACKUP_DIR };
|
||||
const format = 'tgz';
|
||||
|
||||
const id = `bc-${uuid.v4()}`;
|
||||
await database.query('INSERT INTO backupTargets (id, label, configJson, limitsJson, retentionJson, schedule, encryptionJson, format, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, label, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, priority ]);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function getTarget(id) {
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE id=?`, [ id ]);
|
||||
if (results.length === 0) return null;
|
||||
return postProcess(results[0]);
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE priority=?`, [ true ]);
|
||||
const result = postProcess(results[0]);
|
||||
|
||||
const config = result.config;
|
||||
config.format = result.format;
|
||||
config.encryption = result.encryption;
|
||||
config.rootPath = getRootPath(config, paths.MANAGED_BACKUP_MOUNT_DIR); // note: rootPath will be dynamic for managed mount providers during app import
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function setConfig(backupConfig) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
|
||||
await updateTarget({
|
||||
config: _.omit(backupConfig, ['limits', 'format', 'encryption']),
|
||||
format: backupConfig.format,
|
||||
encryption: backupConfig.encryption || null,
|
||||
limits: backupConfig.limits || null
|
||||
});
|
||||
}
|
||||
|
||||
async function setLimits(limits) {
|
||||
assert.strictEqual(typeof limits, 'object');
|
||||
|
||||
await settings.setJson(settings.BACKUP_LIMITS_KEY, limits);
|
||||
}
|
||||
|
||||
function validateFormat(format) {
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
if (format === 'tgz' || format == 'rsync') return null;
|
||||
|
||||
return new BoxError(BoxError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
async function setupManagedStorage(storageConfig, hostPath) {
|
||||
assert.strictEqual(typeof storageConfig, 'object');
|
||||
assert.strictEqual(typeof hostPath, 'string');
|
||||
|
||||
if (!mounts.isManagedProvider(storageConfig.provider)) return null;
|
||||
|
||||
if (!storageConfig.mountOptions || typeof storageConfig.mountOptions !== 'object') throw new BoxError(BoxError.BAD_FIELD, 'mountOptions must be an object');
|
||||
|
||||
const error = mounts.validateMountOptions(storageConfig.provider, storageConfig.mountOptions);
|
||||
if (error) throw error;
|
||||
|
||||
debug(`setupManagedStorage: setting up mount at ${hostPath} with ${storageConfig.provider}`);
|
||||
|
||||
const newMount = {
|
||||
name: path.basename(hostPath),
|
||||
hostPath,
|
||||
mountType: storageConfig.provider,
|
||||
mountOptions: storageConfig.mountOptions
|
||||
};
|
||||
|
||||
await mounts.tryAddMount(newMount, { timeout: 10 }); // 10 seconds
|
||||
|
||||
return newMount;
|
||||
}
|
||||
|
||||
async function setStorage(storageConfig) {
|
||||
assert.strictEqual(typeof storageConfig, 'object');
|
||||
async function setConfig(target, newConfig) {
|
||||
assert.strictEqual(typeof target, 'object');
|
||||
assert.strictEqual(typeof newConfig, 'object');
|
||||
|
||||
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
|
||||
|
||||
const oldConfig = await getConfig();
|
||||
const oldConfig = target.config;
|
||||
|
||||
if (storageConfig.provider === oldConfig.provider) storage.api(storageConfig.provider).injectPrivateFields(storageConfig, oldConfig);
|
||||
storage.api(target.provider).injectPrivateFields(newConfig, oldConfig);
|
||||
|
||||
const formatError = validateFormat(storageConfig.format);
|
||||
debug('setConfig: validating new storage configuration');
|
||||
await storage.testMount(target.provider, newConfig, '/mnt/backup-storage-validation');
|
||||
|
||||
debug('setConfig: removing old storage configuration');
|
||||
if (mounts.isManagedProvider(target.provider)) await safe(mounts.removeMount(managedBackupMountObject(oldConfig)));
|
||||
|
||||
debug('setConfig: setting up new storage configuration');
|
||||
await storage.setupManagedMount(target.provider, newConfig, paths.MANAGED_BACKUP_MOUNT_DIR);
|
||||
|
||||
debug('setConfig: clearing backup cache');
|
||||
cleanupCacheFilesSync(target);
|
||||
|
||||
await update(target, { config: newConfig });
|
||||
}
|
||||
|
||||
async function add(data) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
|
||||
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
|
||||
|
||||
const { provider, label, config, format, retention, schedule } = data; // required
|
||||
const limits = data.limits || null,
|
||||
encryptionPassword = data.encryptionPassword || null,
|
||||
encryptedFilenames = data.encryptedFilenames || false;
|
||||
|
||||
const formatError = validateFormat(format);
|
||||
if (formatError) throw formatError;
|
||||
|
||||
storageConfig.encryption = null;
|
||||
if ('password' in storageConfig) { // user set password
|
||||
if (storageConfig.password === constants.SECRET_PLACEHOLDER) {
|
||||
storageConfig.encryption = oldConfig.encryption || null;
|
||||
} else {
|
||||
const encryptionPasswordError = validateEncryptionPassword(storageConfig.password);
|
||||
if (encryptionPasswordError) throw encryptionPasswordError;
|
||||
const labelError = validateLabel(label);
|
||||
if (labelError) throw labelError;
|
||||
|
||||
storageConfig.encryption = generateEncryptionKeysSync(storageConfig.password);
|
||||
}
|
||||
delete storageConfig.password;
|
||||
let encryption = null;
|
||||
if (encryptionPassword) {
|
||||
const encryptionPasswordError = validateEncryptionPassword(encryptionPassword);
|
||||
if (encryptionPasswordError) throw encryptionPasswordError;
|
||||
encryption = hush.generateEncryptionKeysSync(encryptionPassword);
|
||||
encryption.encryptedFilenames = !!encryptedFilenames;
|
||||
}
|
||||
|
||||
debug('setStorage: validating new storage configuration');
|
||||
const testMountObject = await setupManagedStorage(storageConfig, '/mnt/backup-storage-validation'); // this validates mountOptions
|
||||
const testStorageError = await testStorage(Object.assign({ mountPath: '/mnt/backup-storage-validation' }, storageConfig)); // this validates provider and it's api options. requires mountPath
|
||||
if (testMountObject) await mounts.removeMount(testMountObject);
|
||||
if (testStorageError) throw testStorageError;
|
||||
|
||||
debug('setStorage: removing old storage configuration');
|
||||
if (mounts.isManagedProvider(oldConfig.provider)) await safe(mounts.removeMount(managedBackupMountObject(oldConfig)));
|
||||
debug('add: validating new storage configuration');
|
||||
await storage.testMount(provider, config, '/mnt/backup-storage-validation');
|
||||
|
||||
debug('setStorage: setting up new storage configuration');
|
||||
await setupManagedStorage(storageConfig, paths.MANAGED_BACKUP_MOUNT_DIR);
|
||||
await storage.setupManagedMount(provider, config, paths.MANAGED_BACKUP_MOUNT_DIR);
|
||||
|
||||
debug('setStorage: clearing backup cache');
|
||||
cleanupCacheFilesSync();
|
||||
|
||||
await setConfig(storageConfig);
|
||||
const id = `bc-${uuid.v4()}`;
|
||||
await database.query('INSERT INTO backupTargets (id, label, provider, configJson, limitsJson, retentionJson, schedule, encryptionJson, format, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, label, provider, JSON.stringify(config), JSON.stringify(limits), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, false ]);
|
||||
return id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user