diff --git a/CHANGES b/CHANGES index 0c38984f3..035108bde 100644 --- a/CHANGES +++ b/CHANGES @@ -1741,4 +1741,6 @@ * Hide access tokens from logs * Add missing '@' sign for email address in app mailbox * Add app fqdn to backup progress message +* import: add option to import app in-place +* import: add option to import app from arbitrary backup config diff --git a/src/apps.js b/src/apps.js index 53d1ead7f..af25909b1 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1450,28 +1450,40 @@ function importApp(appId, data, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); - error = validateBackupFormat(data.backupFormat); + // all fields are optional + data.backupId = data.backupId || null; + data.backupFormat = data.backupFormat || null; + data.backupConfig = data.backupConfig || null; + const { backupId, backupFormat, backupConfig } = data; + + error = backupFormat ? validateBackupFormat(backupFormat) : null; if (error) return callback(error); error = checkAppState(app, exports.ISTATE_PENDING_RESTORE); if (error) return callback(error); - // TODO: check if the file exists in the storage backend - const restoreConfig = { backupId: data.backupId, backupFormat: data.backupFormat, oldManifest: app.manifest }; + // TODO: make this smarter to do a read-only test and check if the file exists in the storage backend + const testBackupConfig = backupConfig ? backups.testConfig.bind(null, backupConfig) : (next) => next(); - const task = { - args: { - restoreConfig, - overwriteDns: true - }, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { + testBackupConfig(function (error) { if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: data.backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId }); + const restoreConfig = { backupId, backupFormat, backupConfig, oldManifest: app.manifest }; - callback(null, { taskId: result.taskId }); + const task = { + args: { + restoreConfig, + overwriteDns: true + }, + values: {} + }; + addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); }); }); } diff --git a/src/backups.js b/src/backups.js index 2e0350fb5..7e848d3e5 100644 --- a/src/backups.js +++ b/src/backups.js @@ -658,13 +658,20 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); + if (!restoreConfig.backupId) { // in-place import + debug('restoreApp: in-place import'); + + return addons.restoreAddons(app, addonsToRestore, callback); + } + 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' }] : []); - var startTime = new Date(); + const startTime = new Date(); + const getBackupConfigFunc = restoreConfig.backupConfig ? (next) => next(null, restoreConfig.backupConfig) : settings.getBackupConfig; - settings.getBackupConfig(function (error, backupConfig) { + getBackupConfigFunc(function (error, backupConfig) { if (error) return callback(error); async.series([ diff --git a/src/routes/apps.js b/src/routes/apps.js index 8270e5f46..2b4c16f98 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -395,8 +395,24 @@ function importApp(req, res, next) { debug('Importing app id:%s', req.params.id); - if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string')); - if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string')); + if ('backupId' in data) { // if not provided, we import in-place + if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string')); + if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string')); + + if ('backupConfig' in data && typeof data.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object')); + + const backupConfig = req.body.backupConfig; + + if (req.body.backupConfig) { + if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required')); + if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string')); + if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string')); + if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean')); + + // testing backup config can take sometime + req.clearTimeout(); + } + } apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error));