mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 17:00:55 +00:00
Merge branch 'data-transfer/pull' of https://github.com/strapi/strapi into data-transfer/pull
This commit is contained in:
commit
284cc7512f
@ -298,7 +298,7 @@ describe('Transfer engine', () => {
|
|||||||
versionStrategy: 'exact',
|
versionStrategy: 'exact',
|
||||||
schemaStrategy: 'exact',
|
schemaStrategy: 'exact',
|
||||||
exclude: [],
|
exclude: [],
|
||||||
} as ITransferEngineOptions;
|
} as unknown as ITransferEngineOptions;
|
||||||
|
|
||||||
let completeSource;
|
let completeSource;
|
||||||
let completeDestination;
|
let completeDestination;
|
||||||
@ -490,7 +490,7 @@ describe('Transfer engine', () => {
|
|||||||
versionStrategy: 'exact',
|
versionStrategy: 'exact',
|
||||||
schemaStrategy: 'exact',
|
schemaStrategy: 'exact',
|
||||||
exclude: [],
|
exclude: [],
|
||||||
} as ITransferEngineOptions;
|
} as unknown as ITransferEngineOptions;
|
||||||
test('source with source schema missing in destination fails', async () => {
|
test('source with source schema missing in destination fails', async () => {
|
||||||
const source = createSource();
|
const source = createSource();
|
||||||
source.getSchemas = jest.fn().mockResolvedValue([...schemas, { foo: 'bar' }]);
|
source.getSchemas = jest.fn().mockResolvedValue([...schemas, { foo: 'bar' }]);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { PassThrough, Transform, Readable, Writable, Stream } from 'stream';
|
import { PassThrough, Transform, Readable, Writable } from 'stream';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { EOL } from 'os';
|
import { EOL } from 'os';
|
||||||
import { isEmpty, uniq, last } from 'lodash/fp';
|
import { isEmpty, uniq, last, isNumber } from 'lodash/fp';
|
||||||
import { diff as semverDiff } from 'semver';
|
import { diff as semverDiff } from 'semver';
|
||||||
import type { Schema } from '@strapi/strapi';
|
import type { Schema } from '@strapi/strapi';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ import type { Diff } from '../utils/json';
|
|||||||
import { compareSchemas, validateProvider } from './validation';
|
import { compareSchemas, validateProvider } from './validation';
|
||||||
import { filter, map } from '../utils/stream';
|
import { filter, map } from '../utils/stream';
|
||||||
|
|
||||||
import { TransferEngineValidationError } from './errors';
|
import { TransferEngineError, TransferEngineValidationError } from './errors';
|
||||||
import {
|
import {
|
||||||
createDiagnosticReporter,
|
createDiagnosticReporter,
|
||||||
IDiagnosticReporter,
|
IDiagnosticReporter,
|
||||||
@ -88,13 +88,19 @@ class TransferEngine<
|
|||||||
|
|
||||||
#metadata: { source?: IMetadata; destination?: IMetadata } = {};
|
#metadata: { source?: IMetadata; destination?: IMetadata } = {};
|
||||||
|
|
||||||
|
// Progress of the current stage
|
||||||
progress: {
|
progress: {
|
||||||
|
// metrics on the progress such as size and record count
|
||||||
data: TransferProgress;
|
data: TransferProgress;
|
||||||
|
// stream that emits events
|
||||||
stream: PassThrough;
|
stream: PassThrough;
|
||||||
};
|
};
|
||||||
|
|
||||||
diagnostics: IDiagnosticReporter;
|
diagnostics: IDiagnosticReporter;
|
||||||
|
|
||||||
|
// Save the currently open stream so that we can access it at any time
|
||||||
|
#currentStream?: Writable;
|
||||||
|
|
||||||
constructor(sourceProvider: S, destinationProvider: D, options: ITransferEngineOptions) {
|
constructor(sourceProvider: S, destinationProvider: D, options: ITransferEngineOptions) {
|
||||||
this.diagnostics = createDiagnosticReporter();
|
this.diagnostics = createDiagnosticReporter();
|
||||||
|
|
||||||
@ -163,6 +169,7 @@ class TransferEngine<
|
|||||||
options: { includeGlobal?: boolean } = {}
|
options: { includeGlobal?: boolean } = {}
|
||||||
): PassThrough | Transform {
|
): PassThrough | Transform {
|
||||||
const { includeGlobal = true } = options;
|
const { includeGlobal = true } = options;
|
||||||
|
const { throttle } = this.options;
|
||||||
const { global: globalTransforms, [key]: stageTransforms } = this.options?.transforms ?? {};
|
const { global: globalTransforms, [key]: stageTransforms } = this.options?.transforms ?? {};
|
||||||
|
|
||||||
let stream = new PassThrough({ objectMode: true });
|
let stream = new PassThrough({ objectMode: true });
|
||||||
@ -183,6 +190,20 @@ class TransferEngine<
|
|||||||
applyTransforms(globalTransforms);
|
applyTransforms(globalTransforms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNumber(throttle) && throttle > 0) {
|
||||||
|
stream = stream.pipe(
|
||||||
|
new PassThrough({
|
||||||
|
objectMode: true,
|
||||||
|
async transform(data, _encoding, callback) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, throttle);
|
||||||
|
});
|
||||||
|
callback(null, data);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
applyTransforms(stageTransforms as TransferTransform<unknown>[]);
|
applyTransforms(stageTransforms as TransferTransform<unknown>[]);
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
@ -266,7 +287,10 @@ class TransferEngine<
|
|||||||
/**
|
/**
|
||||||
* Shorthand method used to trigger stage update events to every listeners
|
* Shorthand method used to trigger stage update events to every listeners
|
||||||
*/
|
*/
|
||||||
#emitStageUpdate(type: 'start' | 'finish' | 'progress' | 'skip', transferStage: TransferStage) {
|
#emitStageUpdate(
|
||||||
|
type: 'start' | 'finish' | 'progress' | 'skip' | 'error',
|
||||||
|
transferStage: TransferStage
|
||||||
|
) {
|
||||||
this.progress.stream.emit(`stage::${type}`, {
|
this.progress.stream.emit(`stage::${type}`, {
|
||||||
data: this.progress.data,
|
data: this.progress.data,
|
||||||
stage: transferStage,
|
stage: transferStage,
|
||||||
@ -475,7 +499,7 @@ class TransferEngine<
|
|||||||
this.#emitStageUpdate('start', stage);
|
this.#emitStageUpdate('start', stage);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let stream: Stream = source;
|
let stream: Readable = source;
|
||||||
|
|
||||||
if (transform) {
|
if (transform) {
|
||||||
stream = stream.pipe(transform);
|
stream = stream.pipe(transform);
|
||||||
@ -485,15 +509,17 @@ class TransferEngine<
|
|||||||
stream = stream.pipe(tracker);
|
stream = stream.pipe(tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream
|
this.#currentStream = stream
|
||||||
.pipe(destination)
|
.pipe(destination)
|
||||||
.on('error', (e) => {
|
.on('error', (e) => {
|
||||||
updateEndTime();
|
updateEndTime();
|
||||||
|
this.#emitStageUpdate('error', stage);
|
||||||
this.#reportError(e, 'error');
|
this.#reportError(e, 'error');
|
||||||
destination.destroy(e);
|
destination.destroy(e);
|
||||||
reject(e);
|
reject(e);
|
||||||
})
|
})
|
||||||
.on('close', () => {
|
.on('close', () => {
|
||||||
|
this.#currentStream = undefined;
|
||||||
updateEndTime();
|
updateEndTime();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@ -502,6 +528,11 @@ class TransferEngine<
|
|||||||
this.#emitStageUpdate('finish', stage);
|
this.#emitStageUpdate('finish', stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cause an ongoing transfer to abort gracefully
|
||||||
|
async abortTransfer(): Promise<void> {
|
||||||
|
this.#currentStream?.destroy(new TransferEngineError('fatal', 'Transfer aborted.'));
|
||||||
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
// Resolve providers' resource and store
|
// Resolve providers' resource and store
|
||||||
// them in the engine's internal state
|
// them in the engine's internal state
|
||||||
|
@ -7,6 +7,7 @@ import type { IHandlerOptions, TransferMethod } from './types';
|
|||||||
import { ProviderTransferError } from '../../../errors/providers';
|
import { ProviderTransferError } from '../../../errors/providers';
|
||||||
|
|
||||||
type WSCallback = (client: WebSocket, request: IncomingMessage) => void;
|
type WSCallback = (client: WebSocket, request: IncomingMessage) => void;
|
||||||
|
type BufferLike = Parameters<WebSocket['send']>[0];
|
||||||
|
|
||||||
const VALID_TRANSFER_COMMANDS = ['init', 'end', 'status'] as const;
|
const VALID_TRANSFER_COMMANDS = ['init', 'end', 'status'] as const;
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ export interface Handler {
|
|||||||
/**
|
/**
|
||||||
* It sends a message to the client
|
* It sends a message to the client
|
||||||
*/
|
*/
|
||||||
send<T = unknown>(message: T, cb?: (err?: Error) => void): void;
|
send<T extends BufferLike>(message: T, cb?: (err?: Error) => void): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It sends a message to the client and waits for a confirmation
|
* It sends a message to the client and waits for a confirmation
|
||||||
@ -185,7 +186,7 @@ export const handlerFactory =
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = JSON.stringify({
|
||||||
uuid,
|
uuid,
|
||||||
data: data ?? null,
|
data: data ?? null,
|
||||||
error: e
|
error: e
|
||||||
@ -194,7 +195,7 @@ export const handlerFactory =
|
|||||||
message: e?.message,
|
message: e?.message,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
};
|
});
|
||||||
|
|
||||||
this.send(payload, (error) => (error ? reject(error) : resolve()));
|
this.send(payload, (error) => (error ? reject(error) : resolve()));
|
||||||
});
|
});
|
||||||
@ -216,7 +217,7 @@ export const handlerFactory =
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
|
|
||||||
const payload = { uuid, data: message };
|
const payload = JSON.stringify({ uuid, data: message });
|
||||||
|
|
||||||
this.send(payload, (error) => {
|
this.send(payload, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -144,4 +144,7 @@ export interface ITransferEngineOptions {
|
|||||||
// List of TransferTransformList preset options to exclude/include
|
// List of TransferTransformList preset options to exclude/include
|
||||||
exclude: TransferFilterPreset[];
|
exclude: TransferFilterPreset[];
|
||||||
only: TransferFilterPreset[];
|
only: TransferFilterPreset[];
|
||||||
|
|
||||||
|
// delay after each record
|
||||||
|
throttle: number;
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,10 @@ const { exitWith, ifOptions, assertUrlHasProtocol } = require('../lib/commands/u
|
|||||||
const {
|
const {
|
||||||
excludeOption,
|
excludeOption,
|
||||||
onlyOption,
|
onlyOption,
|
||||||
|
throttleOption,
|
||||||
validateExcludeOnly,
|
validateExcludeOnly,
|
||||||
} = require('../lib/commands/transfer/utils');
|
} = require('../lib/commands/transfer/utils');
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkCwdIsStrapiApp = (name) => {
|
const checkCwdIsStrapiApp = (name) => {
|
||||||
const logErrorAndExit = () => {
|
const logErrorAndExit = () => {
|
||||||
console.log(
|
console.log(
|
||||||
@ -295,7 +292,16 @@ program
|
|||||||
.addOption(forceOption)
|
.addOption(forceOption)
|
||||||
.addOption(excludeOption)
|
.addOption(excludeOption)
|
||||||
.addOption(onlyOption)
|
.addOption(onlyOption)
|
||||||
|
.addOption(throttleOption)
|
||||||
.hook('preAction', validateExcludeOnly)
|
.hook('preAction', validateExcludeOnly)
|
||||||
|
.hook(
|
||||||
|
'preAction',
|
||||||
|
ifOptions(
|
||||||
|
(opts) => !(opts.from || opts.to) || (opts.from && opts.to),
|
||||||
|
() =>
|
||||||
|
exitWith(1, 'Exactly one remote source (from) or destination (to) option must be provided')
|
||||||
|
)
|
||||||
|
)
|
||||||
// If --from is used, validate the URL and token
|
// If --from is used, validate the URL and token
|
||||||
.hook(
|
.hook(
|
||||||
'preAction',
|
'preAction',
|
||||||
@ -312,7 +318,7 @@ program
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
if (!answers.fromToken?.length) {
|
if (!answers.fromToken?.length) {
|
||||||
exitWith(1, 'No token entered, aborting transfer.');
|
exitWith(1, 'No token provided for remote source, aborting transfer.');
|
||||||
}
|
}
|
||||||
thisCommand.opts().fromToken = answers.fromToken;
|
thisCommand.opts().fromToken = answers.fromToken;
|
||||||
}
|
}
|
||||||
@ -335,7 +341,7 @@ program
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
if (!answers.toToken?.length) {
|
if (!answers.toToken?.length) {
|
||||||
exitWith(1, 'No token entered, aborting transfer.');
|
exitWith(1, 'No token provided for remote destination, aborting transfer.');
|
||||||
}
|
}
|
||||||
thisCommand.opts().toToken = answers.toToken;
|
thisCommand.opts().toToken = answers.toToken;
|
||||||
}
|
}
|
||||||
@ -367,6 +373,7 @@ program
|
|||||||
.addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
|
.addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
|
||||||
.addOption(excludeOption)
|
.addOption(excludeOption)
|
||||||
.addOption(onlyOption)
|
.addOption(onlyOption)
|
||||||
|
.addOption(throttleOption)
|
||||||
.hook('preAction', validateExcludeOnly)
|
.hook('preAction', validateExcludeOnly)
|
||||||
.hook('preAction', promptEncryptionKey)
|
.hook('preAction', promptEncryptionKey)
|
||||||
.action(getLocalScript('transfer/export'));
|
.action(getLocalScript('transfer/export'));
|
||||||
@ -389,6 +396,7 @@ program
|
|||||||
.addOption(forceOption)
|
.addOption(forceOption)
|
||||||
.addOption(excludeOption)
|
.addOption(excludeOption)
|
||||||
.addOption(onlyOption)
|
.addOption(onlyOption)
|
||||||
|
.addOption(throttleOption)
|
||||||
.hook('preAction', validateExcludeOnly)
|
.hook('preAction', validateExcludeOnly)
|
||||||
.hook('preAction', async (thisCommand) => {
|
.hook('preAction', async (thisCommand) => {
|
||||||
const opts = thisCommand.opts();
|
const opts = thisCommand.opts();
|
||||||
|
@ -72,6 +72,7 @@ describe('Export', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
exitMessageText: jest.fn(),
|
||||||
};
|
};
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'../../transfer/utils',
|
'../../transfer/utils',
|
||||||
|
@ -78,6 +78,7 @@ describe('Import', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
exitMessageText: jest.fn(),
|
||||||
};
|
};
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'../../transfer/utils',
|
'../../transfer/utils',
|
||||||
|
@ -22,6 +22,7 @@ describe('Transfer', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
exitMessageText: jest.fn(),
|
||||||
};
|
};
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'../../transfer/utils',
|
'../../transfer/utils',
|
||||||
@ -35,6 +36,9 @@ describe('Transfer', () => {
|
|||||||
strapi: {
|
strapi: {
|
||||||
providers: {
|
providers: {
|
||||||
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testLocalSource' }),
|
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testLocalSource' }),
|
||||||
|
createLocalStrapiDestinationProvider: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ name: 'testLocalDestination' }),
|
||||||
createRemoteStrapiDestinationProvider: jest
|
createRemoteStrapiDestinationProvider: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue({ name: 'testRemoteDest' }),
|
.mockReturnValue({ name: 'testRemoteDest' }),
|
||||||
@ -75,9 +79,11 @@ describe('Transfer', () => {
|
|||||||
jest.spyOn(console, 'info').mockImplementation(() => {});
|
jest.spyOn(console, 'info').mockImplementation(() => {});
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
const destinationUrl = new URL('http://strapi.com/admin');
|
const destinationUrl = new URL('http://one.localhost/admin');
|
||||||
const destinationToken = 'test-token';
|
const destinationToken = 'test-token';
|
||||||
|
|
||||||
|
const sourceUrl = new URL('http://two.localhost/admin');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@ -87,7 +93,19 @@ describe('Transfer', () => {
|
|||||||
await transferCommand({ from: undefined, to: undefined });
|
await transferCommand({ from: undefined, to: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/at least one source/i));
|
expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/one source/i));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exits with error when both --to and --from are provided', async () => {
|
||||||
|
await expectExit(1, async () => {
|
||||||
|
await transferCommand({ from: sourceUrl, to: destinationUrl });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/one source/i));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
mockDataTransfer.strapi.providers.createRemoteStrapiDestinationProvider
|
||||||
@ -139,6 +157,8 @@ describe('Transfer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.todo('uses local Strapi destination when to is not specified');
|
||||||
|
|
||||||
it('uses restore as the default strategy', async () => {
|
it('uses restore as the default strategy', async () => {
|
||||||
await expectExit(0, async () => {
|
await expectExit(0, async () => {
|
||||||
await transferCommand({ from: undefined, to: destinationUrl, toToken: destinationToken });
|
await transferCommand({ from: undefined, to: destinationUrl, toToken: destinationToken });
|
||||||
|
@ -22,15 +22,20 @@ const {
|
|||||||
createStrapiInstance,
|
createStrapiInstance,
|
||||||
formatDiagnostic,
|
formatDiagnostic,
|
||||||
loadersFactory,
|
loadersFactory,
|
||||||
|
exitMessageText,
|
||||||
|
abortTransfer,
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
const { exitWith } = require('../utils/helpers');
|
const { exitWith } = require('../utils/helpers');
|
||||||
/**
|
/**
|
||||||
* @typedef ExportCommandOptions Options given to the CLI import command
|
* @typedef ExportCommandOptions Options given to the CLI import command
|
||||||
*
|
*
|
||||||
* @property {string} [file] The file path to import
|
* @property {string} [file] The file path to export to
|
||||||
* @property {boolean} [encrypt] Used to encrypt the final archive
|
* @property {boolean} [encrypt] Used to encrypt the final archive
|
||||||
* @property {string} [key] Encryption key, only useful when encryption is enabled
|
* @property {string} [key] Encryption key, used only when encryption is enabled
|
||||||
* @property {boolean} [compress] Used to compress the final archive
|
* @property {boolean} [compress] Used to compress the final archive
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
|
||||||
|
* @property {number|undefined} [throttle] Delay in ms after each record
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BYTES_IN_MB = 1024 * 1024;
|
const BYTES_IN_MB = 1024 * 1024;
|
||||||
@ -58,6 +63,7 @@ module.exports = async (opts) => {
|
|||||||
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
|
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
|
||||||
exclude: opts.exclude,
|
exclude: opts.exclude,
|
||||||
only: opts.only,
|
only: opts.only,
|
||||||
|
throttle: opts.throttle,
|
||||||
transforms: {
|
transforms: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -108,12 +114,19 @@ module.exports = async (opts) => {
|
|||||||
|
|
||||||
progress.on('transfer::start', async () => {
|
progress.on('transfer::start', async () => {
|
||||||
console.log(`Starting export...`);
|
console.log(`Starting export...`);
|
||||||
|
|
||||||
await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
||||||
});
|
});
|
||||||
|
|
||||||
let results;
|
let results;
|
||||||
let outFile;
|
let outFile;
|
||||||
try {
|
try {
|
||||||
|
// Abort transfer if user interrupts process
|
||||||
|
['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
|
||||||
|
process.removeAllListeners(signal);
|
||||||
|
process.on(signal, () => abortTransfer({ engine, strapi }));
|
||||||
|
});
|
||||||
|
|
||||||
results = await engine.transfer();
|
results = await engine.transfer();
|
||||||
outFile = results.destination.file.path;
|
outFile = results.destination.file.path;
|
||||||
const outFileExists = await fs.pathExists(outFile);
|
const outFileExists = await fs.pathExists(outFile);
|
||||||
@ -122,7 +135,7 @@ module.exports = async (opts) => {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
exitWith(1, 'Export process failed.');
|
exitWith(1, exitMessageText('export', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
@ -133,8 +146,8 @@ module.exports = async (opts) => {
|
|||||||
console.error('There was an error displaying the results of the transfer.');
|
console.error('There was an error displaying the results of the transfer.');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`${chalk.bold('Export process has been completed successfully!')}`);
|
console.log(`Export archive is in ${chalk.green(outFile)}`);
|
||||||
exitWith(0, `Export archive is in ${chalk.green(outFile)}`);
|
exitWith(0, exitMessageText('export'));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,13 +18,32 @@ const {
|
|||||||
createStrapiInstance,
|
createStrapiInstance,
|
||||||
formatDiagnostic,
|
formatDiagnostic,
|
||||||
loadersFactory,
|
loadersFactory,
|
||||||
|
exitMessageText,
|
||||||
|
abortTransfer,
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
const { exitWith } = require('../utils/helpers');
|
const { exitWith } = require('../utils/helpers');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
|
* @typedef {import('@strapi/data-transfer/src/file/providers').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ImportCommandOptions Options given to the CLI import command
|
||||||
|
*
|
||||||
|
* @property {string} [file] The file path to import
|
||||||
|
* @property {string} [key] Encryption key, used when encryption is enabled
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
|
||||||
|
* @property {number|undefined} [throttle] Delay in ms after each record
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import command.
|
||||||
|
*
|
||||||
|
* It transfers data from a file to a local Strapi instance
|
||||||
|
*
|
||||||
|
* @param {ImportCommandOptions} opts
|
||||||
|
*/
|
||||||
module.exports = async (opts) => {
|
module.exports = async (opts) => {
|
||||||
// validate inputs from Commander
|
// validate inputs from Commander
|
||||||
if (!isObject(opts)) {
|
if (!isObject(opts)) {
|
||||||
@ -63,6 +82,7 @@ module.exports = async (opts) => {
|
|||||||
schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
|
schemaStrategy: opts.schemaStrategy || DEFAULT_SCHEMA_STRATEGY,
|
||||||
exclude: opts.exclude,
|
exclude: opts.exclude,
|
||||||
only: opts.only,
|
only: opts.only,
|
||||||
|
throttle: opts.throttle,
|
||||||
rules: {
|
rules: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -118,11 +138,16 @@ module.exports = async (opts) => {
|
|||||||
|
|
||||||
let results;
|
let results;
|
||||||
try {
|
try {
|
||||||
|
// Abort transfer if user interrupts process
|
||||||
|
['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
|
||||||
|
process.removeAllListeners(signal);
|
||||||
|
process.on(signal, () => abortTransfer({ engine, strapi }));
|
||||||
|
});
|
||||||
|
|
||||||
results = await engine.transfer();
|
results = await engine.transfer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
console.error('Import process failed.');
|
exitWith(1, exitMessageText('import', true));
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -135,7 +160,7 @@ module.exports = async (opts) => {
|
|||||||
await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
await strapiInstance.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
await strapiInstance.destroy();
|
await strapiInstance.destroy();
|
||||||
|
|
||||||
exitWith(0, 'Import process has been completed successfully!');
|
exitWith(0, exitMessageText('import'));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +12,6 @@ const {
|
|||||||
},
|
},
|
||||||
} = require('@strapi/data-transfer');
|
} = require('@strapi/data-transfer');
|
||||||
const { isObject } = require('lodash/fp');
|
const { isObject } = require('lodash/fp');
|
||||||
const chalk = require('chalk');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
buildTransferTable,
|
buildTransferTable,
|
||||||
@ -20,6 +19,8 @@ const {
|
|||||||
DEFAULT_IGNORED_CONTENT_TYPES,
|
DEFAULT_IGNORED_CONTENT_TYPES,
|
||||||
formatDiagnostic,
|
formatDiagnostic,
|
||||||
loadersFactory,
|
loadersFactory,
|
||||||
|
exitMessageText,
|
||||||
|
abortTransfer,
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
const { exitWith } = require('../utils/helpers');
|
const { exitWith } = require('../utils/helpers');
|
||||||
|
|
||||||
@ -30,6 +31,9 @@ const { exitWith } = require('../utils/helpers');
|
|||||||
* @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
|
* @property {URL|undefined} [from] The url of a remote Strapi to use as remote source
|
||||||
* @property {string|undefined} [toToken] The transfer token for the remote Strapi destination
|
* @property {string|undefined} [toToken] The transfer token for the remote Strapi destination
|
||||||
* @property {string|undefined} [fromToken] The transfer token for the remote Strapi source
|
* @property {string|undefined} [fromToken] The transfer token for the remote Strapi source
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [only] If present, only include these filtered groups of data
|
||||||
|
* @property {(keyof import('@strapi/data-transfer/src/engine').TransferGroupFilter)[]} [exclude] If present, exclude these filtered groups of data
|
||||||
|
* @property {number|undefined} [throttle] Delay in ms after each record
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,15 +49,14 @@ module.exports = async (opts) => {
|
|||||||
exitWith(1, 'Could not parse command arguments');
|
exitWith(1, 'Could not parse command arguments');
|
||||||
}
|
}
|
||||||
|
|
||||||
const strapi = await createStrapiInstance();
|
if (!(opts.from || opts.to) || (opts.from && opts.to)) {
|
||||||
|
exitWith(1, 'Exactly one source (from) or destination (to) option must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const strapi = await createStrapiInstance();
|
||||||
let source;
|
let source;
|
||||||
let destination;
|
let destination;
|
||||||
|
|
||||||
if (!opts.from && !opts.to) {
|
|
||||||
exitWith(1, 'At least one source (from) or destination (to) option must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no URL provided, use local Strapi
|
// if no URL provided, use local Strapi
|
||||||
if (!opts.from) {
|
if (!opts.from) {
|
||||||
source = createLocalStrapiSourceProvider({
|
source = createLocalStrapiSourceProvider({
|
||||||
@ -62,6 +65,10 @@ module.exports = async (opts) => {
|
|||||||
}
|
}
|
||||||
// if URL provided, set up a remote source provider
|
// if URL provided, set up a remote source provider
|
||||||
else {
|
else {
|
||||||
|
if (!opts.fromToken) {
|
||||||
|
exitWith(1, 'Missing token for remote destination');
|
||||||
|
}
|
||||||
|
|
||||||
source = createRemoteStrapiSourceProvider({
|
source = createRemoteStrapiSourceProvider({
|
||||||
getStrapi: () => strapi,
|
getStrapi: () => strapi,
|
||||||
url: opts.from,
|
url: opts.from,
|
||||||
@ -108,6 +115,9 @@ module.exports = async (opts) => {
|
|||||||
const engine = createTransferEngine(source, destination, {
|
const engine = createTransferEngine(source, destination, {
|
||||||
versionStrategy: 'exact',
|
versionStrategy: 'exact',
|
||||||
schemaStrategy: 'strict',
|
schemaStrategy: 'strict',
|
||||||
|
exclude: opts.exclude,
|
||||||
|
only: opts.only,
|
||||||
|
throttle: opts.throttle,
|
||||||
transforms: {
|
transforms: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@ -147,15 +157,26 @@ module.exports = async (opts) => {
|
|||||||
updateLoader(stage, data);
|
updateLoader(stage, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
progress.on('stage::error', ({ stage, data }) => {
|
||||||
|
updateLoader(stage, data).fail();
|
||||||
|
});
|
||||||
|
|
||||||
let results;
|
let results;
|
||||||
try {
|
try {
|
||||||
console.log(`Starting transfer...`);
|
console.log(`Starting transfer...`);
|
||||||
|
|
||||||
|
// Abort transfer if user interrupts process
|
||||||
|
['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach((signal) => {
|
||||||
|
process.removeAllListeners(signal);
|
||||||
|
process.on(signal, () => abortTransfer({ engine, strapi }));
|
||||||
|
});
|
||||||
|
|
||||||
results = await engine.transfer();
|
results = await engine.transfer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
exitWith(1, 'Transfer process failed.');
|
exitWith(1, exitMessageText('transfer', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = buildTransferTable(results.engine);
|
const table = buildTransferTable(results.engine);
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
exitWith(0, `${chalk.bold('Transfer process has been completed successfully!')}`);
|
exitWith(0, exitMessageText('transfer'));
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,19 @@ const {
|
|||||||
const ora = require('ora');
|
const ora = require('ora');
|
||||||
const { readableBytes, exitWith } = require('../utils/helpers');
|
const { readableBytes, exitWith } = require('../utils/helpers');
|
||||||
const strapi = require('../../index');
|
const strapi = require('../../index');
|
||||||
const { getParseListWithChoices } = require('../utils/commander');
|
const { getParseListWithChoices, parseInteger } = require('../utils/commander');
|
||||||
|
|
||||||
|
const exitMessageText = (process, error = false) => {
|
||||||
|
const processCapitalized = process[0].toUpperCase() + process.slice(1);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return chalk.bold(
|
||||||
|
chalk.green(`${processCapitalized} process has been completed successfully!`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chalk.bold(chalk.red(`${processCapitalized} process failed.`));
|
||||||
|
};
|
||||||
|
|
||||||
const pad = (n) => {
|
const pad = (n) => {
|
||||||
return (n < 10 ? '0' : '') + String(n);
|
return (n < 10 ? '0' : '') + String(n);
|
||||||
@ -90,12 +102,23 @@ const DEFAULT_IGNORED_CONTENT_TYPES = [
|
|||||||
'admin::audit-log',
|
'admin::audit-log',
|
||||||
];
|
];
|
||||||
|
|
||||||
const createStrapiInstance = async (logLevel = 'error') => {
|
const abortTransfer = async ({ engine, strapi }) => {
|
||||||
|
try {
|
||||||
|
await engine.abortTransfer();
|
||||||
|
await strapi.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore because there's not much else we can do
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStrapiInstance = async (opts = {}) => {
|
||||||
try {
|
try {
|
||||||
const appContext = await strapi.compile();
|
const appContext = await strapi.compile();
|
||||||
const app = strapi(appContext);
|
const app = strapi({ ...opts, ...appContext });
|
||||||
|
|
||||||
app.log.level = logLevel;
|
app.log.level = opts.logLevel || 'error';
|
||||||
return await app.load();
|
return await app.load();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ECONNREFUSED') {
|
if (err.code === 'ECONNREFUSED') {
|
||||||
@ -107,6 +130,13 @@ const createStrapiInstance = async (logLevel = 'error') => {
|
|||||||
|
|
||||||
const transferDataTypes = Object.keys(TransferGroupPresets);
|
const transferDataTypes = Object.keys(TransferGroupPresets);
|
||||||
|
|
||||||
|
const throttleOption = new Option(
|
||||||
|
'--throttle <delay after each entity>',
|
||||||
|
`Add a delay in milliseconds between each transferred entity`
|
||||||
|
)
|
||||||
|
.argParser(parseInteger)
|
||||||
|
.hideHelp(); // This option is not publicly documented
|
||||||
|
|
||||||
const excludeOption = new Option(
|
const excludeOption = new Option(
|
||||||
'--exclude <comma-separated data types>',
|
'--exclude <comma-separated data types>',
|
||||||
`Exclude data using comma-separated types. Available types: ${transferDataTypes.join(',')}`
|
`Exclude data using comma-separated types. Available types: ${transferDataTypes.join(',')}`
|
||||||
@ -219,7 +249,10 @@ module.exports = {
|
|||||||
DEFAULT_IGNORED_CONTENT_TYPES,
|
DEFAULT_IGNORED_CONTENT_TYPES,
|
||||||
createStrapiInstance,
|
createStrapiInstance,
|
||||||
excludeOption,
|
excludeOption,
|
||||||
|
exitMessageText,
|
||||||
onlyOption,
|
onlyOption,
|
||||||
|
throttleOption,
|
||||||
validateExcludeOnly,
|
validateExcludeOnly,
|
||||||
formatDiagnostic,
|
formatDiagnostic,
|
||||||
|
abortTransfer,
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
const inquirer = require('inquirer');
|
const inquirer = require('inquirer');
|
||||||
const { InvalidOptionArgumentError, Option } = require('commander');
|
const { InvalidOptionArgumentError, Option } = require('commander');
|
||||||
const { bold, green, cyan } = require('chalk');
|
const { bold, green, cyan } = require('chalk');
|
||||||
|
const { isNaN } = require('lodash/fp');
|
||||||
const { exitWith } = require('./helpers');
|
const { exitWith } = require('./helpers');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +41,18 @@ const getParseListWithChoices = (choices, errorMessage = 'Invalid options:') =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* argParser: Parse a string as an integer
|
||||||
|
*/
|
||||||
|
const parseInteger = (value) => {
|
||||||
|
// parseInt takes a string and a radix
|
||||||
|
const parsedValue = parseInt(value, 10);
|
||||||
|
if (isNaN(parsedValue)) {
|
||||||
|
throw new InvalidOptionArgumentError(`Not an integer: ${value}`);
|
||||||
|
}
|
||||||
|
return parsedValue;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* argParser: Parse a string as a URL object
|
* argParser: Parse a string as a URL object
|
||||||
*/
|
*/
|
||||||
@ -131,6 +144,7 @@ module.exports = {
|
|||||||
getParseListWithChoices,
|
getParseListWithChoices,
|
||||||
parseList,
|
parseList,
|
||||||
parseURL,
|
parseURL,
|
||||||
|
parseInteger,
|
||||||
promptEncryptionKey,
|
promptEncryptionKey,
|
||||||
confirmMessage,
|
confirmMessage,
|
||||||
forceOption,
|
forceOption,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user