Skip to content

Commit 2b20b67

Browse files
Benjamin Coeaddaleax
authored andcommitted
fs: implement mkdir recursive (mkdirp)
Implements mkdirp functionality in node_file.cc. The Benefit of implementing in C++ layer is that the logic is more easily shared between the Promise and callback implementation and there are notable performance improvements. This commit is part of the Tooling Group Initiative. Refs: nodejs/user-feedback#70 PR-URL: #21875 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Jon Moss <[email protected]> Reviewed-By: Ron Korving <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Anatoli Papirovski <[email protected]> Reviewed-By: Sam Ruby <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent a448c8b commit 2b20b67

File tree

8 files changed

+441
-36
lines changed

8 files changed

+441
-36
lines changed

benchmark/fs/bench-mkdirp.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fs = require('fs');
5+
const tmpdir = require('../../test/common/tmpdir');
6+
tmpdir.refresh();
7+
let dirc = 0;
8+
9+
const bench = common.createBenchmark(main, {
10+
n: [1e4],
11+
});
12+
13+
function main({ n }) {
14+
bench.start();
15+
(function r(cntr) {
16+
if (cntr-- <= 0)
17+
return bench.end(n);
18+
const pathname = `${tmpdir.path}/${++dirc}/${++dirc}/${++dirc}/${++dirc}`;
19+
fs.mkdir(pathname, { createParents: true }, (err) => {
20+
r(cntr);
21+
});
22+
}(n));
23+
}

doc/api/fs.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,7 +2139,7 @@ changes:
21392139

21402140
Synchronous lstat(2).
21412141

2142-
## fs.mkdir(path[, mode], callback)
2142+
## fs.mkdir(path[, options], callback)
21432143
<!-- YAML
21442144
added: v0.1.8
21452145
changes:
@@ -2158,16 +2158,29 @@ changes:
21582158
-->
21592159

21602160
* `path` {string|Buffer|URL}
2161-
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
2161+
* `options` {Object|integer}
2162+
* `recursive` {boolean} **Default:** `false`
2163+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
21622164
* `callback` {Function}
21632165
* `err` {Error}
21642166

21652167
Asynchronously creates a directory. No arguments other than a possible exception
21662168
are given to the completion callback.
21672169

2170+
The optional `options` argument can be an integer specifying mode (permission
2171+
and sticky bits), or an object with a `mode` property and a `recursive`
2172+
property indicating whether parent folders should be created.
2173+
2174+
```js
2175+
// Creates /tmp/a/apple, regardless of whether `/tmp` and /tmp/a exist.
2176+
fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
2177+
if (err) throw err;
2178+
});
2179+
```
2180+
21682181
See also: mkdir(2).
21692182

2170-
## fs.mkdirSync(path[, mode])
2183+
## fs.mkdirSync(path[, options])
21712184
<!-- YAML
21722185
added: v0.1.21
21732186
changes:
@@ -2178,7 +2191,9 @@ changes:
21782191
-->
21792192

21802193
* `path` {string|Buffer|URL}
2181-
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
2194+
* `options` {Object|integer}
2195+
* `recursive` {boolean} **Default:** `false`
2196+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
21822197

21832198
Synchronously creates a directory. Returns `undefined`.
21842199
This is the synchronous version of [`fs.mkdir()`][].
@@ -4100,18 +4115,24 @@ changes:
41004115
Asynchronous lstat(2). The `Promise` is resolved with the [`fs.Stats`][] object
41014116
for the given symbolic link `path`.
41024117

4103-
### fsPromises.mkdir(path[, mode])
4118+
### fsPromises.mkdir(path[, options])
41044119
<!-- YAML
41054120
added: v10.0.0
41064121
-->
41074122

41084123
* `path` {string|Buffer|URL}
4109-
* `mode` {integer} **Default:** `0o777`
4124+
* `options` {Object|integer}
4125+
* `recursive` {boolean} **Default:** `false`
4126+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
41104127
* Returns: {Promise}
41114128

41124129
Asynchronously creates a directory then resolves the `Promise` with no
41134130
arguments upon success.
41144131

4132+
The optional `options` argument can be an integer specifying mode (permission
4133+
and sticky bits), or an object with a `mode` property and a `recursive`
4134+
property indicating whether parent folders should be created.
4135+
41154136
### fsPromises.mkdtemp(prefix[, options])
41164137
<!-- YAML
41174138
added: v10.0.0
@@ -4749,7 +4770,7 @@ the file contents.
47494770
[`fs.ftruncate()`]: #fs_fs_ftruncate_fd_len_callback
47504771
[`fs.futimes()`]: #fs_fs_futimes_fd_atime_mtime_callback
47514772
[`fs.lstat()`]: #fs_fs_lstat_path_options_callback
4752-
[`fs.mkdir()`]: #fs_fs_mkdir_path_mode_callback
4773+
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
47534774
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
47544775
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
47554776
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback

lib/fs.js

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -705,29 +705,48 @@ function fsyncSync(fd) {
705705
handleErrorFromBinding(ctx);
706706
}
707707

