fs: behaviour of readFile and writeFile with file descriptors #23433
Description
- Version: v11.0.0-pre
- Platform: MacOS, Ubuntu (I have tried in these two operating systems personally)
- Subsystem: fs
This problem has been discussed multiple times without reaching any conclusion. That is why opening a separate issue to discuss.
Documented Expectations
As per our docs
readFile
is defined as
Asynchronously reads the entire contents of a file.
and writeFile
is defined as
Asynchronously writes data to a file, replacing the file if it already exists.
Current State of readFile
& writeFile
:
When a regular file path is passed:
The file is either read from the beginning or completely overwritten. This complies with the documentation.
When a file descriptor is passed:
readFile
The file is read from the current position in the file (the current position is maintained by the file descriptor itself).
readFile problem reproduction
'use strict';const fs = require('fs');
const assert = require('assert');
const buffer = Buffer.alloc(5);/* Open the file descriptor. */
const fd = fs.openSync(__filename, 'r');/* Read only the first five characters. */
assert.deepStrictEqual(fs.readSync(fd, buffer, 0, 5), 5);
assert.deepStrictEqual(buffer.toString('UTF-8'), '\'use ');/* As per the docs, both of these should be the same. */
assert.deepStrictEqual(
fs.readFileSync(fd).toString(),
fs.readFileSync(__filename).toString()
);
writeFile
Writes from the beginning of the file, without replacing the contents of the file. For example, if the existing content in the file isABCD
and the newly written data is12
, then the actual contents of the file after both thewriteFile
s is12CD
.
writeFile problem reproduction
'use strict';const fs = require('fs');
const assert = require('assert');/* Write only five characters. */
fs.writeFileSync('/tmp/test1', 'Hello');
assert.deepStrictEqual(fs.readFileSync('/tmp/test1').toString(), 'Hello');/* Write something else. */
fs.writeFileSync('/tmp/test1', 'Sun');
assert.deepStrictEqual(fs.readFileSync('/tmp/test1').toString(), 'Sun');/* Open the file descriptor. */
const fd = fs.openSync('/tmp/test', 'w');/* Write only five characters. */
assert.deepStrictEqual(fs.writeSync(fd, 'Hello'), 5);
assert.deepStrictEqual(fs.readFileSync('/tmp/test').toString(), 'Hello');/* Write something else. */
fs.writeFileSync(fd, 'Sun');
assert.deepStrictEqual(fs.readFileSync('/tmp/test').toString(), 'Sun');
These two behaviours deviate from the documented expectations.
Cases for changing the behaviour to work from the beginning
#9671 (It presents the case with readFile
)
Cases for keeping the current behaviour
Its intuitive for people to expect read/write from the current position till the end of the file. (For example, read a header from a file and then decide whether to read the rest of the file or not.)
Proposed courses of action
- Land fs: make writeFile consistent with readFile wrt fd #23709, which makes
writeFile
also to write from the current file position just likereadFile
reads from the current file position, and be done with it. (semver-major) - Make the
readFile
andwriteFile
to work from the beginning of the file. (semver-major)
a. If the file descriptor is not seekable, an error will be thrown from libuv. - Variant of 2, if the file descriptor doesn't correspond to a regular file (basically a non-seekable file descriptor), throw an error. Otherwise, try to work from the beginning of the file. (semver-major)
- Remove fd support, its just a helper function for a not so oftenly used use-case, and there are valid reasons to support both seeking and non-seeking, so whatever we choose someone will need to write their own wrapper to make functions that work as they expect. Let there be an npm package to do this, or several. (semver-major)
- Just leave the current behaviour as it is with the doc changes in doc: clarify fd behaviour with {read,write}File #23706.
This is not an exhaustive list. Please feel free to propose more ideas.
PS: This comment by @sam-github summarises my personal expectations from the API.
Name | Option 1 | Option 2 | Option 3 | Option 4 | Option 5 |
---|---|---|---|---|---|
@thefourtheye | X |
X |
|||
@mcollina | X |
X |
|||
@addaleax | X |
X |
|||
@TimothyGu | X |
||||
@mhdawson | X |
X |
|||
@Trott | |||||
@ChALkeR | X + warn * |
X |
|||
@rvagg | X |
||||
@cjihrig | X |
X |
|||
@joyeecheung | X |
||||
@apapirovski | X |
||||
@Fishrock123 | X |
||||
Total | 9 |
1 |
1 |
2 |
4 |
cc @nodejs/fs
Tagging @nodejs/tsc (Might be a bit early, but this has been discussed before. So tagging to get more inputs)
Tagging @seishun @vsemozhetbyt @sam-github @jorangreef @addaleax from the previous discussions.
If needed we can tag the collaborators to get their expectations.
Previous discussions on this topic happened in #9671, #10809, #10853, #13572,
#22554