Skip to content

Commit 026bd82

Browse files
aduh95jasnell
authored andcommitted
fs: add stream utilities to FileHandle
PR-URL: #40009 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 17bb7b2 commit 026bd82

File tree

3 files changed

+186
-9
lines changed

3 files changed

+186
-9
lines changed

doc/api/fs.md

+98-9
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,99 @@ try {
230230
}
231231
```
232232
233+
#### `filehandle.createReadStream([options])`
234+
<!-- YAML
235+
added: REPLACEME
236+
-->
237+
238+
* `options` {Object}
239+
* `encoding` {string} **Default:** `null`
240+
* `autoClose` {boolean} **Default:** `true`
241+
* `emitClose` {boolean} **Default:** `true`
242+
* `start` {integer}
243+
* `end` {integer} **Default:** `Infinity`
244+
* `highWaterMark` {integer} **Default:** `64 * 1024`
245+
* Returns: {fs.ReadStream}
246+
247+
Unlike the 16 kb default `highWaterMark` for a {stream.Readable}, the stream
248+
returned by this method has a default `highWaterMark` of 64 kb.
249+
250+
`options` can include `start` and `end` values to read a range of bytes from
251+
the file instead of the entire file. Both `start` and `end` are inclusive and
252+
start counting at 0, allowed values are in the
253+
[0, [`Number.MAX_SAFE_INTEGER`][]] range. If `start` is
254+
omitted or `undefined`, `filehandle.createReadStream()` reads sequentially from
255+
the current file position. The `encoding` can be any one of those accepted by
256+
{Buffer}.
257+
258+
If the `FileHandle` points to a character device that only supports blocking
259+
reads (such as keyboard or sound card), read operations do not finish until data
260+
is available. This can prevent the process from exiting and the stream from
261+
closing naturally.
262+
263+
By default, the stream will emit a `'close'` event after it has been
264+
destroyed. Set the `emitClose` option to `false` to change this behavior.
265+
266+
```mjs
267+
import { open } from 'fs/promises';
268+
269+
const fd = await open('/dev/input/event0');
270+
// Create a stream from some character device.
271+
const stream = fd.createReadStream();
272+
setTimeout(() => {
273+
stream.close(); // This may not close the stream.
274+
// Artificially marking end-of-stream, as if the underlying resource had
275+
// indicated end-of-file by itself, allows the stream to close.
276+
// This does not cancel pending read operations, and if there is such an
277+
// operation, the process may still not be able to exit successfully
278+
// until it finishes.
279+
stream.push(null);
280+
stream.read(0);
281+
}, 100);
282+
```
283+
284+
If `autoClose` is false, then the file descriptor won't be closed, even if
285+
there's an error. It is the application's responsibility to close it and make
286+
sure there's no file descriptor leak. If `autoClose` is set to true (default
287+
behavior), on `'error'` or `'end'` the file descriptor will be closed
288+
automatically.
289+
290+
An example to read the last 10 bytes of a file which is 100 bytes long:
291+
292+
```mjs
293+
import { open } from 'fs/promises';
294+
295+
const fd = await open('sample.txt');
296+
fd.createReadStream({ start: 90, end: 99 });
297+
```
298+
299+
#### `filehandle.createWriteStream([options])`
300+
<!-- YAML
301+
added: REPLACEME
302+
-->
303+
304+
* `options` {Object}
305+
* `encoding` {string} **Default:** `'utf8'`
306+
* `autoClose` {boolean} **Default:** `true`
307+
* `emitClose` {boolean} **Default:** `true`
308+
* `start` {integer}
309+
* Returns: {fs.WriteStream}
310+
311+
`options` may also include a `start` option to allow writing data at some
312+
position past the beginning of the file, allowed values are in the
313+
[0, [`Number.MAX_SAFE_INTEGER`][]] range. Modifying a file rather than replacing
314+
it may require the `flags` `open` option to be set to `r+` rather than the
315+
default `r`. The `encoding` can be any one of those accepted by {Buffer}.
316+
317+
If `autoClose` is set to true (default behavior) on `'error'` or `'finish'`
318+
the file descriptor will be closed automatically. If `autoClose` is false,
319+
then the file descriptor won't be closed, even if there's an error.
320+
It is the application's responsibility to close it and make sure there's no
321+
file descriptor leak.
322+
323+
By default, the stream will emit a `'close'` event after it has been
324+
destroyed. Set the `emitClose` option to `false` to change this behavior.
325+
233326
#### `filehandle.datasync()`
234327
<!-- YAML
235328
added: v10.0.0
@@ -1985,9 +2078,9 @@ changes:
19852078
* `end` {integer} **Default:** `Infinity`
19862079
* `highWaterMark` {integer} **Default:** `64 * 1024`
19872080
* `fs` {Object|null} **Default:** `null`
1988-
* Returns: {fs.ReadStream} See [Readable Stream][].
2081+
* Returns: {fs.ReadStream}
19892082
1990-
Unlike the 16 kb default `highWaterMark` for a readable stream, the stream
2083+
Unlike the 16 kb default `highWaterMark` for a {stream.Readable}, the stream
19912084
returned by this method has a default `highWaterMark` of 64 kb.
19922085
19932086
`options` can include `start` and `end` values to read a range of bytes from
@@ -2009,8 +2102,7 @@ available. This can prevent the process from exiting and the stream from
20092102
closing naturally.
20102103
20112104
By default, the stream will emit a `'close'` event after it has been
2012-
destroyed, like most `Readable` streams. Set the `emitClose` option to
2013-
`false` to change this behavior.
2105+
destroyed. Set the `emitClose` option to `false` to change this behavior.
20142106
20152107
By providing the `fs` option, it is possible to override the corresponding `fs`
20162108
implementations for `open`, `read`, and `close`. When providing the `fs` option,
@@ -2106,7 +2198,7 @@ changes:
21062198
* `emitClose` {boolean} **Default:** `true`
21072199
* `start` {integer}
21082200
* `fs` {Object|null} **Default:** `null`
2109-
* Returns: {fs.WriteStream} See [Writable Stream][].
2201+
* Returns: {fs.WriteStream}
21102202
21112203
`options` may also include a `start` option to allow writing data at some
21122204
position past the beginning of the file, allowed values are in the
@@ -2121,8 +2213,7 @@ It is the application's responsibility to close it and make sure there's no
21212213
file descriptor leak.
21222214
21232215
By default, the stream will emit a `'close'` event after it has been
2124-
destroyed, like most `Writable` streams. Set the `emitClose` option to
2125-
`false` to change this behavior.
2216+
destroyed. Set the `emitClose` option to `false` to change this behavior.
21262217
21272218
By providing the `fs` option it is possible to override the corresponding `fs`
21282219
implementations for `open`, `write`, `writev` and `close`. Overriding `write()`
@@ -6921,8 +7012,6 @@ the file contents.
69217012
[MSDN-Rel-Path]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#fully-qualified-vs-relative-paths
69227013
[MSDN-Using-Streams]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams
69237014
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
6924-
[Readable Stream]: stream.md#class-streamreadable
6925-
[Writable Stream]: stream.md#class-streamwritable
69267015
[`AHAFS`]: https://developer.ibm.com/articles/au-aix_event_infrastructure/
69277016
[`Buffer.byteLength`]: buffer.md#static-method-bufferbytelengthstring-encoding
69287017
[`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events

