-
Notifications
You must be signed in to change notification settings - Fork 30.4k
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
feature-request - add [lazy] option parents
to fs.writeFile[Sync], fs.appendFile[Sync], fs.open[Sync]
#35775
base: main
Are you sure you want to change the base?
feature-request - add [lazy] option parents
to fs.writeFile[Sync], fs.appendFile[Sync], fs.open[Sync]
#35775
Conversation
Welcome @kaizhu256 and thanks for the pull request. I'm going to put this into Draft mode while it's being discussed. It will need tests and documentation before we can land it. |
@nodejs/fs |
mkdirRecursive
to fs.writeFile[Sync] and fs.appendFile[Sync]mkdirp
to fs.writeFile[Sync], fs.appendFile[Sync], and new function fs.openWithMkdirp[Sync]
e6e1324
to
b8d6132
Compare
@Trott thx for the guidance in my first attempt at a node-pr (regardless whether will cl or not : ) i decided to rename the option from overall the pr seems mostly complete in substance from my end, and now just awaiting feedback/review. |
doc/api/fs.md
Outdated
@@ -2884,6 +2894,35 @@ Returns an integer representing the file descriptor. | |||
For detailed information, see the documentation of the asynchronous version of | |||
this API: [`fs.open()`][]. | |||
|
|||
## `fs.openWithMkdirp(path[, flags[, mode]], callback)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's not an elegant way to add this functionality to open()
, without introducing this new method, I'd rather we start with adding the folder creation option to fs.write
and fs.append
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another option is to allow option-bag for 2nd argument: fs.open(path[, options[, mode]])
. but yea, being conservative with api is probably best for now. the main motivation is simplifying scripting-chores with fs.writeFile
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think an options bag would be consistent with the other APIs 👍
doc/api/fs.md
Outdated
@@ -1419,6 +1427,8 @@ changes: | |||
* `encoding` {string|null} **Default:** `'utf8'` | |||
* `mode` {integer} **Default:** `0o666` | |||
* `flag` {string} See [support of file system `flags`][]. **Default:** `'a'`. | |||
* `mkdirp` {boolean} "mkdir -p" directories in `path` if they do not exist. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we call this flag parents
(which is what the -p
of mkdir
) stands for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea. i didn't know it was shorthand for --parent until now.
mkdirp
to fs.writeFile[Sync], fs.appendFile[Sync], and new function fs.openWithMkdirp[Sync]parents
to fs.writeFile[Sync], fs.appendFile[Sync], fs.open[Sync]
a8ebcd3
to
7e0d6b4
Compare
in latest pr-commit:
|
- fs.open[Sync], - fs.writeFile[Sync], - fs.appendFile[Sync] fs: add new signature-form fs.open(path[, options], callback) fs: add new signature-form fs.openSync(path[, options]) fs: rename option `flags` to `flag` in fs.open for consistency with fs.writeFile[Sync] this feature is intended to improve ergonomics/simplify-scripting when: - creating build-artifacts/coverage-files during ci - scaffolding new web-projects - cloning website with web-crawler allowing user to lazily create ad-hoc directory-structures as need during file-creation with ergonomic syntax: ``` fs.writeFileSync( "foo/bar/baz/qux.txt", "hello world!", { parents: true } // will lazily create parent foo/bar/baz/ as needed ); ``` Fixes: nodejs#33559
16875e2
to
87f66f2
Compare
nudge to @nodejs/tooling @nodejs/fs for review. |
…...catch block - remove unnecessary EEXISTS error-check since node v12 - add extra promise test for fs.promises.open()
Can you please amend the description in the PR? The code uses |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work, +1.
doc/api/fs.md
Outdated
@@ -2805,7 +2815,7 @@ changes: | |||
--> | |||
|
|||
* `path` {string|Buffer|URL} | |||
* `flags` {string|number} See [support of file system `flags`][]. | |||
* `flag` {string|number} See [support of file system `flags`][]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this changed from flags to flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair, the FS API and documentation already uses those inconsistently (E.G.: flags
in fs.openSync
, flag
in fs.readFileSync
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that changing the documentation from flags
to flag
would break the existing hypertext links (from StackOverflow, blog posts, etc.), which is something we'd rather not do.
doc/api/fs.md
Outdated
@@ -2901,7 +2933,7 @@ changes: | |||
--> | |||
|
|||
* `path` {string|Buffer|URL} | |||
* `flags` {string|number} **Default:** `'r'`. | |||
* `flag` {string|number} **Default:** `'r'`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this changed from flags to flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this changed from flags to flag?
- i think its lesser evil to alternative -- internal monkey-patching/future-tech-debt converting
flag => flags
, when called byfs.writeFile[Sync] and fs.appendFile[Sync]
fs.writeFileSync("aa/bb/cc/dd.txt", {
// do we really want internal monkey-patching from
// `flag` => `flags` when passed to fs.openSync()?
flag: "w+",
parents: true
});
-
now is the time to change it without backward-compatibility issues.
-
flags
appears infs.createReadStream(path,{flags})
andfs.createWriteStream(path,{flags})
but those api's aren't as tightly coupled withfs.writeFile[Sync]
andfs.appendFile[Sync]
if ppl feel strongly about it, i can revert back to flags
and add monkey-patching to play nice with fs.writeFile[Sync]
and fs.appendFile[Sync]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be reverted from this PR as it's not related with the parents
option. Maybe you can open another PR focusing on this change? I agree the lack of consistency is kinda bad here, and if we decide to keep the inconsistency, we should at least discuss it thoroughly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
k, reverted documentation of fs.open[Sync]()
and fs.promises.open()
back to flags
in commit ca8deb8.
lib/fs.js
Outdated
mode = options.mode ?? 0o666; | ||
parents = options.parents; | ||
} | ||
flag = stringToFlags(flag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be flags
everywhere, not flag
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
k, added shim converting writeFile[Sync]:flag
to open[Sync]:flags
in commit 7b5b3e1
async function writeFile(path, data, options) {
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
- const flag = options.flag || 'w';
+ // Don't make changes directly on options object
+ options = copyObject(options);
+ // flag to flags shim:
+ // Resolve inconsistency between writeFile[Sync]:flag to open[Sync]:flags
+ options.flags = options.flag ?? 'w';
+ delete options.flag;
...
- const fd = await open(path, flag, options.mode);
+ const fd = await open(path, options);
lib/fs.js
Outdated
callback = makeCallback(callback); | ||
|
||
const req = new FSReqCallback(); | ||
req.oncomplete = callback; | ||
req.oncomplete = (err, fd) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If parents
is not set, please avoid creating this closure in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
k, added conditional-branch to avoid closure if possible in commit 480eaf7:
is below what was wanted?
diff --git a/lib/fs.js b/lib/fs.js
index e5ac3f6e01..28d3f81474 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -481,21 +481,25 @@ function open(path, flag, mode, callback) {
callback = makeCallback(callback);
const req = new FSReqCallback();
- req.oncomplete = (err, fd) => {
- // Lazily create parent-subdirectories if they do not exist
- if (err?.code === 'ENOENT' && parents && (flag & O_CREAT)) {
- mkdir(pathModule.dirname(path), { recursive: true }, (err2) => {
- if (err2) {
- callback(err2);
- return;
- }
- // Retry open() after lazily creating parent-subdirectories
- open(path, { flag, mode }, callback);
- });
- return;
- }
- callback(err, fd);
- };
+ if (parents) {
+ req.oncomplete = (err, fd) => {
+ // Lazily create parent-subdirectories if they do not exist
+ if (err?.code === 'ENOENT' && (flag & O_CREAT)) {
+ mkdir(pathModule.dirname(path), { recursive: true }, (err2) => {
+ if (err2) {
+ callback(err2);
+ return;
+ }
+ // Retry open() after lazily creating parent-subdirectories
+ open(path, { flag, mode }, callback);
+ });
+ return;
+ }
+ callback(err, fd);
+ };
+ } else {
+ req.oncomplete = callback;
+ }
binding.open(pathModule.toNamespacedPath(path),
flag,
Co-authored-by: Antoine du Hamel <[email protected]>
Co-authored-by: Antoine du Hamel <[email protected]>
Co-authored-by: Antoine du Hamel <[email protected]>
Co-authored-by: Antoine du Hamel <[email protected]>
is there anything I can do to help move this PR forward? It technically Closes #37733 and would generally be a massive DX improvement for those who do this exact operation often. |
@bnb @kaizhu256 I'm in support of this feature if the feedback around flags can be addressed, i.e., adding the @kaizhu256 anything we can do to help support this work? |
thx for encouragement. revisiting pr, adding a shim converting currently on break, and will get around to updating pr prolly next month. |
Resolve inconsistency between writeFile[Sync]:flag to open[Sync]:flags
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can get rid of the IIFEs here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add missing tests that fs.promises.[appendFile|writeFile]
will disables option mkdirp for flags missing O_CREAT.
Co-authored-by: Antoine du Hamel <[email protected]>
} | ||
const flagsNumber = stringToFlags(flags); | ||
// Handle case where 2nd argument is options-bag | ||
if (typeof flags === 'object' && flags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're only using it in an if statement to check whether flags is an object and you don't need the value of flags itself, then using typeof flags === 'object' is sufficient:
if (typeof flags === 'object' && flags) { | |
if (typeof flags === 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would crash if flags
was null
, as typeof null === "object"
path = getValidatedPath(path); | ||
const flagsNumber = stringToFlags(flags); | ||
// Handle case where 2nd argument is options-bag | ||
if (typeof flags === 'object' && flags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📓 similar to https://github.com/nodejs/node/pull/35775/files#r1380873544
if (typeof flags === 'object' && flags) { | |
if (typeof flags === 'object') { |
let options; | ||
let parents; | ||
// Handle case where 2nd argument is options-bag | ||
if (typeof flags === 'object' && flags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📓 similar to https://github.com/nodejs/node/pull/35775/files#r1380873544
if (typeof flags === 'object' && flags) { | |
if (typeof flags === 'object') { |
I'm sorry I've missed this. Is there a way to resurrect this PR? Seems useful. |
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesthis is proof-of-concept to implement feature-request #33559. if feedback is positive/viable, then i will proceed with adding documentation, tests, and other checklist items.
motivation
this feature is intended to improve ergonomics/simplify-scripting-tasks when:
in above tasks, user often has no deterministic-knowledge on directory-structure before file-creation.
this feature allows user to lazily create ad-hoc directory-structures as need during file-creation using ergonomic syntax:
performance impact and benchmark
the benchmark (in windows 10) comparing this pr-branch against master-branch shows no-performance-impact on fs.writeFile[Sync] or fs.appendFile[Sync] when option
{ parents: true }
is not used.when option
{ parents: true }
is enabled:at lazy-adhoc-directory-creation (vs eager-determistic-directory-creation)
at lazy-adhoc-directory-creation (vs eager-determistic-directory-creation)
windows benchmark result
the following results should be reproducible by following benchmark instructions at https://github.com/kaizhu256/node/tree/benchmark.fs.writeFile.mkdirRecursive#run-windows-benchmark
# bikeshed of namemkdirRecursive
when scripting the benchmark, i realized the namemkdirRecursive
is tedious to type. it also doesn't convey the lazy-nature of this operation, or the-p
attribute. am open to other naming suggestions (e.g.{ mkdirp: true }
)renamed to
parents
(from unix idiommkdir --parents
)