Skip to content

Commit e9d7a26

Browse files
aduh95danielleadams
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 9af2592 commit e9d7a26

File tree

3 files changed

+213
-9
lines changed

3 files changed

+213
-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
@@ -1937,9 +2030,9 @@ changes:
19372030
* `end` {integer} **Default:** `Infinity`
19382031
* `highWaterMark` {integer} **Default:** `64 * 1024`
19392032
* `fs` {Object|null} **Default:** `null`
1940-
* Returns: {fs.ReadStream} See [Readable Stream][].
2033+
* Returns: {fs.ReadStream}
19412034
1942-
Unlike the 16 kb default `highWaterMark` for a readable stream, the stream
2035+
Unlike the 16 kb default `highWaterMark` for a {stream.Readable}, the stream
19432036
returned by this method has a default `highWaterMark` of 64 kb.
19442037
19452038
`options` can include `start` and `end` values to read a range of bytes from
@@ -1961,8 +2054,7 @@ available. This can prevent the process from exiting and the stream from
19612054
closing naturally.
19622055
19632056
By default, the stream will emit a `'close'` event after it has been
1964-
destroyed, like most `Readable` streams. Set the `emitClose` option to
1965-
`false` to change this behavior.
2057+
destroyed. Set the `emitClose` option to `false` to change this behavior.
19662058
19672059
By providing the `fs` option, it is possible to override the corresponding `fs`
19682060
implementations for `open`, `read`, and `close`. When providing the `fs` option,
@@ -2058,7 +2150,7 @@ changes:
20582150
* `emitClose` {boolean} **Default:** `true`
20592151
* `start` {integer}
20602152
* `fs` {Object|null} **Default:** `null`
2061-
* Returns: {fs.WriteStream} See [Writable Stream][].
2153+
* Returns: {fs.WriteStream}
20622154
20632155
`options` may also include a `start` option to allow writing data at some
20642156
position past the beginning of the file, allowed values are in the
@@ -2073,8 +2165,7 @@ It is the application's responsibility to close it and make sure there's no
20732165
file descriptor leak.
20742166
20752167
By default, the stream will emit a `'close'` event after it has been
2076-
destroyed, like most `Writable` streams. Set the `emitClose` option to
2077-
`false` to change this behavior.
2168+
destroyed. Set the `emitClose` option to `false` to change this behavior.
20782169
20792170
By providing the `fs` option it is possible to override the corresponding `fs`
20802171
implementations for `open`, `write`, `writev` and `close`. Overriding `write()`
@@ -6867,8 +6958,6 @@ the file contents.
68676958
[MSDN-Rel-Path]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#fully-qualified-vs-relative-paths
68686959
[MSDN-Using-Streams]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams
68696960
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
6870-
[Readable Stream]: stream.md#stream_class_stream_readable
6871-
[Writable Stream]: stream.md#stream_class_stream_writable
68726961
[`AHAFS`]: https://developer.ibm.com/articles/au-aix_event_infrastructure/
68736962
[`Buffer.byteLength`]: buffer.md#buffer_static_method_buffer_bytelength_string_encoding
68746963
[`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events

lib/internal/fs/promises.js

+67
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ function lazyLoadCpPromises() {
103103
return cpPromises ??= require('internal/fs/cp/cp').cpFn;
104104
}
105105

106+
// Lazy loaded to avoid circular dependency.
107+
let fsStreams;
108+
function lazyFsStreams() {
109+
return fsStreams ??= require('internal/fs/streams');
110+
}
111+
106112
class FileHandle extends EventEmitterMixin(JSTransferable) {
107113
/**
108114
* @param {InternalFSBinding.FileHandle | undefined} filehandle
@@ -213,6 +219,67 @@ class FileHandle extends EventEmitterMixin(JSTransferable) {
213219
return this[kClosePromise];
214220
}
215221

222+
/**
223+
* @typedef {import('../webstreams/readablestream').ReadableStream
224+
* } ReadableStream
225+
* @returns {ReadableStream}
226+
*/
227+
readableWebStream() {
228+
if (this[kFd] === -1)
229+
throw new ERR_INVALID_STATE('The FileHandle is closed');
230+
if (this[kClosePromise])
231+
throw new ERR_INVALID_STATE('The FileHandle is closing');
232+
if (this[kLocked])
233+
throw new ERR_INVALID_STATE('The FileHandle is locked');
234+
this[kLocked] = true;
235+
236+
const readable = newReadableStreamFromStreamBase(
237+
this[kHandle],
238+
undefined,
239+
{ ondone: () => this[kUnref]() });
240+
241+
this[kRef]();
242+
this.once('close', () => {
243+
readableStreamCancel(readable);
244+
});
245+
246+
return readable;
247+
}
248+
249+
/**
250+
* @typedef {import('./streams').ReadStream
251+
* } ReadStream
252+
* @param {{
253+
* encoding?: string;
254+
* autoClose?: boolean;
255+
* emitClose?: boolean;
256+
* start: number;
257+
* end?: number;
258+
* highWaterMark?: number;
259+
* }} [options]
260+
* @returns {ReadStream}
261+
*/
262+
createReadStream(options = undefined) {
263+
const { ReadStream } = lazyFsStreams();
264+
return new ReadStream(undefined, { ...options, fd: this });
265+
}
266+
267+
/**
268+
* @typedef {import('./streams').WriteStream
269+
* } WriteStream
270+
* @param {{
271+
* encoding?: string;
272+
* autoClose?: boolean;
273+
* emitClose?: boolean;
274+
* start: number;
275+
* }} [options]
276+
* @returns {WriteStream}
277+
*/
278+
createWriteStream(options = undefined) {
279+
const { WriteStream } = lazyFsStreams();
280+
return new WriteStream(undefined, { ...options, fd: this });
281+
}
282+
216283
[kTransfer]() {
217284
if (this[kClosePromise] || this[kRefs] > 1) {
218285
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)