strapi/packages/core/data-transfer/lib/providers/local-file-destination-provider.ts

203 lines
5.3 KiB
TypeScript
Raw Normal View History

2022-10-19 15:43:52 +02:00
import fs from 'fs';
import path from 'path';
import zip from 'zlib';
2022-11-02 17:26:32 +01:00
import { Writable } from 'stream';
import { chain } from 'stream-chain';
2022-10-19 15:43:52 +02:00
import { stringer } from 'stream-json/jsonl/Stringer';
import type { IDestinationProvider, ProviderType, Stream } from '../../types';
2022-10-31 10:13:32 +01:00
import { createCipher } from '../encryption/encrypt';
2022-10-19 15:43:52 +02:00
export interface ILocalFileDestinationProviderOptions {
// Encryption
2022-10-24 16:27:56 +02:00
encryption: {
enabled: boolean;
key: string;
};
// Compressions
compression: {
enabled: boolean;
};
// File
file: {
path: string;
maxSize?: number;
};
2022-10-19 15:43:52 +02:00
}
export const createLocalFileDestinationProvider = (
options: ILocalFileDestinationProviderOptions
) => {
2022-10-20 10:01:36 +02:00
return new LocalFileDestinationProvider(options);
};
class LocalFileDestinationProvider implements IDestinationProvider {
2022-10-20 09:52:18 +02:00
name: string = 'destination::local-file';
2022-10-19 15:43:52 +02:00
type: ProviderType = 'destination';
options: ILocalFileDestinationProviderOptions;
constructor(options: ILocalFileDestinationProviderOptions) {
this.options = options;
}
2022-11-02 17:26:32 +01:00
#getDataTransformers() {
const transforms = [];
// Convert to stringified JSON lines
transforms.push(stringer());
// Compression
if (this.options.compression.enabled) {
transforms.push(zip.createGzip());
}
// Encryption
if (this.options.encryption.enabled) {
const cipher = createCipher(this.options.encryption.key);
transforms.push(cipher);
}
return transforms;
}
2022-10-19 15:43:52 +02:00
bootstrap(): void | Promise<void> {
2022-10-24 16:27:56 +02:00
const rootDir = this.options.file.path;
2022-10-19 15:43:52 +02:00
const dirExists = fs.existsSync(rootDir);
if (dirExists) {
fs.rmSync(rootDir, { force: true, recursive: true });
}
fs.mkdirSync(rootDir, { recursive: true });
fs.mkdirSync(path.join(rootDir, 'entities'));
fs.mkdirSync(path.join(rootDir, 'links'));
fs.mkdirSync(path.join(rootDir, 'media'));
fs.mkdirSync(path.join(rootDir, 'configuration'));
}
rollback(): void | Promise<void> {
2022-10-24 16:27:56 +02:00
fs.rmSync(this.options.file.path, { force: true, recursive: true });
2022-10-19 15:43:52 +02:00
}
getMetadata() {
return null;
}
2022-11-02 17:26:32 +01:00
getEntitiesStream(): NodeJS.WritableStream {
const filePathFactory = createFilePathFactory(this.options.file.path, 'entities');
2022-10-19 15:43:52 +02:00
2022-11-02 17:26:32 +01:00
// Transform streams
const transforms: Writable[] = this.#getDataTransformers();
2022-10-19 15:43:52 +02:00
// FS write stream
2022-11-02 17:26:32 +01:00
const fileStream = createMultiFilesWriteStream(filePathFactory, this.options.file.maxSize);
// Full pipeline
const streams = transforms.concat(fileStream);
2022-10-19 15:43:52 +02:00
return chain(streams);
}
2022-11-02 17:26:32 +01:00
getLinksStream(): NodeJS.WritableStream {
const filePathFactory = createFilePathFactory(this.options.file.path, 'links');
2022-10-19 15:43:52 +02:00
2022-11-02 17:26:32 +01:00
// Transform streams
const transforms: Writable[] = this.#getDataTransformers();
2022-10-19 15:43:52 +02:00
2022-11-02 17:26:32 +01:00
// FS write stream
const fileStream = createMultiFilesWriteStream(filePathFactory, this.options.file.maxSize);
// Full pipelines
const streams = transforms.concat(fileStream);
return chain(streams);
}
getConfigurationStream(): NodeJS.WritableStream {
const filePathFactory = createFilePathFactory(this.options.file.path, 'configuration');
// Transform streams
const transforms: Writable[] = this.#getDataTransformers();
2022-10-19 15:43:52 +02:00
// FS write stream
2022-11-02 17:26:32 +01:00
const fileStream = createMultiFilesWriteStream(filePathFactory, this.options.file.maxSize);
// Full pipeline
const streams = transforms.concat(fileStream);
2022-10-19 15:43:52 +02:00
return chain(streams);
}
}
/**
* Create a writable stream that can split the streamed data into
* multiple files based on a provided maximum file size value.
*/
const createMultiFilesWriteStream = (
filePathFactory: (index?: number) => string,
maxFileSize?: number
2022-11-02 17:26:32 +01:00
): Writable => {
2022-10-19 15:43:52 +02:00
let fileIndex = 0;
let fileSize = 0;
let maxSize = maxFileSize;
2022-10-19 15:49:34 +02:00
let writeStream: fs.WriteStream;
2022-10-19 15:43:52 +02:00
const createIndexedWriteStream = () => fs.createWriteStream(filePathFactory(fileIndex));
// If no maximum file size is provided, then return a basic fs write stream
if (maxFileSize === undefined) {
return createIndexedWriteStream();
}
if (maxFileSize <= 0) {
throw new Error('Max file size must be a positive number');
}
return new Writable({
write(chunk, encoding, callback) {
// Initialize the write stream value if undefined
if (!writeStream) {
writeStream = createIndexedWriteStream();
}
// Check that by adding this new chunk of data, we
// are not going to reach the maximum file size.
if (maxSize && fileSize + chunk.length > maxSize) {
2022-10-19 15:43:52 +02:00
// Update the counters' value
fileIndex++;
fileSize = 0;
// Replace old write stream
writeStream.destroy();
writeStream = createIndexedWriteStream();
}
// Update the actual file size
fileSize += chunk.length;
// Transfer the data to the up-to-date write stream
writeStream.write(chunk, encoding, callback);
},
});
};
2022-11-02 17:26:32 +01:00
/**
* Create a file path factory for a given path & prefix.
* Upon being called, the factory will return a file path for a given index
*/
const createFilePathFactory =
(src: string, directory: string, prefix: string = directory) =>
(fileIndex: number = 0): string => {
return path.join(
// Backup path
src,
// "{directory}/" directory
directory,
// "${prefix}_XXXXX.jsonl" file
`${prefix}_${String(fileIndex).padStart(5, '0')}.jsonl`
);
};