diff --git a/src/apps.js b/src/apps.js index cc4f9015f..93987d81b 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1420,8 +1420,15 @@ async function install(data, auditSource) { if (constants.DEMO && (await getCount() >= constants.DEMO_APP_LIMIT)) throw new BoxError(BoxError.BAD_STATE, 'Too many installed apps, please uninstall a few and try again'); + let restoreConfig = null; + if ('backupId' in data) { // install from archive + const backup = await backups.get(data.backupId); + if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'Backup not found in archive'); + restoreConfig = { remotePath: backup.remotePath, backupFormat: backup.format }; + } + const appId = uuid.v4(); - debug('Will install app with id : ' + appId); + debug(`Installing app ${appId}`); const app = { accessRestriction, @@ -1455,7 +1462,7 @@ async function install(data, auditSource) { await purchaseApp({ appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }); const task = { - args: { restoreConfig: null, skipDnsSetup, overwriteDns }, + args: { restoreConfig, skipDnsSetup, overwriteDns }, values: { }, requiredState: app.installationState }; diff --git a/src/backups.js b/src/backups.js index 6d85f0495..025521db8 100644 --- a/src/backups.js +++ b/src/backups.js @@ -10,6 +10,12 @@ exports = module.exports = { list, del, + archives: { + get: archivesGet, + list: archivesList, + del: archivesDel + }, + startBackupTask, startCleanupTask, @@ -171,6 +177,34 @@ async function getByTypePaged(type, page, perPage) { return results; } +async function archivesGet(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups WHERE id = ? AND archive = 1 ORDER BY creationTime DESC`, [ id ]); + if (result.length === 0) return null; + + return postProcess(result[0]); +} + +async function archivesList(page, perPage) { + assert(typeof page === 'number' && page > 0); + assert(typeof perPage === 'number' && perPage > 0); + + const results = await database.query(`SELECT ${BACKUPS_FIELDS} FROM backups WHERE archive = 1 ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]); + + results.forEach(function (result) { postProcess(result); }); + + return results; +} + +async function archivesDel(id, auditSource) { + assert.strictEqual(typeof id, 'string'); + assert(auditSource && typeof auditSource === 'object'); + + const result = await database.query('UPDATE backups SET archive = 0 WHERE id=?', [ id ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found in archive'); +} + function validateLabel(label) { assert.strictEqual(typeof label, 'string'); diff --git a/src/routes/apps.js b/src/routes/apps.js index 6365489cc..b13cb3300 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -185,6 +185,8 @@ async function install(req, res, next) { if ('enableTurn' in data && typeof data.enableTurn !== 'boolean') return next(new HttpError(400, 'enableTurn must be boolean')); + if ('backupId' in data && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string')); + let [error, result] = await safe(appstore.downloadManifest(data.appStoreId, data.manifest)); if (error) return next(BoxError.toHttpError(error)); diff --git a/src/routes/archives.js b/src/routes/archives.js new file mode 100644 index 000000000..0b8e8b78f --- /dev/null +++ b/src/routes/archives.js @@ -0,0 +1,57 @@ +'use strict'; + +exports = module.exports = { + load, + + list, + get, + del +}; + +const assert = require('assert'), + { archives } = require('../backups.js'), + AuditSource = require('../auditsource.js'), + BoxError = require('../boxerror.js'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'); + +async function load(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + const [error, result] = await safe(archives.get(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Backup not found')); + + req.resource = result; + + next(); +} + +async function list(req, res, next) { + const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; + if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number')); + + const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; + if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); + + const [error, result] = await safe(archives.list(page, perPage)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, { archives: result })); +} + +async function get(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + next(new HttpSuccess(200, req.resource)); +} + +async function del(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + const [error] = await safe(archives.del(req.resource.id, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204)); +} diff --git a/src/routes/index.js b/src/routes/index.js index df8cba903..141a05c76 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,6 +6,7 @@ exports = module.exports = { apps: require('./apps.js'), applinks: require('./applinks.js'), appstore: require('./appstore.js'), + archives: require('./archives.js'), auth: require('./auth.js'), backups: require('./backups.js'), branding: require('./branding.js'), diff --git a/src/routes/test/archives-test.js b/src/routes/test/archives-test.js new file mode 100644 index 000000000..0c76d8078 --- /dev/null +++ b/src/routes/test/archives-test.js @@ -0,0 +1,81 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const backups = require('../../backups.js'), + common = require('./common.js'), + expect = require('expect.js'), + superagent = require('superagent'); + +describe('Archives API', function () { + const { setup, cleanup, serverUrl, owner } = common; + + const nonArchiveBackup = { + id: null, + remotePath: 'app_appid_123', + encryptionVersion: null, + packageVersion: '1.0.0', + type: backups.BACKUP_TYPE_APP, + state: backups.BACKUP_STATE_CREATING, + identifier: 'appid', + dependsOn: [ ], + manifest: { foo: 'bar' }, + format: 'tgz', + preserveSecs: 0, + label: '', + archive: false + }; + + const archiveBackup = Object.assign({}, nonArchiveBackup, {archive: true, remotePath: 'app_appid_234'}); + + before(async function () { + await setup(); + nonArchiveBackup.id = await backups.add(nonArchiveBackup); + archiveBackup.id = await backups.add(archiveBackup); + await backups.update(archiveBackup.id, { archive: true }); + }); + after(cleanup); + + it('list succeeds', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/archives`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.archives.length).to.be(1); + expect(response.body.archives[0].id).to.be(archiveBackup.id); + }); + + it('get valid archive', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/archives/${archiveBackup.id}`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.remotePath).to.be('app_appid_234'); + }); + + it('cannot get invalid archive', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/archives/random`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(404); + }); + + it('cannot del invalid archive', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/archives/${nonArchiveBackup.id}`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(404); + }); + + it('del valid archive', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/archives/${archiveBackup.id}`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(204); + + const response2 = await superagent.get(`${serverUrl}/api/v1/archives`) + .query({ access_token: owner.token }); + expect(response2.statusCode).to.equal(200); + expect(response2.body.archives.length).to.be(0); + }); +}); diff --git a/src/server.js b/src/server.js index 20b8ed1fa..25281e1df 100644 --- a/src/server.js +++ b/src/server.js @@ -161,6 +161,11 @@ async function initializeExpressSync() { router.post('/api/v1/backups/policy', json, token, authorizeOwner, routes.backups.setPolicy); router.post('/api/v1/backups/:backupId', json, token, authorizeAdmin, routes.backups.update); + // app archive routes + router.get ('/api/v1/archives', token, authorizeAdmin, routes.archives.list); + router.get ('/api/v1/archives/:id', token, authorizeAdmin, routes.archives.load, routes.archives.get); + router.del ('/api/v1/archives/:id', token, authorizeAdmin, routes.archives.load, routes.archives.del); + // working off the user behind the provided token router.get ('/api/v1/profile', token, authorizeUser, routes.profile.get); router.post('/api/v1/profile/display_name', json, token, authorizeUser, routes.profile.canEditProfile, routes.profile.setDisplayName);