import * as plugins from './plugins.js'; import { Readable } from 'stream'; type TStreamSource = (streamFile: StreamFile) => Promise; /** * The StreamFile class represents a file as a stream. * It allows creating streams from a file path, a URL, or a buffer. * Use SmartFileFactory to create instances of this class. */ export class StreamFile { // STATIC public static async fromPath(filePath: string, smartFs?: any): Promise { if (!smartFs) { throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.'); } const streamSource: TStreamSource = async (streamFileArg) => { return await streamFileArg.smartFs.file(filePath).readStream(); }; const streamFile = new StreamFile(streamSource, filePath, smartFs); streamFile.multiUse = true; streamFile.byteLengthComputeFunction = async () => { const stats = await smartFs.file(filePath).stat(); return stats.size; }; return streamFile; } public static async fromUrl(url: string, smartFs?: any): Promise { const streamSource: TStreamSource = async (streamFileArg) => { const response = await plugins.smartrequest.SmartRequest.create() .url(url) .get(); return response.stream(); }; const streamFile = new StreamFile(streamSource, undefined, smartFs); streamFile.multiUse = true; streamFile.byteLengthComputeFunction = async () => { const response = await plugins.smartrequest.SmartRequest.create() .url(url) .accept('binary') .get(); const buffer = Buffer.from(await response.arrayBuffer()); return buffer.length; }; return streamFile; } public static fromBuffer( buffer: Buffer, relativeFilePath?: string, smartFs?: any ): StreamFile { const streamSource: TStreamSource = async (streamFileArg) => { const stream = new Readable(); stream.push(buffer); stream.push(null); // End of stream return stream; }; const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs); streamFile.multiUse = true; streamFile.byteLengthComputeFunction = async () => buffer.length; return streamFile; } /** * Creates a StreamFile from an existing Readable stream with an option for multiple uses. * @param stream A Node.js Readable stream. * @param relativeFilePath Optional file path for the stream. * @param multiUse If true, the stream can be read multiple times, caching its content. * @param smartFs Optional SmartFs instance for filesystem operations * @returns A StreamFile instance. */ public static fromStream( stream: Readable, relativeFilePath?: string, multiUse: boolean = false, smartFs?: any ): StreamFile { const streamSource: TStreamSource = (streamFileArg) => { if (streamFileArg.multiUse) { // If multi-use is enabled and we have cached content, create a new readable stream from the buffer const bufferedStream = new Readable(); bufferedStream.push(streamFileArg.cachedStreamBuffer); bufferedStream.push(null); // No more data to push return Promise.resolve(bufferedStream); } else { return Promise.resolve(stream); } }; const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs); streamFile.multiUse = multiUse; // If multi-use is enabled, cache the stream when it's first read if (multiUse) { const chunks: Buffer[] = []; stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); stream.on('end', () => { streamFile.cachedStreamBuffer = Buffer.concat(chunks); }); // It's important to handle errors that may occur during streaming stream.on('error', (err) => { console.error('Error while caching stream:', err); }); } return streamFile; } // INSTANCE relativeFilePath?: string; private streamSource: TStreamSource; private smartFs?: any; // enable stream based multi use private cachedStreamBuffer?: Buffer; public multiUse: boolean; public used: boolean = false; public byteLengthComputeFunction: () => Promise; private constructor(streamSource: TStreamSource, relativeFilePath?: string, smartFs?: any) { this.streamSource = streamSource; this.relativeFilePath = relativeFilePath; this.smartFs = smartFs; } // METHODS private checkMultiUse() { if (!this.multiUse && this.used) { throw new Error('This stream can only be used once.'); } this.used = true; } /** * Creates a new readable stream from the source. */ public async createReadStream(): Promise { const stream = await this.streamSource(this); // Check if it's a Web ReadableStream and convert to Node.js Readable if (stream && typeof (stream as any).getReader === 'function') { // This is a Web ReadableStream, convert it to Node.js Readable return Readable.fromWeb(stream as any); } // It's already a Node.js Readable stream return stream as Readable; } /** * Writes the stream to the disk at the specified path. * @param filePathArg The file path where the stream should be written. */ public async writeToDisk(filePathArg: string): Promise { if (!this.smartFs) { throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.'); } this.checkMultiUse(); const readStream = await this.createReadStream(); const writeStream = await this.smartFs.file(filePathArg).writeStream(); return new Promise((resolve, reject) => { readStream.pipe(writeStream); readStream.on('error', reject); writeStream.on('error', reject); writeStream.on('finish', resolve); }); } public async writeToDir(dirPathArg: string) { if (!this.smartFs) { throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.'); } this.checkMultiUse(); const filePath = plugins.path.join(dirPathArg, this.relativeFilePath); const dirPath = plugins.path.parse(filePath).dir; await this.smartFs.directory(dirPath).create({ recursive: true }); return this.writeToDisk(filePath); } public async getContentAsBuffer() { this.checkMultiUse(); const done = plugins.smartpromise.defer(); const readStream = await this.createReadStream(); const chunks: Buffer[] = []; readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); readStream.on('error', done.reject); readStream.on('end', () => { const contentBuffer = Buffer.concat(chunks); done.resolve(contentBuffer); }); return done.promise; } public async getContentAsString(formatArg: 'utf8' | 'binary' = 'utf8') { const contentBuffer = await this.getContentAsBuffer(); return contentBuffer.toString(formatArg); } /** * Returns the size of the file content in bytes. */ public async getSize(): Promise { if (this.byteLengthComputeFunction) { return this.byteLengthComputeFunction(); } else { return null; } } /** * Converts the StreamFile to a SmartFile by loading content into memory */ public async toSmartFile(): Promise { const { SmartFile } = await import('./classes.smartfile.js'); const buffer = await this.getContentAsBuffer(); return new SmartFile({ path: this.relativeFilePath || 'stream', contentBuffer: buffer, base: process.cwd() }, this.smartFs); } }