Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support stream as source in promises version of writeFile #1069

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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