Skip to content

Commit 11f8a36

Browse files
authored
feat: support stream as source in promises version of writeFile (#1069)
1 parent 5460fce commit 11f8a36

File tree

5 files changed

+44
-7
lines changed

5 files changed

+44
-7
lines changed

src/__tests__/promises.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Volume } from '../volume';
2+
import { Readable } from 'stream';
23

34
describe('Promises API', () => {
45
describe('FileHandle', () => {
@@ -704,6 +705,22 @@ describe('Promises API', () => {
704705
expect(vol.readFileSync('/foo').toString()).toEqual('bar');
705706
await fileHandle.close();
706707
});
708+
it('Write data to an existing file using stream as source', async () => {
709+
const vol = new Volume();
710+
const { promises } = vol;
711+
vol.fromJSON({
712+
'/foo': '',
713+
});
714+
const text = 'bar';
715+
const stream = new Readable({
716+
read() {
717+
this.push(text);
718+
this.push(null);
719+
},
720+
});
721+
await promises.writeFile('/foo', stream);
722+
expect(vol.readFileSync('/foo').toString()).toEqual(text);
723+
});
707724
it('Reject when trying to write on a directory', () => {
708725
const vol = new Volume();
709726
const { promises } = vol;

src/node/FsPromises.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { promisify } from './util';
1+
import { isReadableStream, promisify, streamToBuffer } from './util';
22
import { constants } from '../constants';
33
import type * as opts from './types/options';
44
import type * as misc from './types/misc';
@@ -63,13 +63,12 @@ export class FsPromises implements FsPromisesApi {
6363

6464
public readonly writeFile = (
6565
id: misc.TFileHandle,
66-
data: misc.TData,
66+
data: misc.TPromisesData,
6767
options?: opts.IWriteFileOptions,
6868
): Promise<void> => {
69-
return promisify(this.fs, 'writeFile')(
70-
id instanceof this.FileHandle ? id.fd : (id as misc.PathLike),
71-
data,
72-
options,
69+
const dataPromise = isReadableStream(data) ? streamToBuffer(data) : Promise.resolve(data);
70+
return dataPromise.then(data =>
71+
promisify(this.fs, 'writeFile')(id instanceof this.FileHandle ? id.fd : (id as misc.PathLike), data, options),
7372
);
7473
};
7574

src/node/types/FsPromisesApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ export interface FsPromisesApi {
3737
filename: misc.PathLike,
3838
options?: opts.IWatchOptions,
3939
): AsyncIterableIterator<{ eventType: string; filename: string | Buffer }>;
40-
writeFile(id: misc.TFileHandle, data: misc.TData, options?: opts.IWriteFileOptions): Promise<void>;
40+
writeFile(id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions): Promise<void>;
4141
}

src/node/types/misc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type TDataOut = string | Buffer; // Data formats we give back to users.
1717
export type TEncodingExtended = BufferEncoding | 'buffer';
1818
export type TFileId = PathLike | number; // Number is used as a file descriptor.
1919
export type TData = TDataOut | ArrayBufferView | DataView; // Data formats users can give us.
20+
export type TPromisesData = TData | Readable; // Data formats users can give us in the promises API.
2021
export type TFlags = string | number;
2122
export type TMode = string | number; // Mode can be a String, although docs say it should be a Number.
2223
export type TTime = number | string | Date;

src/node/util.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type * as misc from './types/misc';
66
import { ENCODING_UTF8, TEncodingExtended } from '../encoding';
77
import { bufferFrom } from '../internal/buffer';
88
import queueMicrotask from '../queueMicrotask';
9+
import { Readable } from 'stream';
910

1011
export const isWin = process.platform === 'win32';
1112

@@ -178,6 +179,15 @@ export function validateFd(fd) {
178179
if (!isFd(fd)) throw TypeError(ERRSTR.FD);
179180
}
180181

182+
export function streamToBuffer(stream: Readable) {
183+
const chunks: any[] = [];
184+
return new Promise<Buffer>((resolve, reject) => {
185+
stream.on('data', chunk => chunks.push(chunk));
186+
stream.on('end', () => resolve(Buffer.concat(chunks)));
187+
stream.on('error', reject);
188+
});
189+
}
190+
181191
export function dataToBuffer(data: misc.TData, encoding: string = ENCODING_UTF8): Buffer {
182192
if (Buffer.isBuffer(data)) return data;
183193
else if (data instanceof Uint8Array) return bufferFrom(data);
@@ -289,6 +299,16 @@ export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended):
289299
else return buffer.toString(encoding);
290300
}
291301

302+
export function isReadableStream(stream): stream is Readable {
303+
return (
304+
stream !== null &&
305+
typeof stream === 'object' &&
306+
typeof stream.pipe === 'function' &&
307+
typeof stream.on === 'function' &&
308+
stream.readable === true
309+
);
310+
}
311+
292312
const isSeparator = (str, i) => {
293313
let char = str[i];
294314
return i > 0 && (char === '/' || (isWin && char === '\\'));

0 commit comments

Comments
 (0)