lib/internal/fs/promises.js

+40
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ function lazyLoadCpPromises() {
115115
return cpPromises ??= require('internal/fs/cp/cp').cpFn;
116116
}
117117

118+
// Lazy loaded to avoid circular dependency.
119+
let fsStreams;
120+
function lazyFsStreams() {
121+
return fsStreams ??= require('internal/fs/streams');
122+
}
123+
118124
class FileHandle extends EventEmitterMixin(JSTransferable) {
119125
/**
120126
* @param {InternalFSBinding.FileHandle | undefined} filehandle
@@ -252,6 +258,40 @@ class FileHandle extends EventEmitterMixin(JSTransferable) {
252258
return readable;
253259
}
254260

261+
/**
262+
* @typedef {import('./streams').ReadStream
263+
* } ReadStream
264+
* @param {{
265+
* encoding?: string;
266+
* autoClose?: boolean;
267+
* emitClose?: boolean;
268+
* start: number;
269+
* end?: number;
270+
* highWaterMark?: number;
271+
* }} [options]
272+
* @returns {ReadStream}
273+
*/
274+
createReadStream(options = undefined) {
275+
const { ReadStream } = lazyFsStreams();
276+
return new ReadStream(undefined, { ...options, fd: this });
277+
}
278+
279+
/**
280+
* @typedef {import('./streams').WriteStream
281+
* } WriteStream
282+
* @param {{
283+
* encoding?: string;
284+
* autoClose?: boolean;
285+
* emitClose?: boolean;
286+
* start: number;
287+
* }} [options]
288+
* @returns {WriteStream}
289+
*/
290+
createWriteStream(options = undefined) {
291+
const { WriteStream } = lazyFsStreams();
292+
return new WriteStream(undefined, { ...options, fd: this });
293+
}
294+
255295
[kTransfer]() {
256296
if (this[kClosePromise] || this[kRefs] > 1) {
257297
throw lazyDOMException('Cannot transfer FileHandle while in use',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
// The following tests validate base functionality for the fs.promises
6+
// FileHandle.write method.
7+
8+
const fs = require('fs');
9+
const { open } = fs.promises;
10+
const path = require('path');
11+
const tmpdir = require('../common/tmpdir');
12+
const assert = require('assert');
13+
const { finished } = require('stream/promises');
14+
const { buffer } = require('stream/consumers');
15+
const tmpDir = tmpdir.path;
16+
17+
tmpdir.refresh();
18+
19+
async function validateWrite() {
20+
const filePathForHandle = path.resolve(tmpDir, 'tmp-write.txt');
21+
const fileHandle = await open(filePathForHandle, 'w');
22+
const buffer = Buffer.from('Hello world'.repeat(100), 'utf8');
23+
24+
const stream = fileHandle.createWriteStream();
25+
stream.end(buffer);
26+
await finished(stream);
27+
28+
const readFileData = fs.readFileSync(filePathForHandle);
29+
assert.deepStrictEqual(buffer, readFileData);
30+
}
31+
32+
async function validateRead() {
33+
const filePathForHandle = path.resolve(tmpDir, 'tmp-read.txt');
34+
const buf = Buffer.from('Hello world'.repeat(100), 'utf8');
35+
36+
fs.writeFileSync(filePathForHandle, buf);
37+
38+
const fileHandle = await open(filePathForHandle);
39+
assert.deepStrictEqual(
40+
await buffer(fileHandle.createReadStream()),
41+
buf
42+
);
43+
}
44+
45+
Promise.all([
46+
validateWrite(),
47+
validateRead(),
48+
]).then(common.mustCall());

0 commit comments

Comments
 (0)