diff --git a/migrations/20241209150823-backups-add-archive.js b/migrations/20241209150823-backups-add-archive.js index 16eb10652..c3b58b39a 100644 --- a/migrations/20241209150823-backups-add-archive.js +++ b/migrations/20241209150823-backups-add-archive.js @@ -2,9 +2,15 @@ exports.up = async function(db) { await db.runSql('ALTER TABLE backups ADD COLUMN archive BOOLEAN DEFAULT 0'); + await db.runSql('ALTER TABLE backups ADD COLUMN icon MEDIUMBLOB'); + await db.runSql('ALTER TABLE backups ADD COLUMN appStoreIcon MEDIUMBLOB'); + await db.runSql('ALTER TABLE backups ADD COLUMN appConfigJson TEXT'); }; exports.down = async function(db) { await db.runSql('ALTER TABLE backups DROP COLUMN archive'); + await db.runSql('ALTER TABLE backups DROP COLUMN icon'); + await db.runSql('ALTER TABLE backups DROP COLUMN appStoreIcon'); + await db.runSql('ALTER TABLE backups DROP COLUMN appConfigJson'); }; diff --git a/migrations/schema.sql b/migrations/schema.sql index 8e7ebadb1..21de98502 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -157,6 +157,9 @@ CREATE TABLE IF NOT EXISTS backups( format VARCHAR(16) DEFAULT "tgz", preserveSecs INTEGER DEFAULT 0, archive BOOLEAN DEFAULT 0, + appStoreIcon MEDIUMBLOB, /* only valid with archive */ + icon MEDIUMBLOB, /* only valid with archive */ + appConfigJson TEXT, /* only valid with archive */ INDEX creationTime_index (creationTime), PRIMARY KEY (id)); diff --git a/src/apps.js b/src/apps.js index 93987d81b..6ee97ad45 100644 --- a/src/apps.js +++ b/src/apps.js @@ -2494,8 +2494,10 @@ async function archive(app, backupId, auditSource) { if (result.length === 0) throw new BoxError(BoxError.BAD_STATE, 'No recent backup to archive'); if (result[0].id !== backupId) throw new BoxError(BoxError.BAD_STATE, 'Latest backup id has changed'); + const icons = await getIcons(app.id); const { taskId } = await uninstall(app, auditSource); - await backups.update(result[0].id, { archive: true }); + await backups.update(result[0].id, { archive: true, icon: icons.icon, appStoreIcon: icons.appStoreIcon }); + if (!result[0].appConfig) await backups.update(result[0].id, { appConfig: app }); // workaround for previous versions not setting appConfig return { taskId }; } diff --git a/src/backups.js b/src/backups.js index 025521db8..3bf77be79 100644 --- a/src/backups.js +++ b/src/backups.js @@ -12,6 +12,7 @@ exports = module.exports = { archives: { get: archivesGet, + getIcon: archivesGetIcon, list: archivesList, del: archivesDel }, @@ -79,7 +80,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', 'archive' ]; +const BACKUPS_FIELDS = [ 'id', 'remotePath', 'label', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOnJson', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion', 'archive', 'appConfigJson' ]; function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -90,6 +91,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; } @@ -133,9 +137,10 @@ async function add(data) { 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 = 'appConfig' in data ? 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; @@ -186,6 +191,27 @@ async function archivesGet(id) { return postProcess(result[0]); } +async function archivesGetIcons(id) { + assert.strictEqual(typeof id, 'string'); + + const results = await database.query('SELECT icon, appStoreIcon FROM backups WHERE id = ?', [ id ]); + if (results.length === 0) return null; + return { icon: results[0].icon, appStoreIcon: results[0].appStoreIcon }; +} + +async function archivesGetIcon(id, options) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof options, 'object'); + + const icons = await archivesGetIcons(id); + if (!icons) throw new BoxError(BoxError.NOT_FOUND, 'No such backup'); + + if (!options.original && icons.icon) return icons.icon; + if (icons.appStoreIcon) return icons.appStoreIcon; + + return null; +} + async function archivesList(page, perPage) { assert(typeof page === 'number' && page > 0); assert(typeof perPage === 'number' && perPage > 0); @@ -243,9 +269,12 @@ async function update(id, data) { const fields = [], values = []; for (const p in data) { - if (p === 'label' || p === 'preserveSecs' || p === 'archive') { + if (p === 'label' || p === 'preserveSecs' || p === 'archive' || p === 'icon' || p === 'appStoreIcon') { fields.push(p + ' = ?'); values.push(data[p]); + } else if (p === 'appConfig') { + fields.push(`${p}Json = ?`); + values.push(JSON.stringify(data[p])); } } values.push(id); diff --git a/src/backuptask.js b/src/backuptask.js index ebbe9b940..cbdb3aa52 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -301,7 +301,8 @@ async function rotateAppBackup(backupConfig, app, tag, options, progressCallback dependsOn: [], manifest, format, - preserveSecs: options.preserveSecs || 0 + preserveSecs: options.preserveSecs || 0, + appConfig: app }; const id = await backups.add(data); diff --git a/src/routes/archives.js b/src/routes/archives.js index 0b8e8b78f..f8fde5f49 100644 --- a/src/routes/archives.js +++ b/src/routes/archives.js @@ -5,7 +5,8 @@ exports = module.exports = { list, get, - del + getIcon, + del, }; const assert = require('assert'), @@ -47,6 +48,15 @@ async function get(req, res, next) { next(new HttpSuccess(200, req.resource)); } +async function getIcon(req, res, next) { + assert.strictEqual(typeof req.app, 'object'); + + const [error, icon] = await safe(archives.getIcon(req.params.id, { original: req.query.original })); + if (error) return next(BoxError.toHttpError(error)); + + res.send(icon); +} + async function del(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); diff --git a/src/server.js b/src/server.js index 25281e1df..3ffac2474 100644 --- a/src/server.js +++ b/src/server.js @@ -165,6 +165,7 @@ async function initializeExpressSync() { 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); + router.get ('/api/v1/archives/:id/icon', routes.archives.load, routes.archives.getIcon); // working off the user behind the provided token router.get ('/api/v1/profile', token, authorizeUser, routes.profile.get); diff --git a/src/test/backups-test.js b/src/test/backups-test.js index 5870a4046..5ee4fff4f 100644 --- a/src/test/backups-test.js +++ b/src/test/backups-test.js @@ -31,7 +31,8 @@ describe('backups', function () { format: 'tgz', preserveSecs: 0, label: '', - archive: false + archive: false, + appConfig: null }; const appBackup = { @@ -47,7 +48,8 @@ describe('backups', function () { format: 'tgz', preserveSecs: 0, label: '', - archive: false + archive: false, + appConfig: null }; describe('crud', function () {