Skip to content

Commit

Permalink
fs: add option mkdirp to fs.writeFile[Sync], fs.appendFile[Sync]
Browse files Browse the repository at this point in the history
... and new function fs.openWithMkdirp[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!",
    { mkdirp: true } // will "mkdir -p" foo/bar/baz as needed
);
```

Fixes: #33559
  • Loading branch information
kaizhu256 committed Nov 17, 2020
1 parent cbfa2d1 commit b8d6132
Show file tree
Hide file tree
Showing 3 changed files with 338 additions and 4 deletions.
49 changes: 49 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,9 @@ try {
<!-- YAML
added: v0.6.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `mkdirp`.
- 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 @@ -1366,6 +1369,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.
**Default:** `false`.
* `callback` {Function}
* `err` {Error}

Expand Down Expand Up @@ -1405,6 +1410,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 `mkdirp`.
- version: v7.0.0
pr-url: https://github.com/nodejs/node/pull/7831
description: The passed `options` object will never be modified.
Expand All @@ -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.
**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 @@ -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)`
<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `flags` {string|number} See [support of file system `flags`][].
**Default:** `'r'`.
* `mode` {string|integer} **Default:** `0o666` (readable and writable)
* `callback` {Function}
* `err` {Error}
* `fd` {integer}

Behaves like [`fs.open()`][], except will "mkdir -p" directories in `file`
if they do not exist

## `fs.openWithMkdirpSync(path[, flags, mode])`
<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `flags` {string|number} See [support of file system `flags`][].
**Default:** `'r'`.
* `mode` {string|integer} **Default:** `0o666` (readable and writable)

Behaves like `fs.openSync()`, except will "mkdir -p" directories in `file`
if they do not exist

## `fs.read(fd, buffer, offset, length, position, callback)`
<!-- YAML
added: v0.0.2
Expand Down Expand Up @@ -4385,6 +4424,9 @@ details.
<!-- YAML
added: v0.1.29
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35775
description: add option `mkdirp`.
- 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 @@ -4423,6 +4465,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
* `mkdirp` {boolean} "mkdir -p" directories in `file` if they do not exist.
**Default:** `false`.
* `signal` {AbortSignal} allows aborting an in-progress writeFile
* `callback` {Function}
* `err` {Error}
Expand Down Expand Up @@ -4507,6 +4551,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 `mkdirp`.
- 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 @@ -4533,6 +4580,8 @@ changes:
* `encoding` {string|null} **Default:** `'utf8'`
* `mode` {integer} **Default:** `0o666`
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
* `mkdirp` {boolean} "mkdir -p" directories in `file` if they do not exist.
**Default:** `false`.

Returns `undefined`.

Expand Down
93 changes: 89 additions & 4 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const {
R_OK,
W_OK,
X_OK,
O_CREAT,
O_WRONLY,
O_SYMLINK
} = constants;
Expand Down Expand Up @@ -488,6 +489,68 @@ function openSync(path, flags, mode) {
return result;
}

function openWithMkdirp(path, flags, mode, callback) {
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);
}
const flagsNumber = stringToFlags(flags);
callback = makeCallback(callback);
fs.open(path, flags, mode, (err, fd) => {
// lazy-mkdirp if directories in `path` do not exist
if (
(flagsNumber & O_CREAT) &&
err?.code === 'ENOENT'
) {
fs.mkdir(pathModule.dirname(path), { recursive: true }, (err) => {
// Ignore EEXIST race-condition from multiple, async fs.mkdir()
if (err && err.code !== 'EEXIST') {
callback(err);
return;
}
// Retry open() after lazy-mkdirp
fs.open(path, flags, mode, callback);
});
return;
}
callback(err, fd);
});
}

function openWithMkdirpSync(path, flags, mode) {
path = getValidatedPath(path);
const flagsNumber = stringToFlags(flags);
mode = parseFileMode(mode, 'mode', 0o666);
try {
return fs.openSync(path, flags, mode);
} catch (err) {
// lazy-mkdirp if directories in `path` do not exist
if (
(flagsNumber & O_CREAT) &&
err?.code === 'ENOENT'
) {
try {
fs.mkdirSync(pathModule.dirname(path), { recursive: true });
} catch (errCaught) {
// Ignore EEXIST race-condition from multiple, async fs.mkdir()
if (errCaught.code !== 'EEXIST') {
throw errCaught;
}
}
// Retry openSync() after lazy-mkdirp
return fs.openSync(path, flags, mode);
}
throw err;
}
}

// usage:
// fs.read(fd, buffer, offset, length, position, callback);
// OR
Expand Down Expand Up @@ -1448,7 +1511,12 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {

function writeFile(path, data, options, callback) {
callback = maybeCallback(callback || options);
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
options = getOptions(options, {
encoding: 'utf8',
mode: 0o666,
flag: 'w',
mkdirp: false
});
const flag = options.flag || 'w';

if (!isArrayBufferView(data)) {
Expand All @@ -1467,7 +1535,11 @@ function writeFile(path, data, options, callback) {
callback(lazyDOMException('The operation was aborted', 'AbortError'));
return;
}
fs.open(path, flag, options.mode, (openErr, fd) => {
(
options.mkdirp ?
fs.openWithMkdirp :
fs.open
)(path, flag, options.mode, (openErr, fd) => {
if (openErr) {
callback(openErr);
} else {
Expand All @@ -1479,7 +1551,12 @@ function writeFile(path, data, options, callback) {
}

function writeFileSync(path, data, options) {
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
options = getOptions(options, {
encoding: 'utf8',
mode: 0o666,
flag: 'w',
mkdirp: false
});

if (!isArrayBufferView(data)) {
validateStringAfterArrayBufferView(data, 'data');
Expand All @@ -1489,7 +1566,13 @@ function writeFileSync(path, data, options) {
const flag = options.flag || 'w';

const isUserFd = isFd(path); // File descriptor ownership
const fd = isUserFd ? path : fs.openSync(path, flag, options.mode);
const fd = (
isUserFd ?
path :
options.mkdirp ?
fs.openWithMkdirpSync(path, flag, options.mode) :
fs.openSync(path, flag, options.mode)
);

let offset = 0;
let length = data.byteLength;
Expand Down Expand Up @@ -2080,6 +2163,8 @@ module.exports = fs = {
mkdtempSync,
open,
openSync,
openWithMkdirp,
openWithMkdirpSync,
opendir,
opendirSync,
readdir,
Expand Down
Loading

0 comments on commit b8d6132

Please sign in to comment.