import * as plugins from './smartfile.plugins.js'; import * as smartfileFs from './fs.js'; import * as smartfileFsStream from './fsstream.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. */ export class StreamFile { // INSTANCE relativeFilePath?: string; private streamSource: TStreamSource; // enable stream based multi use private cachedStreamBuffer?: Buffer; public multiUse: boolean; public used: boolean = false; private constructor(streamSource: TStreamSource, relativeFilePath?: string) { this.streamSource = streamSource; this.relativeFilePath = relativeFilePath; } // STATIC public static async fromPath(filePath: string): Promise { const streamSource: TStreamSource = async (stremFileArg) => smartfileFsStream.createReadStream(filePath); const streamFile = new StreamFile(streamSource, filePath); streamFile.multiUse = true; return streamFile; } public static async fromUrl(url: string): Promise { const streamSource: TStreamSource = async (streamFileArg) => plugins.smartrequest.getStream(url); // Replace with actual plugin method const streamFile = new StreamFile(streamSource); streamFile.multiUse = true; return streamFile; } public static fromBuffer(buffer: Buffer, relativeFilePath?: string): 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); streamFile.multiUse = true; 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. * @returns A StreamFile instance. */ public static fromStream(stream: Readable, relativeFilePath?: string, multiUse: boolean = false): 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); 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; } // 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 { return this.streamSource(this); } /** * 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 { this.checkMultiUse(); const readStream = await this.createReadStream(); const writeStream = smartfileFsStream.createWriteStream(filePathArg); 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) { this.checkMultiUse(); const filePath = plugins.path.join(dirPathArg, this.relativeFilePath); await smartfileFs.ensureDir(plugins.path.parse(filePath).dir); 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); } }