@strapi/data-transfer
Version:
Data transfer capabilities for Strapi
393 lines (389 loc) • 14.4 kB
JavaScript
'use strict';
var crypto = require('crypto');
var stream = require('stream');
var fp = require('lodash/fp');
var utils = require('../utils.js');
var constants = require('../../remote/constants.js');
var providers = require('../../../errors/providers.js');
function _class_private_field_loose_base(receiver, privateKey) {
if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
throw new TypeError("attempted to use private field on non-instance");
}
return receiver;
}
var id = 0;
function _class_private_field_loose_key(name) {
return "__private_" + id++ + "_" + name;
}
const jsonLength = (obj)=>Buffer.byteLength(JSON.stringify(obj));
var _diagnostics = /*#__PURE__*/ _class_private_field_loose_key("_diagnostics"), _startStepOnce = /*#__PURE__*/ _class_private_field_loose_key("_startStepOnce"), _startStep = /*#__PURE__*/ _class_private_field_loose_key("_startStep"), _endStep = /*#__PURE__*/ _class_private_field_loose_key("_endStep"), _streamStep = /*#__PURE__*/ _class_private_field_loose_key("_streamStep"), _writeStream = /*#__PURE__*/ _class_private_field_loose_key("_writeStream"), _reportInfo = /*#__PURE__*/ _class_private_field_loose_key("_reportInfo");
class RemoteStrapiDestinationProvider {
resetStats() {
this.stats = {
assets: {
count: 0
},
entities: {
count: 0
},
links: {
count: 0
},
configuration: {
count: 0
}
};
}
async initTransfer() {
const { strategy, restore } = this.options;
const query = this.dispatcher?.dispatchCommand({
command: 'init',
params: {
options: {
strategy,
restore
},
transfer: 'push'
}
});
const res = await query;
if (!res?.transferID) {
throw new providers.ProviderTransferError('Init failed, invalid response from the server');
}
this.resetStats();
return res.transferID;
}
async bootstrap(diagnostics) {
_class_private_field_loose_base(this, _diagnostics)[_diagnostics] = diagnostics;
const { url, auth } = this.options;
const validProtocols = [
'https:',
'http:'
];
let ws;
if (!validProtocols.includes(url.protocol)) {
throw new providers.ProviderValidationError(`Invalid protocol "${url.protocol}"`, {
check: 'url',
details: {
protocol: url.protocol,
validProtocols
}
});
}
const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${url.host}${utils.trimTrailingSlash(url.pathname)}${constants.TRANSFER_PATH}/push`;
_class_private_field_loose_base(this, _reportInfo)[_reportInfo]('establishing websocket connection');
// No auth defined, trying public access for transfer
if (!auth) {
ws = await utils.connectToWebsocket(wsUrl, undefined, _class_private_field_loose_base(this, _diagnostics)[_diagnostics]);
} else if (auth.type === 'token') {
const headers = {
Authorization: `Bearer ${auth.token}`
};
ws = await utils.connectToWebsocket(wsUrl, {
headers
}, _class_private_field_loose_base(this, _diagnostics)[_diagnostics]);
} else {
throw new providers.ProviderValidationError('Auth method not available', {
check: 'auth.type',
details: {
auth: auth.type
}
});
}
_class_private_field_loose_base(this, _reportInfo)[_reportInfo]('established websocket connection');
this.ws = ws;
const { retryMessageOptions } = this.options;
_class_private_field_loose_base(this, _reportInfo)[_reportInfo]('creating dispatcher');
this.dispatcher = utils.createDispatcher(this.ws, retryMessageOptions, (message)=>_class_private_field_loose_base(this, _reportInfo)[_reportInfo](message));
_class_private_field_loose_base(this, _reportInfo)[_reportInfo]('created dispatcher');
_class_private_field_loose_base(this, _reportInfo)[_reportInfo]('initialize transfer');
this.transferID = await this.initTransfer();
_class_private_field_loose_base(this, _reportInfo)[_reportInfo](`initialized transfer ${this.transferID}`);
this.dispatcher.setTransferProperties({
id: this.transferID,
kind: 'push'
});
await this.dispatcher.dispatchTransferAction('bootstrap');
}
async close() {
// Gracefully close the remote transfer process
if (this.transferID && this.dispatcher) {
await this.dispatcher.dispatchTransferAction('close');
await this.dispatcher.dispatchCommand({
command: 'end',
params: {
transferID: this.transferID
}
});
}
await new Promise((resolve)=>{
const { ws } = this;
if (!ws || ws.CLOSED) {
resolve();
return;
}
ws.on('close', ()=>resolve()).close();
});
}
getMetadata() {
return this.dispatcher?.dispatchTransferAction('getMetadata') ?? null;
}
async beforeTransfer() {
await this.dispatcher?.dispatchTransferAction('beforeTransfer');
}
async rollback() {
await this.dispatcher?.dispatchTransferAction('rollback');
}
getSchemas() {
if (!this.dispatcher) {
return Promise.resolve(null);
}
return this.dispatcher.dispatchTransferAction('getSchemas');
}
createEntitiesWriteStream() {
return _class_private_field_loose_base(this, _writeStream)[_writeStream]('entities');
}
createLinksWriteStream() {
return _class_private_field_loose_base(this, _writeStream)[_writeStream]('links');
}
createConfigurationWriteStream() {
return _class_private_field_loose_base(this, _writeStream)[_writeStream]('configuration');
}
createAssetsWriteStream() {
let batch = [];
let hasStarted = false;
const batchSize = 1024 * 1024; // 1MB;
const batchLength = ()=>{
return batch.reduce((acc, chunk)=>chunk.action === 'stream' ? acc + chunk.data.byteLength : acc, 0);
};
const startAssetsTransferOnce = _class_private_field_loose_base(this, _startStepOnce)[_startStepOnce]('assets');
const flush = async ()=>{
const streamError = await _class_private_field_loose_base(this, _streamStep)[_streamStep]('assets', batch);
batch = [];
return streamError;
};
const safePush = async (chunk)=>{
batch.push(chunk);
if (batchLength() >= batchSize) {
const streamError = await flush();
if (streamError) {
throw streamError;
}
}
};
return new stream.Writable({
objectMode: true,
final: async (callback)=>{
if (batch.length > 0) {
await flush();
}
if (hasStarted) {
const { error: endStepError } = await _class_private_field_loose_base(this, _endStep)[_endStep]('assets');
if (endStepError) {
return callback(endStepError);
}
}
return callback(null);
},
async write (asset, _encoding, callback) {
const startError = await startAssetsTransferOnce();
if (startError) {
return callback(startError);
}
hasStarted = true;
const assetID = crypto.randomUUID();
const { filename, filepath, stats, stream, metadata } = asset;
try {
await safePush({
action: 'start',
assetID,
data: {
filename,
filepath,
stats,
metadata
}
});
for await (const chunk of stream){
await safePush({
action: 'stream',
assetID,
data: chunk
});
}
await safePush({
action: 'end',
assetID
});
callback();
} catch (error) {
if (error instanceof Error) {
callback(error);
}
}
}
});
}
constructor(options){
Object.defineProperty(this, _startStepOnce, {
value: startStepOnce
});
Object.defineProperty(this, _startStep, {
value: startStep
});
Object.defineProperty(this, _endStep, {
value: endStep
});
Object.defineProperty(this, _streamStep, {
value: streamStep
});
Object.defineProperty(this, _writeStream, {
value: writeStream
});
Object.defineProperty(this, _reportInfo, {
value: reportInfo
});
Object.defineProperty(this, _diagnostics, {
writable: true,
value: void 0
});
this.name = 'destination::remote-strapi';
this.type = 'destination';
this.options = options;
this.ws = null;
this.dispatcher = null;
this.transferID = null;
this.resetStats();
}
}
function startStepOnce(stage) {
return fp.once(()=>_class_private_field_loose_base(this, _startStep)[_startStep](stage));
}
async function startStep(step) {
try {
await this.dispatcher?.dispatchTransferStep({
action: 'start',
step
});
} catch (e) {
if (e instanceof Error) {
return e;
}
if (typeof e === 'string') {
return new providers.ProviderTransferError(e);
}
return new providers.ProviderTransferError('Unexpected error');
}
this.stats[step] = {
count: 0
};
return null;
}
async function endStep(step) {
try {
const res = await this.dispatcher?.dispatchTransferStep({
action: 'end',
step
});
return {
stats: res?.stats ?? null,
error: null
};
} catch (e) {
if (e instanceof Error) {
return {
stats: null,
error: e
};
}
if (typeof e === 'string') {
return {
stats: null,
error: new providers.ProviderTransferError(e)
};
}
return {
stats: null,
error: new providers.ProviderTransferError('Unexpected error')
};
}
}
async function streamStep(step, message) {
try {
if (step === 'assets') {
const assetMessage = message;
this.stats[step].count += assetMessage.filter((data)=>data.action === 'start').length;
} else {
this.stats[step].count += message.length;
}
await this.dispatcher?.dispatchTransferStep({
action: 'stream',
step,
data: message
});
} catch (e) {
if (e instanceof Error) {
return e;
}
if (typeof e === 'string') {
return new providers.ProviderTransferError(e);
}
return new providers.ProviderTransferError('Unexpected error');
}
return null;
}
function writeStream(step) {
const batchSize = 1024 * 1024; // 1MB;
const startTransferOnce = _class_private_field_loose_base(this, _startStepOnce)[_startStepOnce](step);
let batch = [];
const batchLength = ()=>jsonLength(batch);
return new stream.Writable({
objectMode: true,
final: async (callback)=>{
if (batch.length > 0) {
const streamError = await _class_private_field_loose_base(this, _streamStep)[_streamStep](step, batch);
batch = [];
if (streamError) {
return callback(streamError);
}
}
const { error, stats } = await _class_private_field_loose_base(this, _endStep)[_endStep](step);
const { count } = this.stats[step];
if (stats && (stats.started !== count || stats.finished !== count)) {
callback(new Error(`Data missing: sent ${this.stats[step].count} ${step}, recieved ${stats.started} and saved ${stats.finished} ${step}`));
}
callback(error);
},
write: async (chunk, _encoding, callback)=>{
const startError = await startTransferOnce();
if (startError) {
return callback(startError);
}
batch.push(chunk);
if (batchLength() >= batchSize) {
const streamError = await _class_private_field_loose_base(this, _streamStep)[_streamStep](step, batch);
batch = [];
if (streamError) {
return callback(streamError);
}
}
callback();
}
});
}
function reportInfo(message) {
_class_private_field_loose_base(this, _diagnostics)[_diagnostics]?.report({
details: {
createdAt: new Date(),
message,
origin: 'remote-destination-provider'
},
kind: 'info'
});
}
const createRemoteStrapiDestinationProvider = (options)=>{
return new RemoteStrapiDestinationProvider(options);
};
exports.createRemoteStrapiDestinationProvider = createRemoteStrapiDestinationProvider;
//# sourceMappingURL=index.js.map