diff --git a/packages/core/data-transfer/lib/providers/local-file-destination-provider/index.ts b/packages/core/data-transfer/lib/providers/local-file-destination-provider/index.ts index e34df30cf1..b5f123b79a 100644 --- a/packages/core/data-transfer/lib/providers/local-file-destination-provider/index.ts +++ b/packages/core/data-transfer/lib/providers/local-file-destination-provider/index.ts @@ -18,18 +18,15 @@ import { chain, Writable } from 'stream-chain'; import { createEncryptionCipher } from '../../encryption/encrypt'; import { createFilePathFactory, createTarEntryStream } from './utils'; export interface ILocalFileDestinationProviderOptions { - // Encryption encryption: { enabled: boolean; key?: string; }; - // Compression compression: { enabled: boolean; }; - // File file: { path: string; maxSize?: number; diff --git a/packages/core/data-transfer/lib/providers/local-file-source-provider.ts b/packages/core/data-transfer/lib/providers/local-file-source-provider.ts index 3ccbea728d..072d5ebae9 100644 --- a/packages/core/data-transfer/lib/providers/local-file-source-provider.ts +++ b/packages/core/data-transfer/lib/providers/local-file-source-provider.ts @@ -24,25 +24,18 @@ const METADATA_FILE_PATH = 'metadata.json'; * Provider options */ export interface ILocalFileSourceProviderOptions { - /** - * Path to the backup archive - */ - backupFilePath: string; + file: { + path: string; + }; - /** - * Whether the backup data is encrypted or not - */ - encrypted?: boolean; + encryption: { + enabled: boolean; + key?: string; + }; - /** - * Encryption key used to decrypt the encrypted data (if necessary) - */ - encryptionKey?: string; - - /** - * Whether the backup data is compressed or not - */ - compressed?: boolean; + compression: { + enabled: boolean; + }; } export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderOptions) => { @@ -58,7 +51,9 @@ class LocalFileSourceProvider implements ISourceProvider { constructor(options: ILocalFileSourceProviderOptions) { this.options = options; - if (this.options.encrypted && this.options.encryptionKey === undefined) { + const { encryption } = this.options; + + if (encryption.enabled && encryption.key === undefined) { throw new Error('Missing encryption key'); } } @@ -67,7 +62,7 @@ class LocalFileSourceProvider implements ISourceProvider { * Pre flight checks regarding the provided options (making sure that the provided path is correct, etc...) */ bootstrap() { - const path = this.options.backupFilePath; + const { path } = this.options.file; const isValidBackupPath = fs.existsSync(path); // Check if the provided path exists @@ -108,13 +103,17 @@ class LocalFileSourceProvider implements ISourceProvider { return this.#streamJsonlDirectory('configuration'); } - #getBackupStream(decompress: boolean = true) { - const path = this.options.backupFilePath; - const readStream = fs.createReadStream(path); - const streams: StreamItemArray = [readStream]; + #getBackupStream() { + const { file, encryption, compression } = this.options; - // Handle decompression - if (decompress) { + const fileStream = fs.createReadStream(file.path); + const streams: StreamItemArray = [fileStream]; + + if (encryption.enabled && encryption.key) { + streams.push(createDecryptionCipher(encryption.key)); + } + + if (compression.enabled) { streams.push(zip.createGunzip()); } @@ -146,22 +145,12 @@ class LocalFileSourceProvider implements ISourceProvider { }, onentry(entry) { - const transforms = []; - - if (options.encrypted) { - transforms.push(createDecryptionCipher(options.encryptionKey!)); - } - - if (options.compressed) { - transforms.push(zip.createGunzip()); - } - - transforms.push( + const transforms = [ // JSONL parser to read the data chunks one by one (line by line) parser(), // The JSONL parser returns each line as key/value - (line: { key: string; value: any }) => line.value - ); + (line: { key: string; value: any }) => line.value, + ]; entry // Pipe transforms @@ -220,7 +209,7 @@ class LocalFileSourceProvider implements ISourceProvider { () => { // If the promise hasn't been resolved and we've parsed all // the archive entries, then the file doesn't exist - reject(`${filePath} not found in the archive stream`); + reject(new Error(`File "${filePath}" not found`)); } ); }); diff --git a/packages/core/data-transfer/lib/providers/local-strapi-destination-provider/index.ts b/packages/core/data-transfer/lib/providers/local-strapi-destination-provider/index.ts index d79d733029..f42852baca 100644 --- a/packages/core/data-transfer/lib/providers/local-strapi-destination-provider/index.ts +++ b/packages/core/data-transfer/lib/providers/local-strapi-destination-provider/index.ts @@ -15,10 +15,6 @@ interface ILocalStrapiDestinationProviderOptions { strategy: 'restore' | 'merge'; } -// TODO: getting some type errors with @strapi/logger that need to be resolved first -// const log = createLogger(); -const log = console; - export const createLocalStrapiDestinationProvider = ( options: ILocalStrapiDestinationProviderOptions ) => { diff --git a/packages/core/strapi/bin/strapi.js b/packages/core/strapi/bin/strapi.js index d1c8c1ecce..97bfadfffa 100755 --- a/packages/core/strapi/bin/strapi.js +++ b/packages/core/strapi/bin/strapi.js @@ -5,6 +5,7 @@ // FIXME /* eslint-disable import/extensions */ const _ = require('lodash'); +const path = require('path'); const resolveCwd = require('resolve-cwd'); const { yellow } = require('chalk'); const { Command, Option } = require('commander'); @@ -316,14 +317,15 @@ program 'path and filename to the Strapi export file you want to import' ) .addOption( - new Option('--key ', 'Provide encryption key in command instead of using a prompt') + new Option('-k, --key ', 'Provide encryption key in command instead of using a prompt') ) .allowExcessArguments(false) .hook('preAction', async (thisCommand) => { const opts = thisCommand.opts(); + const ext = path.extname(String(opts.file)); // check extension to guess if we should prompt for key - if (String(opts.file).endsWith('.enc')) { + if (ext === '.enc') { if (!opts.key) { const answers = await inquirer.prompt([ { diff --git a/packages/core/strapi/lib/commands/transfer/import.js b/packages/core/strapi/lib/commands/transfer/import.js index 44e75b27ca..d0413a91a4 100644 --- a/packages/core/strapi/lib/commands/transfer/import.js +++ b/packages/core/strapi/lib/commands/transfer/import.js @@ -8,10 +8,15 @@ const { // eslint-disable-next-line import/no-unresolved, node/no-missing-require } = require('@strapi/data-transfer'); const { isObject } = require('lodash/fp'); +const path = require('path'); const strapi = require('../../index'); const { buildTransferTable } = require('./utils'); +/** + * @typedef {import('@strapi/data-transfer').ILocalFileSourceProviderOptions} ILocalFileSourceProviderOptions + */ + const logger = console; module.exports = async (opts) => { @@ -20,14 +25,12 @@ module.exports = async (opts) => { logger.error('Could not parse arguments'); process.exit(1); } - const filename = opts.file; /** * From strapi backup file */ - const sourceOptions = { - backupFilePath: filename, - }; + const sourceOptions = getLocalFileSourceOptions(opts); + const source = createLocalFileSourceProvider(sourceOptions); /** @@ -69,16 +72,50 @@ module.exports = async (opts) => { const engine = createTransferEngine(source, destination, engineOptions); try { - logger.log('Starting import...'); + logger.info('Starting import...'); const results = await engine.transfer(); const table = buildTransferTable(results.engine); - logger.log(table.toString()); + logger.info(table.toString()); - logger.log('Import process has been completed successfully!'); + logger.info('Import process has been completed successfully!'); process.exit(0); } catch (e) { - logger.log(`Import process failed unexpectedly: ${e.message}`); + logger.error(`Import process failed unexpectedly: ${e.message}`); process.exit(1); } }; + +/** + * Infer local file source provider options based on a given filename + * + * @param {{ file: string; key?: string }} opts + * + * @return {ILocalFileSourceProviderOptions} + */ +const getLocalFileSourceOptions = (opts) => { + /** + * @type {ILocalFileSourceProviderOptions} + */ + const options = { + file: { path: opts.file }, + compression: { enabled: false }, + encryption: { enabled: false }, + }; + + const { extname, parse } = path; + + let file = options.file.path; + + if (extname(file) === '.enc') { + file = parse(file).name; + options.encryption = { enabled: true, key: opts.key }; + } + + if (extname(file) === '.gz') { + file = parse(file).name; + options.compression = { enabled: true }; + } + + return options; +};