diff --git a/packages/xo-server/package.json b/packages/xo-server/package.json index 0a4283e3a..2954d3110 100644 --- a/packages/xo-server/package.json +++ b/packages/xo-server/package.json @@ -25,7 +25,7 @@ "xo-server-recover-account": "dist/recover-account-cli.mjs" }, "engines": { - "node": ">=14.17" + "node": ">=14.18" }, "dependencies": { "@iarna/toml": "^2.2.1", diff --git a/packages/xo-server/src/api/xo.mjs b/packages/xo-server/src/api/xo.mjs index adc897f5c..22b691a1d 100644 --- a/packages/xo-server/src/api/xo.mjs +++ b/packages/xo-server/src/api/xo.mjs @@ -15,8 +15,11 @@ clean.permission = 'admin' // ------------------------------------------------------------------- -export async function exportConfig({ entries, passphrase }) { +export async function exportConfig({ compress, entries, passphrase }) { let suffix = '/config.json' + if (compress) { + suffix += '.gz' + } if (passphrase !== undefined) { suffix += '.enc' } @@ -26,10 +29,9 @@ export async function exportConfig({ entries, passphrase }) { (req, res) => { res.set({ 'content-disposition': 'attachment', - 'content-type': 'application/json', }) - return this.exportConfig({ entries, passphrase }) + return this.exportConfig({ compress, entries, passphrase }) }, undefined, { suffix } @@ -40,6 +42,7 @@ export async function exportConfig({ entries, passphrase }) { exportConfig.permission = 'admin' exportConfig.params = { + compress: { type: 'boolean', default: true }, entries: { type: 'array', items: { type: 'string' }, optional: true }, passphrase: { type: 'string', optional: true }, } diff --git a/packages/xo-server/src/xo-mixins/config-management.mjs b/packages/xo-server/src/xo-mixins/config-management.mjs index 4363b273f..2844597fd 100644 --- a/packages/xo-server/src/xo-mixins/config-management.mjs +++ b/packages/xo-server/src/xo-mixins/config-management.mjs @@ -1,6 +1,8 @@ import * as openpgp from 'openpgp' import DepTree from 'deptree' +import fromCallback from 'promise-toolbox/fromCallback' import { createLogger } from '@xen-orchestra/log' +import { gunzip, gzip } from 'node:zlib' import { asyncMapValues } from '../_asyncMapValues.mjs' @@ -23,7 +25,7 @@ export default class ConfigManagement { this._managers[id] = { dependencies, exporter, importer } } - async exportConfig({ entries, passphrase } = {}) { + async exportConfig({ compress = false, entries, passphrase } = {}) { let managers = this._managers if (entries !== undefined) { const subset = { __proto__: null } @@ -39,11 +41,15 @@ export default class ConfigManagement { let config = JSON.stringify(await asyncMapValues(managers, ({ exporter }) => exporter())) + if (compress) { + config = await fromCallback(gzip, config) + } + if (passphrase !== undefined) { config = Buffer.from( await openpgp.encrypt({ format: 'binary', - message: await openpgp.createMessage({ text: config }), + message: await openpgp.createMessage(typeof config === 'string' ? { text: config } : { binary: config }), passwords: passphrase, }) ) @@ -56,12 +62,17 @@ export default class ConfigManagement { if (passphrase !== undefined) { config = ( await openpgp.decrypt({ + format: 'binary', message: await openpgp.readMessage({ binaryMessage: config }), passwords: passphrase, }) ).data } + if (typeof config !== 'string' && config[0] === 0x1f && config[1] === 0x8b) { + config = await fromCallback(gunzip, config) + } + config = JSON.parse(config) const managers = this._managers