Skip to content

Commit bdef1b1

Browse files
author
Benjamin Coe
committed
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 <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Jon Moss <me@jonathanmoss.me> Reviewed-By: Ron Korving <ron@ronkorving.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com> Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Reviewed-By: Sam Ruby <rubys@intertwingly.net> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent e039524 commit bdef1b1

File tree

8 files changed

+441
-36
lines changed

8 files changed

+441
-36
lines changed

benchmark/fs/bench-mkdirp.js

+23
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

+28-7
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,7 @@ changes:
20472047

20482048
Synchronous lstat(2).
20492049

2050-
## fs.mkdir(path[, mode], callback)
2050+
## fs.mkdir(path[, options], callback)
20512051
<!-- YAML
20522052
added: v0.1.8
20532053
changes:
@@ -2066,16 +2066,29 @@ changes:
20662066
-->
20672067

20682068
* `path` {string|Buffer|URL}
2069-
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
2069+
* `options` {Object|integer}
2070+
* `recursive` {boolean} **Default:** `false`
2071+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
20702072
* `callback` {Function}
20712073
* `err` {Error}
20722074

20732075
Asynchronously creates a directory. No arguments other than a possible exception
20742076
are given to the completion callback.
20752077

2078+
The optional `options` argument can be an integer specifying mode (permission
2079+
and sticky bits), or an object with a `mode` property and a `recursive`
2080+
property indicating whether parent folders should be created.
2081+
2082+
```js
2083+
// Creates /tmp/a/apple, regardless of whether `/tmp` and /tmp/a exist.
2084+
fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
2085+
if (err) throw err;
2086+
});
2087+
```
2088+
20762089
See also: mkdir(2).
20772090

2078-
## fs.mkdirSync(path[, mode])
2091+
## fs.mkdirSync(path[, options])
20792092
<!-- YAML
20802093
added: v0.1.21
20812094
changes:
@@ -2086,7 +2099,9 @@ changes:
20862099
-->
20872100

20882101
* `path` {string|Buffer|URL}
2089-
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
2102+
* `options` {Object|integer}
2103+
* `recursive` {boolean} **Default:** `false`
2104+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
20902105

20912106
Synchronously creates a directory. Returns `undefined`.
20922107
This is the synchronous version of [`fs.mkdir()`][].
@@ -3979,18 +3994,24 @@ changes:
39793994
Asynchronous lstat(2). The `Promise` is resolved with the [`fs.Stats`][] object
39803995
for the given symbolic link `path`.
39813996

3982-
### fsPromises.mkdir(path[, mode])
3997+
### fsPromises.mkdir(path[, options])
39833998
<!-- YAML
39843999
added: v10.0.0
39854000
-->
39864001

39874002
* `path` {string|Buffer|URL}
3988-
* `mode` {integer} **Default:** `0o777`
4003+
* `options` {Object|integer}
4004+
* `recursive` {boolean} **Default:** `false`
4005+
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
39894006
* Returns: {Promise}
39904007

39914008
Asynchronously creates a directory then resolves the `Promise` with no
39924009
arguments upon success.
39934010

4011+
The optional `options` argument can be an integer specifying mode (permission
4012+
and sticky bits), or an object with a `mode` property and a `recursive`
4013+
property indicating whether parent folders should be created.
4014+
39944015
### fsPromises.mkdtemp(prefix[, options])
39954016
<!-- YAML
39964017
added: v10.0.0
@@ -4627,7 +4648,7 @@ the file contents.
46274648
[`fs.ftruncate()`]: #fs_fs_ftruncate_fd_len_callback
46284649
[`fs.futimes()`]: #fs_fs_futimes_fd_atime_mtime_callback
46294650
[`fs.lstat()`]: #fs_fs_lstat_path_options_callback
4630-
[`fs.mkdir()`]: #fs_fs_mkdir_path_mode_callback
4651+
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
46314652
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
46324653
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
46334654
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback

lib/fs.js

+32-13
Original file line numberDiff line numberDiff line change
@@ -721,29 +721,48 @@ function fsyncSync(fd) {
721721
handleErrorFromBinding(ctx);
722722
}
723723

