diff --git a/src/__tests__/promises.test.ts b/src/__tests__/promises.test.ts index 6a768b677..8259850ee 100644 --- a/src/__tests__/promises.test.ts +++ b/src/__tests__/promises.test.ts @@ -1,4 +1,5 @@ import { Volume } from '../volume'; +import { Readable } from 'stream'; describe('Promises API', () => { describe('FileHandle', () => { @@ -704,6 +705,22 @@ describe('Promises API', () => { expect(vol.readFileSync('/foo').toString()).toEqual('bar'); await fileHandle.close(); }); + it('Write data to an existing file using stream as source', async () => { + const vol = new Volume(); + const { promises } = vol; + vol.fromJSON({ + '/foo': '', + }); + const text = 'bar'; + const stream = new Readable({ + read() { + this.push(text); + this.push(null); + }, + }); + await promises.writeFile('/foo', stream); + expect(vol.readFileSync('/foo').toString()).toEqual(text); + }); it('Reject when trying to write on a directory', () => { const vol = new Volume(); const { promises } = vol; diff --git a/src/node/FsPromises.ts b/src/node/FsPromises.ts index 680d614a2..21a9551a6 100644 --- a/src/node/FsPromises.ts +++ b/src/node/FsPromises.ts @@ -1,4 +1,4 @@ -import { promisify } from './util'; +import { isReadableStream, promisify, streamToBuffer } from './util'; import { constants } from '../constants'; import type * as opts from './types/options'; import type * as misc from './types/misc'; @@ -63,13 +63,12 @@ export class FsPromises implements FsPromisesApi { public readonly writeFile = ( id: misc.TFileHandle, - data: misc.TData, + data: misc.TPromisesData, options?: opts.IWriteFileOptions, ): Promise => { - return promisify(this.fs, 'writeFile')( - id instanceof this.FileHandle ? id.fd : (id as misc.PathLike), - data, - options, + const dataPromise = isReadableStream(data) ? streamToBuffer(data) : Promise.resolve(data); + return dataPromise.then(data => + promisify(this.fs, 'writeFile')(id instanceof this.FileHandle ? id.fd : (id as misc.PathLike), data, options), ); }; diff --git a/src/node/types/FsPromisesApi.ts b/src/node/types/FsPromisesApi.ts index 22ea29df3..b8719f5bb 100644 --- a/src/node/types/FsPromisesApi.ts +++ b/src/node/types/FsPromisesApi.ts @@ -37,5 +37,5 @@ export interface FsPromisesApi { filename: misc.PathLike, options?: opts.IWatchOptions, ): AsyncIterableIterator<{ eventType: string; filename: string | Buffer }>; - writeFile(id: misc.TFileHandle, data: misc.TData, options?: opts.IWriteFileOptions): Promise; + writeFile(id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions): Promise; } diff --git a/src/node/types/misc.ts b/src/node/types/misc.ts index 4e82977a2..199202fa2 100644 --- a/src/node/types/misc.ts +++ b/src/node/types/misc.ts @@ -17,6 +17,7 @@ export type TDataOut = string | Buffer; // Data formats we give back to users. export type TEncodingExtended = BufferEncoding | 'buffer'; export type TFileId = PathLike | number; // Number is used as a file descriptor. export type TData = TDataOut | ArrayBufferView | DataView; // Data formats users can give us. +export type TPromisesData = TData | Readable; // Data formats users can give us in the promises API. export type TFlags = string | number; export type TMode = string | number; // Mode can be a String, although docs say it should be a Number. export type TTime = number | string | Date; diff --git a/src/node/util.ts b/src/node/util.ts index 26eae1157..09e6c682a 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -6,6 +6,7 @@ import type * as misc from './types/misc'; import { ENCODING_UTF8, TEncodingExtended } from '../encoding'; import { bufferFrom } from '../internal/buffer'; import queueMicrotask from '../queueMicrotask'; +import { Readable } from 'stream'; export const isWin = process.platform === 'win32'; @@ -178,6 +179,15 @@ export function validateFd(fd) { if (!isFd(fd)) throw TypeError(ERRSTR.FD); } +export function streamToBuffer(stream: Readable) { + const chunks: any[] = []; + return new Promise((resolve, reject) => { + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + stream.on('error', reject); + }); +} + export function dataToBuffer(data: misc.TData, encoding: string = ENCODING_UTF8): Buffer { if (Buffer.isBuffer(data)) return data; else if (data instanceof Uint8Array) return bufferFrom(data); @@ -289,6 +299,16 @@ export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended): else return buffer.toString(encoding); } +export function isReadableStream(stream): stream is Readable { + return ( + stream !== null && + typeof stream === 'object' && + typeof stream.pipe === 'function' && + typeof stream.on === 'function' && + stream.readable === true + ); +} + const isSeparator = (str, i) => { let char = str[i]; return i > 0 && (char === '/' || (isWin && char === '\\'));