From ec8477cf1fb52991f3fb697a976679ebf43079b0 Mon Sep 17 00:00:00 2001 From: Juri Leino Date: Wed, 18 Oct 2023 00:42:33 +0200 Subject: [PATCH] feat(package): package upload uses REST by default fixes #5 `xst package install ` will now use exist-db's REST API by default and will fallback to XML-RPC only if REST is not available. This allows uploads of XAR packages larger than 500MB. Setting `--rest` will _force_ uploads to use the REST endpoint and installation wil fail when it is deactivated on the target instance. Setting `--xmlrpc` or `--rpc` on the other hand will always use XML-RPC but limits the size of installable XARs. --- commands/package/install.js | 56 +++++++++++++++++++++++++++++------ package-lock.json | 14 ++++----- package.json | 2 +- spec/tests/package/install.js | 25 +++++++++++++++- utility/configure.js | 1 + 5 files changed, 80 insertions(+), 18 deletions(-) diff --git a/commands/package/install.js b/commands/package/install.js index 44d6ec6..645d69e 100644 --- a/commands/package/install.js +++ b/commands/package/install.js @@ -1,8 +1,16 @@ -import { connect } from '@existdb/node-exist' +import { connect, getRestClient } from '@existdb/node-exist' import { readFileSync } from 'node:fs' import { basename } from 'node:path' +const AdminGroup = 'dba' + +async function putPackage (db, restClient, content, fileName) { + const dbPath = db.app.packageCollection + '/' + fileName + const res = await restClient.put(content, dbPath) + return { success: res.statusCode === 201, error: res.body } +} + async function getUserInfo (db) { const { user } = db.client.options.basic_auth return await db.users.getUserInfo(user) @@ -26,13 +34,13 @@ async function removeTemporaryCollection (db) { return await db.collections.remove(db.app.packageCollection) } -async function install (db, localFilePath) { +async function install (db, upload, localFilePath) { const xarName = basename(localFilePath) const contents = readFileSync(localFilePath) console.log(`Install ${xarName} on ${serverName(db)}`) - const uploadResult = await db.app.upload(contents, xarName) + const uploadResult = await upload(contents, xarName) if (!uploadResult.success) { throw new Error(uploadResult.error) } @@ -51,8 +59,24 @@ async function install (db, localFilePath) { return 0 } -export const command = ['install [options] ', 'i'] +export const command = ['install ', 'i'] export const describe = 'Install XAR packages' +const options = { + rest: { + describe: 'force upload over REST API', + type: 'boolean' + }, + xmlrpc: { + alias: 'rpc', + describe: 'force upload over XML-RPC API', + type: 'boolean' + } +} + +export const builder = yargs => { + return yargs.options(options) + .conflicts('xmlrpc', 'rest') +} export async function handler (argv) { if (argv.help) { @@ -60,7 +84,7 @@ export async function handler (argv) { } // main - const { packages } = argv + const { packages, connectionOptions, rest, xmlrpc } = argv packages.forEach(packagePath => { if (!packagePath.match(/\.xar$/i)) { throw Error('Packages must have the file extension .xar! Got: "' + packagePath + '"') @@ -68,17 +92,31 @@ export async function handler (argv) { }) // check permissions (and therefore implicitly the connection) - const db = connect(argv.connectionOptions) + const db = connect(connectionOptions) const accountInfo = await getUserInfo(db) - const isAdmin = accountInfo.groups.includes('dba') + const isAdmin = accountInfo.groups.includes(AdminGroup) if (!isAdmin) { - throw Error(`Package installation failed. User "${accountInfo.name}" is not a member of the "dba" group.`) + throw Error(`Package installation failed. User "${accountInfo.name}" is not a member of the "${AdminGroup}" group.`) + } + + let upload + if (xmlrpc) { + upload = db.app.upload + } else { + const restClient = await getRestClient(connectionOptions) + const boundUpload = putPackage.bind(null, db, restClient) + if (rest) { + upload = boundUpload + } else { + const test = await restClient.get('db') + upload = test.statusCode === 200 ? boundUpload : db.app.upload + } } try { for (const i in packages) { const packagePath = packages[i] - await install(db, packagePath) + await install(db, upload, packagePath) } } finally { await removeTemporaryCollection(db) diff --git a/package-lock.json b/package-lock.json index de5aabc..e348a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-development", "license": "MIT", "dependencies": { - "@existdb/node-exist": "^5.4.0", + "@existdb/node-exist": "^5.4.1", "bottleneck": "^2.19.5", "chalk": "^5.2.0", "dotenv": "^16.0.3", @@ -897,9 +897,9 @@ } }, "node_modules/@existdb/node-exist": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@existdb/node-exist/-/node-exist-5.4.0.tgz", - "integrity": "sha512-4vIqb2PztUH7BFJf8ro0oCHWKsg/qkk/4AMrwZH7C5v9tZWLEBL9ieu5/r73vzzC4bSXRAmPqOXwQpVetOP5Xw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@existdb/node-exist/-/node-exist-5.4.1.tgz", + "integrity": "sha512-8Ws6LanpxAMZ1GpyUX5CWfa90kikp38VZvhkmYBdpFFUuuqbCNvLb5nUswJH2ZtTAjp0uN1t7zbaqairIO2hyQ==", "dependencies": { "got": "^12.1.0", "lodash.assign": "^4.0.2", @@ -12381,9 +12381,9 @@ "dev": true }, "@existdb/node-exist": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@existdb/node-exist/-/node-exist-5.4.0.tgz", - "integrity": "sha512-4vIqb2PztUH7BFJf8ro0oCHWKsg/qkk/4AMrwZH7C5v9tZWLEBL9ieu5/r73vzzC4bSXRAmPqOXwQpVetOP5Xw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@existdb/node-exist/-/node-exist-5.4.1.tgz", + "integrity": "sha512-8Ws6LanpxAMZ1GpyUX5CWfa90kikp38VZvhkmYBdpFFUuuqbCNvLb5nUswJH2ZtTAjp0uN1t7zbaqairIO2hyQ==", "requires": { "got": "^12.1.0", "lodash.assign": "^4.0.2", diff --git a/package.json b/package.json index 409cbac..0d71fbb 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "maintainers": [], "license": "MIT", "dependencies": { - "@existdb/node-exist": "^5.4.0", + "@existdb/node-exist": "^5.4.1", "bottleneck": "^2.19.5", "chalk": "^5.2.0", "dotenv": "^16.0.3", diff --git a/spec/tests/package/install.js b/spec/tests/package/install.js index aaccfc3..cf9deee 100644 --- a/spec/tests/package/install.js +++ b/spec/tests/package/install.js @@ -41,7 +41,7 @@ test('shows help', async function (t) { if (stderr) { return t.fail(stderr) } t.ok(stdout, 'got output') const firstLine = stdout.split('\n')[0] - t.equal(firstLine, 'xst package install [options] ', firstLine) + t.equal(firstLine, 'xst package install ', firstLine) }) test('fails when dependency is not met', async function (t) { @@ -155,6 +155,29 @@ test('multiple valid packages', async function (t) { t.teardown(cleanup) }) +test('using rest for upload', async function (t) { + t.test('installs lib first', async function (st) { + const { stderr, stdout } = await run('xst', ['package', 'install', '--rest', 'spec/fixtures/test-lib.xar'], asAdmin) + if (stderr) { + // console.error(stderr) + st.fail(stderr) + return st.end() + } + st.ok(stdout) + }) + t.test('installs app', async function (st) { + const { stderr, stdout } = await run('xst', ['package', 'install', '--rest', 'spec/fixtures/test-app.xar'], asAdmin) + if (stderr) { + // console.error(stderr) + st.fail(stderr) + return st.end() + } + st.ok(stdout) + }) + + t.teardown(cleanup) +}) + test('multiple packages', async function (t) { t.test('first is broken', async function (st) { const { stderr, stdout } = await run('xst', ['package', 'install', 'spec/fixtures/broken-test-app.xar', 'spec/fixtures/test-app.xar'], asAdmin) diff --git a/utility/configure.js b/utility/configure.js index a6dc0c4..c8b6a82 100644 --- a/utility/configure.js +++ b/utility/configure.js @@ -56,6 +56,7 @@ function compileConnectionOptions (server, user, pass) { connectionOptions.secure = protocol === 'https:' connectionOptions.host = hostname connectionOptions.port = port + connectionOptions.protocol = protocol } return { connectionOptions }