724-
function mkdir(path, mode, callback) {
724+
function mkdir(path, options, callback) {
725+
if (typeof options === 'function') {
726+
callback = options;
727+
options = {};
728+
} else if (typeof options === 'number' || typeof options === 'string') {
729+
options = { mode: options };
730+
}
731+
const {
732+
recursive = false,
733+
mode = 0o777
734+
} = options || {};
735+
callback = makeCallback(callback);
725736
path = getPathFromURL(path);
726-
validatePath(path);
727737

728-
if (arguments.length < 3) {
729-
callback = makeCallback(mode);
730-
mode = 0o777;
731-
} else {
732-
callback = makeCallback(callback);
733-
mode = validateMode(mode, 'mode', 0o777);
734-
}
738+
validatePath(path);
739+
if (typeof recursive !== 'boolean')
740+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
735741

736742
const req = new FSReqCallback();
737743
req.oncomplete = callback;
738-
binding.mkdir(pathModule.toNamespacedPath(path), mode, req);
744+
binding.mkdir(pathModule.toNamespacedPath(path),
745+
validateMode(mode, 'mode', 0o777), recursive, req);
739746
}
740747

741-
function mkdirSync(path, mode) {
748+
function mkdirSync(path, options) {
749+
if (typeof options === 'number' || typeof options === 'string') {
750+
options = { mode: options };
751+
}
742752
path = getPathFromURL(path);
753+
const {
754+
recursive = false,
755+
mode = 0o777
756+
} = options || {};
757+
743758
validatePath(path);
744-
mode = validateMode(mode, 'mode', 0o777);
759+
if (typeof recursive !== 'boolean')
760+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
761+
745762
const ctx = { path };
746-
binding.mkdir(pathModule.toNamespacedPath(path), mode, undefined, ctx);
763+
binding.mkdir(pathModule.toNamespacedPath(path),
764+
validateMode(mode, 'mode', 0o777), recursive, undefined,
765+
ctx);
747766
handleErrorFromBinding(ctx);
748767
}
749768

lib/internal/fs/promises.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,23 @@ async function fsync(handle) {
289289
return binding.fsync(handle.fd, kUsePromises);
290290
}
291291

292-
async function mkdir(path, mode) {
292+
async function mkdir(path, options) {
293+
if (typeof options === 'number' || typeof options === 'string') {
294+
options = { mode: options };
295+
}
296+
const {
297+
recursive = false,
298+
mode = 0o777
299+
} = options || {};
293300
path = getPathFromURL(path);
301+
294302
validatePath(path);
295-
mode = validateMode(mode, 'mode', 0o777);
296-
return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises);
303+
if (typeof recursive !== 'boolean')
304+
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
305+
306+
return binding.mkdir(pathModule.toNamespacedPath(path),
307+
validateMode(mode, 'mode', 0o777), recursive,
308+
kUsePromises);
297309
}
298310

299311
async function readdir(path, options) {

src/node_file.cc

+151-7
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 \
@@ -1148,28 +1158,162 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
11481158
}
11491159
}
11501160

1161+
int MKDirpSync(uv_loop_t* loop, uv_fs_t* req, const std::string& path, int mode,
1162+
uv_fs_cb cb = nullptr) {
1163+
FSContinuationData continuation_data(req, mode, cb);
1164+
continuation_data.PushPath(std::move(path));
1165+
1166+
while (continuation_data.paths.size() > 0) {
1167+
std::string next_path = continuation_data.PopPath();
1168+
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
1169+
while (true) {
1170+
switch (err) {
1171+
case 0:
1172+
if (continuation_data.paths.size() == 0) {
1173+
return 0;
1174+
}
1175+
break;
1176+
case UV_ENOENT: {
1177+
std::string dirname = next_path.substr(0,
1178+
next_path.find_last_of(kPathSeparator));
1179+
if (dirname != next_path) {
1180+
continuation_data.PushPath(std::move(next_path));
1181+
continuation_data.PushPath(std::move(dirname));
1182+
} else if (continuation_data.paths.size() == 0) {
1183+
err = UV_EEXIST;
1184+
continue;
1185+
}
1186+
break;
1187+
}
1188+
case UV_EPERM: {
1189+
return err;
1190+
}
1191+
default:
1192+
uv_fs_req_cleanup(req);
1193+
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
1194+
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) return UV_EEXIST;
1195+
if (err < 0) return err;
1196+
break;
1197+
}
1198+
break;
1199+
}
1200+
uv_fs_req_cleanup(req);
1201+
}
1202+
1203+
return 0;
1204+
}
1205+
1206+
int MKDirpAsync(uv_loop_t* loop,
1207+
uv_fs_t* req,
1208+
const char* path,
1209+
int mode,
1210+
uv_fs_cb cb) {
1211+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1212+
// on the first iteration of algorithm, stash state information.
1213+
if (req_wrap->continuation_data == nullptr) {
1214+
req_wrap->continuation_data = std::unique_ptr<FSContinuationData>{
1215+
new FSContinuationData(req, mode, cb)};
1216+
req_wrap->continuation_data->PushPath(std::move(path));
1217+
}
1218+
1219+
// on each iteration of algorithm, mkdir directory on top of stack.
1220+
std::string next_path = req_wrap->continuation_data->PopPath();
1221+
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode,
1222+
uv_fs_callback_t{[](uv_fs_t* req) {
1223+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1224+
Environment* env = req_wrap->env();
1225+
uv_loop_t* loop = env->event_loop();
1226+
std::string path = req->path;
1227+
int err = req->result;
1228+
1229+
while (true) {
1230+
switch (err) {
1231+
case 0: {
1232+
if (req_wrap->continuation_data->paths.size() == 0) {
1233+
req_wrap->continuation_data->Done(0);
1234+
} else {
1235+
uv_fs_req_cleanup(req);
1236+
MKDirpAsync(loop, req, path.c_str(),
1237+
req_wrap->continuation_data->mode, nullptr);
1238+
}
1239+
break;
1240+
}
1241+
case UV_ENOENT: {
1242+
std::string dirname = path.substr(0,
1243+
path.find_last_of(kPathSeparator));
1244+
if (dirname != path) {
1245+
req_wrap->continuation_data->PushPath(std::move(path));
1246+
req_wrap->continuation_data->PushPath(std::move(dirname));
1247+
} else if (req_wrap->continuation_data->paths.size() == 0) {
1248+
err = UV_EEXIST;
1249+
continue;
1250+
}
1251+
uv_fs_req_cleanup(req);
1252+
MKDirpAsync(loop, req, path.c_str(),
1253+
req_wrap->continuation_data->mode, nullptr);
1254+
break;
1255+
}
1256+
case UV_EPERM: {
1257+
req_wrap->continuation_data->Done(err);
1258+
break;
1259+
}
1260+
default:
1261+
if (err == UV_EEXIST &&
1262+
req_wrap->continuation_data->paths.size() > 0) {
1263+
uv_fs_req_cleanup(req);
1264+
MKDirpAsync(loop, req, path.c_str(),
1265+
req_wrap->continuation_data->mode, nullptr);
1266+
} else {
1267+
// verify that the path pointed to is actually a directory.
1268+
uv_fs_req_cleanup(req);
1269+
int err = uv_fs_stat(loop, req, path.c_str(),
1270+
uv_fs_callback_t{[](uv_fs_t* req) {
1271+
FSReqBase* req_wrap = FSReqBase::from_req(req);
1272+
int err = req->result;
1273+
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) err = UV_EEXIST;
1274+
req_wrap->continuation_data->Done(err);
1275+
}});
1276+
if (err < 0) req_wrap->continuation_data->Done(err);
1277+
}
1278+
break;
1279+
}
1280+
break;
1281+
}
1282+
}});
1283+
1284+
return err;
1285+
}
1286+
11511287
static void MKDir(const FunctionCallbackInfo<Value>& args) {
11521288
Environment* env = Environment::GetCurrent(args);
11531289

11541290
const int argc = args.Length();
1155-
CHECK_GE(argc, 3);
1291+
CHECK_GE(argc, 4);
11561292

11571293
BufferValue path(env->isolate(), args[0]);
11581294
CHECK_NOT_NULL(*path);
11591295

11601296
CHECK(args[1]->IsInt32());
11611297
const int mode = args[1].As<Int32>()->Value();
11621298

1163-
FSReqBase* req_wrap_async = GetReqWrap(env, args[2]);
1299+
CHECK(args[2]->IsBoolean());
1300+
bool mkdirp = args[2]->IsTrue();
1301+
1302+
FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
11641303
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
1165-
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8, AfterNoArgs,
1166-
uv_fs_mkdir, *path, mode);
1304+
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
1305+
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
11671306
} else { // mkdir(path, mode, undefined, ctx)
1168-
CHECK_EQ(argc, 4);
1307+
CHECK_EQ(argc, 5);
11691308
FSReqWrapSync req_wrap_sync;
11701309
FS_SYNC_TRACE_BEGIN(mkdir);
1171-
SyncCall(env, args[3], &req_wrap_sync, "mkdir",
1172-
uv_fs_mkdir, *path, mode);
1310+
if (mkdirp) {
1311+
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1312+
MKDirpSync, *path, mode);
1313+
} else {
1314+
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1315+
uv_fs_mkdir, *path, mode);
1316+
}
11731317
FS_SYNC_TRACE_END(mkdir);
11741318
}
11751319
}

0 commit comments

Comments
 (0)