diff --git a/dashboard/public/views/app.js b/dashboard/public/views/app.js index 5cbfd3e24..4608ca262 100644 --- a/dashboard/public/views/app.js +++ b/dashboard/public/views/app.js @@ -1,6 +1,6 @@ 'use strict'; -/* global angular, localStorage, document, FileReader */ +/* global angular */ /* global $ */ /* global async */ /* global RSTATES */ diff --git a/dashboard/public/views/apps.js b/dashboard/public/views/apps.js index 343f37f91..098311450 100644 --- a/dashboard/public/views/apps.js +++ b/dashboard/public/views/apps.js @@ -4,7 +4,6 @@ /* global $:false */ /* global APP_TYPES */ /* global onAppClick */ -/* global localStorage, document, FileReader */ angular.module('Application').controller('AppsController', ['$scope', '$translate', '$interval', '$location', 'Client', function ($scope, $translate, $interval, $location, Client) { var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter diff --git a/dashboard/public/views/backups.js b/dashboard/public/views/backups.js index 341d30477..636090b1a 100644 --- a/dashboard/public/views/backups.js +++ b/dashboard/public/views/backups.js @@ -385,8 +385,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat submit: function () { $scope.archiveRestore.busy = true; - const app = $scope.archiveRestore.archive.appConfig; - var secondaryDomains = {}; for (var env2 in $scope.archiveRestore.secondaryDomains) { secondaryDomains[env2] = { diff --git a/migrations/20241209150823-archives-add-table.js b/migrations/20241209150823-archives-add-table.js index e019c0295..65bbc6bf3 100644 --- a/migrations/20241209150823-archives-add-table.js +++ b/migrations/20241209150823-archives-add-table.js @@ -7,7 +7,6 @@ exports.up = async function (db) { 'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' + 'appStoreIcon MEDIUMBLOB,' + 'icon MEDIUMBLOB,' + - 'appConfigJson TEXT,' + 'FOREIGN KEY(backupId) REFERENCES backups(id),' + 'PRIMARY KEY (id)) ' + 'CHARACTER SET utf8 COLLATE utf8_bin'; diff --git a/migrations/20241210192343-backups-add-appConfig.js b/migrations/20241210192343-backups-add-appConfig.js new file mode 100644 index 000000000..b655cac31 --- /dev/null +++ b/migrations/20241210192343-backups-add-appConfig.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE backups ADD COLUMN appConfigJson TEXT'); +}; + +exports.down = async function (db) { + await db.runSql('ALTER TABLE backups DROP COLUMN appConfigJson'); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index a03ef2f71..52d2ef062 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS backups( manifestJson TEXT, /* to validate if the app can be installed in this version of box */ format VARCHAR(16) DEFAULT "tgz", preserveSecs INTEGER DEFAULT 0, + appConfigJson TEXT, /* useful for clone and archive */ INDEX creationTime_index (creationTime), PRIMARY KEY (id)); @@ -166,7 +167,6 @@ CREATE TABLE IF NOT EXISTS archives( creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, appStoreIcon MEDIUMBLOB, icon MEDIUMBLOB, - appConfigJson TEXT, FOREIGN KEY(backupId) REFERENCES backups(id), PRIMARY KEY (id)); diff --git a/src/apps.js b/src/apps.js index 96873866c..d6f74822e 100644 --- a/src/apps.js +++ b/src/apps.js @@ -2440,10 +2440,10 @@ async function clone(app, data, user, auditSource) { const icons = await getIcons(app.id); // label, icon, checklist intentionally omitted - const dolly = _.pick(app, 'memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices', + const dolly = _.pick(backupInfo.appConfig || app, 'memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'debugMode', 'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso', - 'notes'); + 'notes', 'checklist'); if (!manifest.addons?.recvmail) dolly.inboxDomain = null; // needed because we are cloning _current_ app settings with old manifest @@ -2496,7 +2496,7 @@ async function unarchive(archive, data, auditSource) { domain = data.domain.toLowerCase(), overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false; - const appConfig = archive.appConfig; + const appConfig = backup.appConfig; const { appStoreId, manifest } = appConfig; let error = validateSecondaryDomains(data.secondaryDomains || {}, manifest); diff --git a/src/archives.js b/src/archives.js index 098d5484f..f1c041fba 100644 --- a/src/archives.js +++ b/src/archives.js @@ -17,7 +17,7 @@ const assert = require('assert'), safe = require('safetydance'), uuid = require('uuid'); -const ARCHIVE_FIELDS = [ 'id', 'backupId', 'creationTime', 'appConfigJson', '(icon IS NOT NULL) AS hasIcon', '(appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ]; +const ARCHIVE_FIELDS = [ 'archives.id', 'backupId', 'archives.creationTime', 'backups.appConfigJson', '(archives.icon IS NOT NULL) AS hasIcon', '(archives.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ]; function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -33,7 +33,7 @@ function postProcess(result) { async function get(id) { assert.strictEqual(typeof id, 'string'); - const result = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives WHERE id = ? ORDER BY creationTime DESC`, [ id ]); + const result = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives LEFT JOIN backups ON archives.backupId = backups.id WHERE archives.id = ? ORDER BY creationTime DESC`, [ id ]); if (result.length === 0) return null; return postProcess(result[0]); @@ -63,13 +63,12 @@ async function getIcon(id, options) { async function add(backupId, data, auditSource) { assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof data, 'object'); - assert(data.appConfig && typeof data.appConfig === 'object'); assert(auditSource && typeof auditSource === 'object'); const id = uuid.v4(); - const [error] = await safe(database.query('INSERT INTO archives (id, backupId, icon, appStoreIcon, appConfigJson) VALUES (?, ?, ?, ?, ?)', - [ id, backupId, data.icon, data.appStoreIcon, JSON.stringify(data.appConfig) ])); + const [error] = await safe(database.query('INSERT INTO archives (id, backupId, icon, appStoreIcon) VALUES (?, ?, ?, ?)', + [ id, backupId, data.icon, data.appStoreIcon ])); if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Backup not found'); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'Archive already exists'); @@ -84,7 +83,7 @@ async function list(page, perPage) { assert(typeof page === 'number' && page > 0); assert(typeof perPage === 'number' && perPage > 0); - const results = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]); + const results = await database.query(`SELECT ${ARCHIVE_FIELDS} FROM archives LEFT JOIN backups ON archives.backupId = backups.id ORDER BY creationTime DESC LIMIT ?,?`, [ (page-1)*perPage, perPage ]); results.forEach(function (result) { postProcess(result); }); diff --git a/src/backups.js b/src/backups.js index ab6468b48..d821af919 100644 --- a/src/backups.js +++ b/src/backups.js @@ -73,7 +73,7 @@ const assert = require('assert'), tasks = require('./tasks.js'), _ = require('underscore'); -const BACKUPS_FIELDS = [ 'id', 'remotePath', 'label', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOnJson', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ]; +const BACKUPS_FIELDS = [ 'id', 'remotePath', 'label', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOnJson', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion', 'appConfigJson' ]; function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -84,6 +84,9 @@ function postProcess(result) { result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null; delete result.manifestJson; + result.appConfig = result.appConfigJson ? safe.JSON.parse(result.appConfigJson) : null; + delete result.appConfigJson; + return result; } @@ -122,14 +125,16 @@ async function add(data) { assert.strictEqual(typeof data.manifest, 'object'); assert.strictEqual(typeof data.format, 'string'); assert.strictEqual(typeof data.preserveSecs, 'number'); + assert.strictEqual(typeof data.appConfig, 'object'); const creationTime = data.creationTime || new Date(); // allow tests to set the time const manifestJson = JSON.stringify(data.manifest); const prefixId = data.type === exports.BACKUP_TYPE_APP ? `${data.type}_${data.identifier}` : data.type; // type and identifier are same for other types const id = `${prefixId}_v${data.packageVersion}_${hat(32)}`; // id is used by the UI to derive dependent packages. making this a UUID will require a lot of db querying + const appConfigJson = data.appConfig ? JSON.stringify(data.appConfig) : null; - const [error] = await safe(database.query('INSERT INTO backups (id, remotePath, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOnJson, manifestJson, format, preserveSecs) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [ id, data.remotePath, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, JSON.stringify(data.dependsOn), manifestJson, data.format, data.preserveSecs ])); + const [error] = await safe(database.query('INSERT INTO backups (id, remotePath, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOnJson, manifestJson, format, preserveSecs, appConfigJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ id, data.remotePath, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, JSON.stringify(data.dependsOn), manifestJson, data.format, data.preserveSecs, appConfigJson ])); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'Backup already exists'); if (error) throw error; diff --git a/src/backuptask.js b/src/backuptask.js index cbdb3aa52..2e0e64f57 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -252,7 +252,8 @@ async function rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCa dependsOn, manifest: null, format, - preserveSecs: options.preserveSecs || 0 + preserveSecs: options.preserveSecs || 0, + appConfig: null }; const id = await backups.add(data); @@ -437,7 +438,8 @@ async function rotateMailBackup(backupConfig, tag, options, progressCallback) { dependsOn: [], manifest: null, format, - preserveSecs: options.preserveSecs || 0 + preserveSecs: options.preserveSecs || 0, + appConfig: null }; const id = await backups.add(data); diff --git a/src/routes/test/archives-test.js b/src/routes/test/archives-test.js index 9ba1632d6..8a2d7b9d9 100644 --- a/src/routes/test/archives-test.js +++ b/src/routes/test/archives-test.js @@ -27,15 +27,15 @@ describe('Archives API', function () { format: 'tgz', preserveSecs: 0, label: '', + appConfig: { loc: 'loc1' } }; - const appConfig = { loc: 'loc1' }; let archiveId; before(async function () { await setup(); appBackup.id = await backups.add(appBackup); - archiveId = await archives.add(appBackup.id, { appConfig }, auditSource); + archiveId = await archives.add(appBackup.id, {}, auditSource); }); after(cleanup); @@ -45,13 +45,14 @@ describe('Archives API', function () { expect(response.statusCode).to.equal(200); expect(response.body.archives.length).to.be(1); expect(response.body.archives[0].id).to.be(archiveId); + expect(response.body.archives[0].appConfig).to.eql(appBackup.appConfig); }); it('get valid archive', async function () { const response = await superagent.get(`${serverUrl}/api/v1/archives/${archiveId}`) .query({ access_token: owner.token }); expect(response.statusCode).to.equal(200); - expect(response.body.appConfig).to.eql(appConfig); + expect(response.body.appConfig).to.eql(appBackup.appConfig); }); it('cannot get invalid archive', async function () { diff --git a/src/test/archives-test.js b/src/test/archives-test.js index 026ac1b36..641abef08 100644 --- a/src/test/archives-test.js +++ b/src/test/archives-test.js @@ -28,6 +28,7 @@ describe('Archives', function () { format: 'tgz', preserveSecs: 0, label: '', + appConfig: { loc: 'loc1' } }; before(async function () { @@ -36,16 +37,15 @@ describe('Archives', function () { }); after(cleanup); - const appConfig = { loc: 'loc1' }; let archiveId; it('cannot add bad backup to archives', async function () { - const [error] = await safe(archives.add('badId', { appConfig }, auditSource)); + const [error] = await safe(archives.add('badId', {}, auditSource)); expect(error.reason).to.be(BoxError.NOT_FOUND); }); it('can add good backup to archives', async function () { - archiveId = await archives.add(appBackup.id, { appConfig }, auditSource); + archiveId = await archives.add(appBackup.id, {}, auditSource); }); it('cannot get invalid archive', async function () { @@ -55,14 +55,14 @@ describe('Archives', function () { it('can get archive', async function () { const result = await archives.get(archiveId); - expect(result.appConfig).to.eql(appConfig); + expect(result.appConfig).to.eql(appBackup.appConfig); }); it('can list archives', async function () { const result = await archives.list(1, 100); expect(result.length).to.be(1); expect(result[0].id).to.be(archiveId); - expect(result[0].appConfig).to.eql(appConfig); + expect(result[0].appConfig).to.eql(appBackup.appConfig); }); it('can list backupIds', async function () { diff --git a/src/test/backupcleaner-test.js b/src/test/backupcleaner-test.js index 9ff734263..55ced70ab 100644 --- a/src/test/backupcleaner-test.js +++ b/src/test/backupcleaner-test.js @@ -34,6 +34,7 @@ describe('backup cleaner', function () { manifest: null, format: 'tgz', preserveSecs: 0, + appConfig: null }; describe('retention', function () { @@ -137,7 +138,8 @@ describe('backup cleaner', function () { dependsOn: [ 'backup-app-00', 'backup-app-01' ], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_0_APP_0 = { // backup of installed app @@ -151,7 +153,8 @@ describe('backup cleaner', function () { dependsOn: [], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_0_APP_1 = { // this app is uninstalled @@ -165,7 +168,8 @@ describe('backup cleaner', function () { dependsOn: [], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_1_BOX = { @@ -179,7 +183,8 @@ describe('backup cleaner', function () { dependsOn: [ 'backup-app-10', 'backup-app-11' ], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_1_APP_0 = { @@ -193,7 +198,8 @@ describe('backup cleaner', function () { dependsOn: [], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_1_APP_1 = { @@ -207,7 +213,8 @@ describe('backup cleaner', function () { dependsOn: [], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; const BACKUP_2_APP_2 = { // this is archived and left alone @@ -221,11 +228,10 @@ describe('backup cleaner', function () { dependsOn: [], manifest: null, format: 'tgz', - preserveSecs: 0 + preserveSecs: 0, + appConfig: null }; - const app2Config = { loc: 'apploc2' }; - before(async function () { await settings._set(settings.BACKUP_STORAGE_KEY, JSON.stringify({ provider: 'filesystem', @@ -271,7 +277,7 @@ describe('backup cleaner', function () { BACKUP_1_BOX.id = await backups.add(BACKUP_1_BOX); BACKUP_2_APP_2.id = await backups.add(BACKUP_2_APP_2); - await archives.add(BACKUP_2_APP_2.id, { appConfig: app2Config }, common.auditSource); + await archives.add(BACKUP_2_APP_2.id, {}, common.auditSource); }); it('succeeds with box backups, keeps latest', async function () { diff --git a/src/test/backups-test.js b/src/test/backups-test.js index 9616507cf..f3f49db7c 100644 --- a/src/test/backups-test.js +++ b/src/test/backups-test.js @@ -31,6 +31,7 @@ describe('backups', function () { format: 'tgz', preserveSecs: 0, label: '', + appConfig: null }; const appBackup = { @@ -46,6 +47,7 @@ describe('backups', function () { format: 'tgz', preserveSecs: 0, label: '', + appConfig: null }; describe('crud', function () {