Skip to content

Commit

Permalink
fs: add option parents to
Browse files Browse the repository at this point in the history
    - fs.open[Sync],
    - fs.writeFile[Sync],
    - fs.appendFile[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
);
```

fs: add new signature-form fs.open(path[, options], callback)
fs: add new signature-form fs.openSync(path[, options])

Fixes: nodejs#33559
  • Loading branch information
kaizhu256 committed Jan 12, 2021
1 parent 2af43f6 commit 45e7b64
Show file tree
Hide file tree
Showing 4 changed files with 596 additions and 13 deletions.
64 changes: 64 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,9 @@ try {
<!-- YAML
added: v0.6.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
- version: v10.0.0
pr-url: https://github.com/nodejs/node/pull/12562
description: The `callback` parameter is no longer optional. Not passing
Expand All @@ -1376,6 +1379,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'a'`.
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.
* `callback` {Function}
* `err` {Error}

Expand Down Expand Up @@ -1415,6 +1420,9 @@ fs.open('message.txt', 'a', (err, fd) => {
<!-- YAML
added: v0.6.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
- version: v7.0.0
pr-url: https://github.com/nodejs/node/pull/7831
description: The passed `options` object will never be modified.
Expand All @@ -1429,6 +1437,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'a'`.
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.

Synchronously append data to a file, creating the file if it does not yet
exist. `data` can be a string or a [`Buffer`][].
Expand Down Expand Up @@ -2828,6 +2838,28 @@ a colon, Node.js will open a file system stream, as described by
Functions based on `fs.open()` exhibit this behavior as well:
`fs.writeFile()`, `fs.readFile()`, etc.

## `fs.open(path[, options], callback)`
<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
-->

* `path` {string|Buffer|URL}
* `options` {Object|string}
* `flags` {string|number} See [support of file system `flags`][].
**Default:** `'r'`.
* `mode` {string|integer} **Default:** `0o666` (readable and writable)
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.
* `callback` {Function}
* `err` {Error}
* `fd` {integer}

Asynchronous file open using `options` signature-form.

## `fs.opendir(path[, options], callback)`
<!-- YAML
added: v12.12.0
Expand Down Expand Up @@ -2911,6 +2943,28 @@ Returns an integer representing the file descriptor.
For detailed information, see the documentation of the asynchronous version of
this API: [`fs.open()`][].

## `fs.openSync(path[, options])`
<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
-->

* `path` {string|Buffer|URL}
* `options` {Object|string}
* `flags` {string|number} See [support of file system `flags`][].
**Default:** `'r'`.
* `mode` {string|integer} **Default:** `0o666` (readable and writable)
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.
* `callback` {Function}
* `err` {Error}
* `fd` {integer}

Synchronous file open using `options` signature-form.

## `fs.read(fd, buffer, offset, length, position, callback)`
<!-- YAML
added: v0.0.2
Expand Down Expand Up @@ -4415,6 +4469,9 @@ details.
<!-- YAML
added: v0.1.29
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
- version: v15.2.0
pr-url: https://github.com/nodejs/node/pull/35993
description: The options argument may include an AbortSignal to abort an
Expand Down Expand Up @@ -4453,6 +4510,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.
* `signal` {AbortSignal} allows aborting an in-progress writeFile
* `callback` {Function}
* `err` {Error}
Expand Down Expand Up @@ -4537,6 +4596,9 @@ to contain only `', World'`.
<!-- YAML
added: v0.1.29
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `parents`.
- version: v14.12.0
pr-url: https://github.com/nodejs/node/pull/34993
description: The `data` parameter will stringify an object with an
Expand All @@ -4563,6 +4625,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
* `parents` {boolean} create parent-directories if they do not exist.
**Default:** `false`.

Returns `undefined`.

Expand Down
95 changes: 82 additions & 13 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const {
R_OK,
W_OK,
X_OK,
O_CREAT,
O_WRONLY,
O_SYMLINK
} = constants;
Expand Down Expand Up @@ -453,42 +454,110 @@ function closeSync(fd) {
handleErrorFromBinding(ctx);
}

// This function has two different signatures:
// fs.open(path[, flags[, mode]], callback)
// fs.open(path[, options], callback)
function open(path, flags, mode, callback) {
let options;
path = getValidatedPath(path);
if (arguments.length < 3) {
callback = flags;
flags = 'r';
mode = 0o666;
} else if (typeof mode === 'function') {
callback = mode;
mode = 0o666;
} else {
mode = parseFileMode(mode, 'mode', 0o666);
// Handle case where 2nd argument is options-bag
if (flags && typeof flags === 'object') {
options = flags;
}
}
const flagsNumber = stringToFlags(flags);
options = getOptions(options || {
flags,
mode
}, {
flags: 'r',
mode: 0o666,
parents: false
});
flags = stringToFlags(options.flags);
mode = parseFileMode(options.mode, 'mode', 0o666);
const parents = options.parents;
callback = makeCallback(callback);

const req = new FSReqCallback();
req.oncomplete = callback;
req.oncomplete = (err, fd) => {
// Lazily create parent-subdirectories if they do not exist
if (err?.code === 'ENOENT' && parents && (flags & O_CREAT)) {
mkdir(pathModule.dirname(path), { recursive: true }, (err) => {
// Ignore EEXIST race-condition from multiple, async mkdir()
if (err && err.code !== 'EEXIST') {
callback(err);
return;
}
// Retry open() after lazily creating parent-subdirectories
open(path, {
flags,
mode
}, callback);
});
return;
}
callback(err, fd);
};

binding.open(pathModule.toNamespacedPath(path),
flagsNumber,
flags,
mode,
req);
}


// This function has two different signatures:
// fs.openSync(path[, flags[, mode]])
// fs.openSync(path[, options])
function openSync(path, flags, mode) {
let options;
path = getValidatedPath(path);
const flagsNumber = stringToFlags(flags);
mode = parseFileMode(mode, 'mode', 0o666);
// Handle case where 2nd argument is options-bag
if (flags && typeof flags === 'object') {
options = flags;
}
options = getOptions(options || {
flags,
mode
}, {
flags: 'r',
mode: 0o666,
parents: false
});
flags = stringToFlags(options.flags);
mode = parseFileMode(options.mode, 'mode', 0o666);
const parents = options.parents;

const ctx = { path };
const result = binding.open(pathModule.toNamespacedPath(path),
flagsNumber, mode,
flags, mode,
undefined, ctx);
handleErrorFromBinding(ctx);
return result;
try {
handleErrorFromBinding(ctx);
return result;
} catch (err) {
// Lazily create parent-subdirectories if they do not exist
if (err?.code === 'ENOENT' && parents && (flags & O_CREAT)) {
try {
mkdirSync(pathModule.dirname(path), { recursive: true });
} catch (err2) {
// Ignore EEXIST race-condition from multiple, async mkdir()
if (err2.code !== 'EEXIST') {
throw err2;
}
}
// Retry openSync() after lazily creating parent-subdirectories
return openSync(path, {
flags,
mode
});
}
throw err;
}
}

// usage:
Expand Down
Loading

0 comments on commit 45e7b64

Please sign in to comment.