200 lines
6.4 KiB
TypeScript
200 lines
6.4 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import * as smartfileFs from './fs.js';
|
|
import * as smartfileFsStream from './fsstream.js';
|
|
import { Readable } from 'stream';
|
|
|
|
type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;
|
|
|
|
/**
|
|
* 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 {
|
|
// STATIC
|
|
|
|
public static async fromPath(filePath: string): Promise<StreamFile> {
|
|
const streamSource: TStreamSource = async (streamFileArg) =>
|
|
smartfileFsStream.createReadStream(filePath);
|
|
const streamFile = new StreamFile(streamSource, filePath);
|
|
streamFile.multiUse = true;
|
|
streamFile.byteLengthComputeFunction = async () => {
|
|
const stats = await smartfileFs.stat(filePath);
|
|
return stats.size;
|
|
};
|
|
return streamFile;
|
|
}
|
|
|
|
public static async fromUrl(url: string): Promise<StreamFile> {
|
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
|
const response = await plugins.smartrequest.SmartRequest.create()
|
|
.url(url)
|
|
.get();
|
|
return response.stream();
|
|
};
|
|
const streamFile = new StreamFile(streamSource);
|
|
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,
|
|
): 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;
|
|
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.
|
|
* @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;
|
|
}
|
|
|
|
// INSTANCE
|
|
relativeFilePath?: string;
|
|
private streamSource: TStreamSource;
|
|
|
|
// enable stream based multi use
|
|
private cachedStreamBuffer?: Buffer;
|
|
public multiUse: boolean;
|
|
public used: boolean = false;
|
|
public byteLengthComputeFunction: () => Promise<number>;
|
|
|
|
private constructor(streamSource: TStreamSource, relativeFilePath?: string) {
|
|
this.streamSource = streamSource;
|
|
this.relativeFilePath = relativeFilePath;
|
|
}
|
|
|
|
// 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<Readable> {
|
|
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<void> {
|
|
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<Buffer>();
|
|
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<number> {
|
|
if (this.byteLengthComputeFunction) {
|
|
return this.byteLengthComputeFunction();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|