This commit is contained in:
Ben Irvin 2023-01-11 11:06:14 +01:00
parent 544fb057c2
commit 11b8d3c9f3
12 changed files with 199 additions and 56 deletions

View File

@ -2,6 +2,7 @@ import { getStrapiFactory } from '../../__tests__/test-utils';
import { createTransferHandler } from '../remote/handlers';
import register from '../register';
import { TRANSFER_PATH } from '../../../lib/strapi/remote/constants';
afterEach(() => {
jest.clearAllMocks();
@ -20,13 +21,13 @@ jest.mock('../remote/handlers', () => ({
}));
describe('Register the Transfer route', () => {
test('registers the /transfer route', () => {
test('registers the transfer route', () => {
const strapi = strapiMockFactory();
register(strapi);
expect(strapi.admin.routes.push).toHaveBeenCalledWith({
method: 'GET',
path: '/transfer',
path: TRANSFER_PATH,
handler: createTransferHandler(),
config: {
auth: false,

View File

@ -1,4 +1,5 @@
import { WebSocket } from 'ws';
import { TRANSFER_PATH } from '../../../../../lib/strapi/remote/constants';
import { CommandMessage } from '../../../../../types/remote/protocol/client';
import { createDispatcher } from '../utils';
@ -18,7 +19,7 @@ afterEach(() => {
describe('Remote Strapi Destination Utils', () => {
test('Dispatch method sends payload', () => {
const ws = new WebSocket('ws://test/admin/transfer');
const ws = new WebSocket(`ws://test/admin${TRANSFER_PATH}`);
const message: CommandMessage = {
type: 'command',
command: 'status',

View File

@ -15,6 +15,7 @@ import type {
} from '../../../../types';
import type { client, server } from '../../../../types/remote/protocol';
import type { ILocalStrapiDestinationProviderOptions } from '../local-destination';
import { TRANSFER_PATH } from '../../remote/constants';
interface ITokenAuth {
type: 'token';
@ -29,7 +30,7 @@ interface ICredentialsAuth {
export interface IRemoteStrapiDestinationProviderOptions
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
url: string;
url: URL;
auth?: ITokenAuth | ICredentialsAuth;
}
@ -100,16 +101,19 @@ class RemoteStrapiDestinationProvider implements IDestinationProvider {
let ws: WebSocket;
const wsUrl = `${url.protocol === 'https:' ? 'wss:' : 'ws:'}//${url.host}${
url.pathname
}${TRANSFER_PATH}`;
// No auth defined, trying public access for transfer
if (!auth) {
ws = new WebSocket(url);
ws = new WebSocket(wsUrl);
}
// Common token auth, this should be the main auth method
else if (auth.type === 'token') {
const headers = { Authentication: `Bearer ${auth.token}` };
ws = new WebSocket(this.options.url, { headers });
const headers = { Authorization: `Bearer ${auth.token}` };
ws = new WebSocket(wsUrl, { headers });
}
// Invalid auth method provided

View File

@ -1 +1 @@
export const TRANSFER_URL = '/transfer';
export const TRANSFER_PATH = '/transfer';

View File

@ -1,7 +1,7 @@
// eslint-disable-next-line node/no-extraneous-import
import type { Context } from 'koa';
import { TRANSFER_URL } from './constants';
import { TRANSFER_PATH } from './constants';
import { createTransferHandler } from './handlers';
// Extend Strapi interface type to access the admin routes' API
@ -29,7 +29,7 @@ declare module '@strapi/strapi' {
export const registerAdminTransferRoute = (strapi: Strapi.Strapi) => {
strapi.admin.routes.push({
method: 'GET',
path: TRANSFER_URL,
path: TRANSFER_PATH,
handler: createTransferHandler(),
config: { auth: false },
});

View File

@ -14,7 +14,12 @@ const inquirer = require('inquirer');
const program = new Command();
const packageJSON = require('../package.json');
const { promptEncryptionKey, confirmMessage } = require('../lib/commands/utils/commander');
const {
promptEncryptionKey,
confirmMessage,
parseURL,
} = require('../lib/commands/utils/commander');
const { ifOptions, assertUrlHasProtocol, exitWith } = require('../lib/commands/utils/helpers');
const checkCwdIsStrapiApp = (name) => {
const logErrorAndExit = () => {
@ -263,16 +268,39 @@ if (process.env.STRAPI_EXPERIMENTAL) {
program
.command('transfer')
.description('Transfer data from one source to another')
.addOption(new Option('--from <sourceURL>', `URL of remote Strapi instance to get data from.`))
.addOption(new Option('--to <destinationURL>', `URL of remote Strapi instance to send data to`))
.hook('preAction', async (thisCommand) => {
const opts = thisCommand.opts();
if (!opts.from && !opts.to) {
console.error('At least one source (from) or destination (to) option must be provided');
process.exit(1);
}
})
.addOption(
new Option('--from <sourceURL>', `URL of remote Strapi instance to get data from.`).argParser(
parseURL
)
)
.addOption(
new Option(
'--to <destinationURL>',
`URL of remote Strapi instance to send data to`
).argParser(parseURL)
)
// Validate URLs
.hook(
'preAction',
ifOptions(
(opts) => opts.from,
(thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
)
)
.hook(
'preAction',
ifOptions(
(opts) => opts.to,
(thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
)
)
.hook(
'preAction',
ifOptions(
(opts) => !opts.from && !opts.to,
() => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
)
)
.allowExcessArguments(false)
.action(getLocalScript('transfer/transfer'));
}

View File

@ -38,7 +38,7 @@ jest.spyOn(console, 'log').mockImplementation(() => {});
jest.mock('../../transfer/utils');
const destinationUrl = 'ws://strapi.com';
const destinationUrl = new URL('http://strapi.com/admin');
describe('Transfer', () => {
beforeEach(() => {

View File

@ -22,8 +22,8 @@ const logger = console;
/**
* @typedef TransferCommandOptions Options given to the CLI transfer command
*
* @property {string|undefined} [to] The url of a remote Strapi to use as remote destination
* @property {string|undefined} [from] The url of a remote Strapi to use as remote source
* @property {URL|undefined} [to] The url of a remote Strapi to use as remote destination
* @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
*/
/**
@ -86,8 +86,8 @@ module.exports = async (opts) => {
}
const engine = createTransferEngine(source, destination, {
versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
versionStrategy: 'exact', // for an export to file, versionStrategy will always be skipped
schemaStrategy: 'exact', // for an export to file, schemaStrategy will always be skipped
transforms: {
links: [
{

View File

@ -2,7 +2,7 @@
const chalk = require('chalk');
const Table = require('cli-table3');
const { readableBytes } = require('../utils');
const { readableBytes } = require('../utils/helpers');
const strapi = require('../../index');
const pad = (n) => {

View File

@ -1,14 +1,36 @@
'use strict';
/**
* This file includes hooks to use for commander.hook and argParsers for commander.argParser
*/
const inquirer = require('inquirer');
const { InvalidOptionArgumentError } = require('commander');
const { exitWith } = require('./helpers');
/**
* argsParser: Parse a comma-delimited string as an array
* argParser: Parse a comma-delimited string as an array
*/
const parseInputList = (value) => {
return value.split(',');
};
/**
* argParser: Parse a string as a URL object
*/
const parseURL = (value) => {
try {
const url = new URL(value);
if (!url.host) {
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
}
return url;
} catch (e) {
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
}
};
/**
* hook: if encrypt==true and key not provided, prompt for it
*/
@ -16,8 +38,7 @@ const promptEncryptionKey = async (thisCommand) => {
const opts = thisCommand.opts();
if (!opts.encrypt && opts.key) {
console.error('Key may not be present unless encryption is used');
process.exit(1);
return exitWith(1, 'Key may not be present unless encryption is used');
}
// if encrypt==true but we have no key, prompt for it
@ -37,12 +58,10 @@ const promptEncryptionKey = async (thisCommand) => {
]);
opts.key = answers.key;
} catch (e) {
console.error('Failed to get encryption key');
process.exit(1);
return exitWith(1, 'Failed to get encryption key');
}
if (!opts.key) {
console.error('Failed to get encryption key');
process.exit(1);
return exitWith(1, 'Failed to get encryption key');
}
}
};
@ -61,13 +80,15 @@ const confirmMessage = (message) => {
},
]);
if (!answers.confirm) {
process.exit(0);
exitWith(0);
}
};
};
module.exports = {
parseInputList,
parseURL,
promptEncryptionKey,
confirmMessage,
exitWith,
};

View File

@ -0,0 +1,108 @@
'use strict';
/**
* Helper functions for the Strapi CLI
*/
const chalk = require('chalk');
const { isString, isArray } = require('lodash/fp');
const bytesPerKb = 1024;
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
/**
* Convert bytes to a human readable formatted string, for example "1024" becomes "1KB"
*
* @param {number} bytes The bytes to be converted
* @param {number} decimals How many decimals to include in the final number
* @param {number} padStart Pad the string with space at the beginning so that it has at least this many characters
*/
const readableBytes = (bytes, decimals = 1, padStart = 0) => {
if (!bytes) {
return '0';
}
const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
2
)}`;
return result.padStart(padStart);
};
/**
*
* Display message(s) to console and then call process.exit with code.
* If code is zero, console.log and green text is used for messages, otherwise console.error and red text.
*
* @param {number} code Code to exit process with
* @param {string | Array} message Message(s) to display before exiting
*/
const exitWith = (code, message = undefined) => {
const logger = (message) => {
if (code === 0) {
console.log(chalk.green(message));
} else {
console.log(chalk.red(message));
}
};
if (isString(message)) {
logger(message);
} else if (isArray(message)) {
message.forEach((msg) => logger(msg));
}
process.exit(code);
};
/**
* assert that a URL object has a protocol value
*
* @param {URL} url
* @param {string[]|string|undefined} [protocol]
*/
const assertUrlHasProtocol = (url, protocol = undefined) => {
if (!url.protocol) {
exitWith(1, `${url.toString()} does not have a protocol`);
}
// if just checking for the existence of a protocol, return
if (!protocol) {
return;
}
if (isString(protocol)) {
if (protocol !== url.protocol) {
exitWith(1, `${url.toString()} must have the protocol ${protocol}`);
}
return;
}
// assume an array
if (!protocol.some((protocol) => url.protocol === protocol)) {
return exitWith(
1,
`${url.toString()} must have one of the following protocols: ${protocol.join(',')}`
);
}
};
/**
* Passes commander options to conditionCallback(). If it returns true, call isMetCallback otherwise call isNotMetCallback
*/
const ifOptions = (conditionCallback, isMetCallback = () => {}, isNotMetCallback = () => {}) => {
return async (command) => {
const opts = command.opts();
if (await conditionCallback(opts)) {
await isMetCallback(command);
} else {
await isNotMetCallback(command);
}
};
};
module.exports = {
exitWith,
assertUrlHasProtocol,
ifOptions,
readableBytes,
};

View File

@ -1,20 +0,0 @@
'use strict';
const bytesPerKb = 1024;
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
const readableBytes = (bytes, decimals = 1, padStart = 0) => {
if (!bytes) {
return '0';
}
const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
2
)}`;
return result.padStart(padStart);
};
module.exports = {
readableBytes,
};