708-
function mkdir(path, mode, callback) {
708+
function mkdir(path, options, callback) {
709+
if (typeof options === 'function') {
710+
callback = options;
711+
options = {};
712+
} else if (typeof options === 'number' || typeof options === 'string') {
713+
options = { mode: options };
714+
}
715+
const {
716+
recursive = false,
717+
mode = 0o777
718+
} = options || {};
719+
callback = makeCallback(callback);
709720
path = getPathFromURL(path);
710-
validatePath(path);
711721

712-
if (arguments.length < 3) {
713-
callback = makeCallback(mode);
714-
mode = 0o777;
715-
} else {
716-
callback = makeCallback(callback);
717-
mode = validateMode(mode, 'mode', 0o777);
718-
}
722+
validatePath(path);
723+
if (typeof recursive !== 'boolean')
724+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
719725

720726
const req = new FSReqWrap();
721727
req.oncomplete = callback;
722-
binding.mkdir(pathModule.toNamespacedPath(path), mode, req);
728+
binding.mkdir(pathModule.toNamespacedPath(path),
729+
validateMode(mode, 'mode', 0o777), recursive, req);
723730
}
724731

725-
function mkdirSync(path, mode) {
732+
function mkdirSync(path, options) {
733+
if (typeof options === 'number' || typeof options === 'string') {
734+
options = { mode: options };
735+
}
726736
path = getPathFromURL(path);
737+
const {
738+
recursive = false,
739+
mode = 0o777
740+
} = options || {};
741+
727742
validatePath(path);
728-
mode = validateMode(mode, 'mode', 0o777);
743+
if (typeof recursive !== 'boolean')
744+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
745+
729746
const ctx = { path };
730-
binding.mkdir(pathModule.toNamespacedPath(path), mode, undefined, ctx);
747+
binding.mkdir(pathModule.toNamespacedPath(path),
748+
validateMode(mode, 'mode', 0o777), recursive, undefined,
749+
ctx);
731750
handleErrorFromBinding(ctx);
732751
}
733752

lib/internal/fs/promises.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,23 @@ async function fsync(handle) {
287287
return binding.fsync(handle.fd, kUsePromises);
288288
}
289289

290-
async function mkdir(path, mode) {
290+
async function mkdir(path, options) {
291+
if (typeof options === 'number' || typeof options === 'string') {
292+
options = { mode: options };
293+
}
294+
const {
295+
recursive = false,
296+
mode = 0o777
297+
} = options || {};
291298
path = getPathFromURL(path);
299+
292300
validatePath(path);
293-
mode = validateMode(mode, 'mode', 0o777);
294-
return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises);
301+
if (typeof recursive !== 'boolean')
302+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
303+
304+
return binding.mkdir(pathModule.toNamespacedPath(path),
305+
validateMode(mode, 'mode', 0o777), recursive,
306+
kUsePromises);
295307
}
296308

297309
async function readdir(path, options) {

src/node_file.cc

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ using v8::Value;
7676
# define MIN(a, b) ((a) < (b) ? (a) : (b))
7777
#endif
7878

79+
#ifndef S_ISDIR
80+
# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
81+
#endif
82+
83+
#ifdef __POSIX__
84+
const char* kPathSeparator = "/";
85+
#else
86+
const char* kPathSeparator = "\\/";
87+
#endif
88+
7989
#define GET_OFFSET(a) ((a)->IsNumber() ? (a).As<Integer>()->Value() : -1)
8090
#define TRACE_NAME(name) "fs.sync." #name
8191
#define GET_TRACE_ENABLED \
@@ -1236,28 +1246,162 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
12361246
}
12371247
}
12381248

1249+
int MKDirpSync(uv_loop_t* loop, uv_fs_t* req, const std::string& path, int mode,
1250+
uv_fs_cb cb = nullptr) {
1251+
FSContinuationData continuation_data(req, mode, cb);
1252+
continuation_data.PushPath(std::move(path));
1253+
1254+
while (continuation_data.paths.size() > 0) {
1255+
std::string next_path = continuation_data.PopPath();
1256+
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
1257+
while (true) {
1258+
switch (err) {
1259+
case 0:
1260+
if (continuation_data.paths.size() == 0) {
1261+
return 0;
1262+
}
1263+
break;
1264+
case UV_ENOENT: {
1265+
std::string dirname = next_path.substr(0,
1266+
next_path.find_last_of(kPathSeparator));
1267+
if (dirname != next_path) {
1268+
continuation_data.PushPath(std::move(next_path));
1269+
continuation_data.PushPath(std::move(dirname));
1270+
} else if (continuation_data.paths.size() == 0) {
1271+
err = UV_EEXIST;
1272+
continue;
1273+
}
1274+
break;
1275+
}
1276+
case UV_EPERM: {
1277+
return err;
1278+
}
1279+
default:
1280+
uv_fs_req_cleanup(req);
1281+
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
1282+
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) return UV_EEXIST;
1283+
if (err < 0) return err;
1284+
break;
1285+
}
1286+
break;
1287+
}
1288+
uv_fs_req_cleanup(req);
1289+
}
1290+
1291+
return 0;
1292+
}
1293+
1294+
int MKDirpAsync(uv_loop_t* loop,
1295+
uv_fs_t* req,
1296+
const char* path,
1297+
int mode,
1298+
uv_fs_cb cb) {
1299+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1300+
// on the first iteration of algorithm, stash state information.
1301+
if (req_wrap->continuation_data == nullptr) {
1302+
req_wrap->continuation_data = std::unique_ptr<FSContinuationData>{
1303+
new FSContinuationData(req, mode, cb)};
1304+
req_wrap->continuation_data->PushPath(std::move(path));
1305+
}
1306+
1307+
// on each iteration of algorithm, mkdir directory on top of stack.
1308+
std::string next_path = req_wrap->continuation_data->PopPath();
1309+
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode,
1310+
uv_fs_callback_t{[](uv_fs_t* req) {
1311+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1312+
Environment* env = req_wrap->env();
1313+
uv_loop_t* loop = env->event_loop();
1314+
std::string path = req->path;
1315+
int err = req->result;
1316+
1317+
while (true) {
1318+
switch (err) {
1319+
case 0: {
1320+
if (req_wrap->continuation_data->paths.size() == 0) {
1321+
req_wrap->continuation_data->Done(0);
1322+
} else {
1323+
uv_fs_req_cleanup(req);
1324+
MKDirpAsync(loop, req, path.c_str(),
1325+
req_wrap->continuation_data->mode, nullptr);
1326+
}
1327+
break;
1328+
}
1329+
case UV_ENOENT: {
1330+
std::string dirname = path.substr(0,
1331+
path.find_last_of(kPathSeparator));
1332+
if (dirname != path) {
1333+
req_wrap->continuation_data->PushPath(std::move(path));
1334+
req_wrap->continuation_data->PushPath(std::move(dirname));
1335+
} else if (req_wrap->continuation_data->paths.size() == 0) {
1336+
err = UV_EEXIST;
1337+
continue;
1338+
}
1339+
uv_fs_req_cleanup(req);
1340+
MKDirpAsync(loop, req, path.c_str(),
1341+
req_wrap->continuation_data->mode, nullptr);
1342+
break;
1343+
}
1344+
case UV_EPERM: {
1345+
req_wrap->continuation_data->Done(err);
1346+
break;
1347+
}
1348+
default:
1349+
if (err == UV_EEXIST &&
1350+
req_wrap->continuation_data->paths.size() > 0) {
1351+
uv_fs_req_cleanup(req);
1352+
MKDirpAsync(loop, req, path.c_str(),
1353+
req_wrap->continuation_data->mode, nullptr);
1354+
} else {
1355+
// verify that the path pointed to is actually a directory.
1356+
uv_fs_req_cleanup(req);
1357+
int err = uv_fs_stat(loop, req, path.c_str(),
1358+
uv_fs_callback_t{[](uv_fs_t* req) {
1359+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1360+
int err = req->result;
1361+
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) err = UV_EEXIST;
1362+
req_wrap->continuation_data->Done(err);
1363+
}});
1364+
if (err < 0) req_wrap->continuation_data->Done(err);
1365+
}
1366+
break;
1367+
}
1368+
break;
1369+
}
1370+
}});
1371+
1372+
return err;
1373+
}
1374+
12391375
static void MKDir(const FunctionCallbackInfo<Value>& args) {
12401376
Environment* env = Environment::GetCurrent(args);
12411377

12421378
const int argc = args.Length();
1243-
CHECK_GE(argc, 3);
1379+
CHECK_GE(argc, 4);
12441380

12451381
BufferValue path(env->isolate(), args[0]);
12461382
CHECK_NOT_NULL(*path);
12471383

12481384
CHECK(args[1]->IsInt32());
12491385
const int mode = args[1].As<Int32>()->Value();
12501386

1251-
FSReqBase* req_wrap_async = GetReqWrap(env, args[2]);
1387+
CHECK(args[2]->IsBoolean());
1388+
bool mkdirp = args[2]->IsTrue();
1389+
1390+
FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
12521391
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
1253-
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8, AfterNoArgs,
1254-
uv_fs_mkdir, *path, mode);
1392+
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
1393+
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
12551394
} else { // mkdir(path, mode, undefined, ctx)
1256-
CHECK_EQ(argc, 4);
1395+
CHECK_EQ(argc, 5);
12571396
FSReqWrapSync req_wrap_sync;
12581397
FS_SYNC_TRACE_BEGIN(mkdir);
1259-
SyncCall(env, args[3], &req_wrap_sync, "mkdir",
1260-
uv_fs_mkdir, *path, mode);
1398+
if (mkdirp) {
1399+
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1400+
MKDirpSync, *path, mode);
1401+
} else {
1402+
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1403+
uv_fs_mkdir, *path, mode);
1404+
}
12611405
FS_SYNC_TRACE_END(mkdir);
12621406
}
12631407
}

0 commit comments

Comments
 (0)