Skip to content

Commit

Permalink
feat: support stream as source in promises version of writeFile (#1069
Browse files Browse the repository at this point in the history
)
  • Loading branch information
SoulKa authored Oct 13, 2024
1 parent 5460fce commit 11f8a36
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 7 deletions.
17 changes: 17 additions & 0 deletions src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Volume } from '../volume';
import { Readable } from 'stream';

describe('Promises API', () => {
describe('FileHandle', () => {
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 5 additions & 6 deletions src/node/FsPromises.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<void> => {
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),
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/node/types/FsPromisesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
writeFile(id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions): Promise<void>;
}
1 change: 1 addition & 0 deletions src/node/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<Buffer>((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);
Expand Down Expand Up @@ -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 === '\\'));
Expand Down

0 comments on commit 11f8a36

Please sign in to comment.