Skip to content

Commit 7260ede

Browse files
bcoetargos
authored andcommitted
fs: return first folder made by mkdir recursive
mkdir('/foo/bar', { recursive: true }) and mkdirSync will now return the path of the first folder created. This matches more closely mkdirp's behavior, and is useful for setting folder permissions. PR-URL: #31530 Refs: #31481 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent e2a090b commit 7260ede

File tree

7 files changed

+225
-23
lines changed

7 files changed

+225
-23
lines changed

doc/api/fs.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,8 +2463,10 @@ changes:
24632463
* `callback` {Function}
24642464
* `err` {Error}
24652465

2466-
Asynchronously creates a directory. No arguments other than a possible exception
2467-
are given to the completion callback.
2466+
Asynchronously creates a directory.
2467+
2468+
The callback is given a possible exception and, if `recursive` is `true`, the
2469+
first folder path created, `(err, [path])`.
24682470

24692471
The optional `options` argument can be an integer specifying mode (permission
24702472
and sticky bits), or an object with a `mode` property and a `recursive`
@@ -2508,8 +2510,10 @@ changes:
25082510
* `options` {Object|integer}
25092511
* `recursive` {boolean} **Default:** `false`
25102512
* `mode` {string|integer} Not supported on Windows. **Default:** `0o777`.
2513+
* Returns: {string|undefined}
25112514

2512-
Synchronously creates a directory. Returns `undefined`.
2515+
Synchronously creates a directory. Returns `undefined`, or if `recursive` is
2516+
`true`, the first folder path created.
25132517
This is the synchronous version of [`fs.mkdir()`][].
25142518

25152519
See also: mkdir(2).
@@ -4818,8 +4822,8 @@ added: v10.0.0
48184822
* `mode` {string|integer} Not supported on Windows. **Default:** `0o777`.
48194823
* Returns: {Promise}
48204824

4821-
Asynchronously creates a directory then resolves the `Promise` with no
4822-
arguments upon success.
4825+
Asynchronously creates a directory then resolves the `Promise` with either no
4826+
arguments, or the first folder path created if `recursive` is `true`.
48234827

48244828
The optional `options` argument can be an integer specifying mode (permission
48254829
and sticky bits), or an object with a `mode` property and a `recursive`

lib/fs.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -835,10 +835,13 @@ function mkdirSync(path, options) {
835835
throw new ERR_INVALID_ARG_TYPE('options.recursive', 'boolean', recursive);
836836

837837
const ctx = { path };
838-
binding.mkdir(pathModule.toNamespacedPath(path),
839-
parseMode(mode, 'mode', 0o777), recursive, undefined,
840-
ctx);
838+
const result = binding.mkdir(pathModule.toNamespacedPath(path),
839+
parseMode(mode, 'mode', 0o777), recursive,
840+
undefined, ctx);
841841
handleErrorFromBinding(ctx);
842+
if (recursive) {
843+
return result;
844+
}
842845
}
843846

844847
function readdir(path, options, callback) {

src/inspector_profiler.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ void V8ProfilerConnection::V8ProfilerSessionDelegate::SendMessageToFrontend(
107107
}
108108

109109
static bool EnsureDirectory(const std::string& directory, const char* type) {
110-
uv_fs_t req;
111-
int ret = fs::MKDirpSync(nullptr, &req, directory, 0777, nullptr);
112-
uv_fs_req_cleanup(&req);
110+
fs::FSReqWrapSync req_wrap_sync;
111+
int ret = fs::MKDirpSync(nullptr, &req_wrap_sync.req, directory, 0777,
112+
nullptr);
113113
if (ret < 0 && ret != UV_EEXIST) {
114114
char err_buf[128];
115115
uv_err_name_r(ret, err_buf, sizeof(err_buf));

src/node_file-inl.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ void FSContinuationData::PushPath(const std::string& path) {
2121
paths_.push_back(path);
2222
}
2323

24+
void FSContinuationData::MaybeSetFirstPath(const std::string& path) {
25+
if (first_path_.empty()) {
26+
first_path_ = path;
27+
}
28+
}
29+
2430
std::string FSContinuationData::PopPath() {
2531
CHECK_GT(paths_.size(), 0);
2632
std::string path = std::move(paths_.back());

src/node_file.cc

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,43 @@ void AfterOpenFileHandle(uv_fs_t* req) {
590590
}
591591
}
592592

593+
// Reverse the logic applied by path.toNamespacedPath() to create a
594+
// namespace-prefixed path.
595+
void FromNamespacedPath(std::string* path) {
596+
#ifdef _WIN32
597+
if (path->compare(0, 8, "\\\\?\\UNC\\", 8) == 0) {
598+
*path = path->substr(8);
599+
path->insert(0, "\\\\");
600+
} else if (path->compare(0, 4, "\\\\?\\", 4) == 0) {
601+
*path = path->substr(4);
602+
}
603+
#endif
604+
}
605+
606+
void AfterMkdirp(uv_fs_t* req) {
607+
FSReqBase* req_wrap = FSReqBase::from_req(req);
608+
FSReqAfterScope after(req_wrap, req);
609+
610+
MaybeLocal<Value> path;
611+
Local<Value> error;
612+
613+
if (after.Proceed()) {
614+
if (!req_wrap->continuation_data()->first_path().empty()) {
615+
std::string first_path(req_wrap->continuation_data()->first_path());
616+
FromNamespacedPath(&first_path);
617+
path = StringBytes::Encode(req_wrap->env()->isolate(), first_path.c_str(),
618+
req_wrap->encoding(),
619+
&error);
620+
if (path.IsEmpty())
621+
req_wrap->Reject(error);
622+
else
623+
req_wrap->Resolve(path.ToLocalChecked());
624+
} else {
625+
req_wrap->Resolve(Undefined(req_wrap->env()->isolate()));
626+
}
627+
}
628+
}
629+
593630
void AfterStringPath(uv_fs_t* req) {
594631
FSReqBase* req_wrap = FSReqBase::from_req(req);
595632
FSReqAfterScope after(req_wrap, req);
@@ -1215,18 +1252,25 @@ int MKDirpSync(uv_loop_t* loop,
12151252
const std::string& path,
12161253
int mode,
12171254
uv_fs_cb cb) {
1218-
FSContinuationData continuation_data(req, mode, cb);
1219-
continuation_data.PushPath(std::move(path));
1255+
FSReqWrapSync* req_wrap = ContainerOf(&FSReqWrapSync::req, req);
1256+
1257+
// on the first iteration of algorithm, stash state information.
1258+
if (req_wrap->continuation_data() == nullptr) {
1259+
req_wrap->set_continuation_data(
1260+
std::make_unique<FSContinuationData>(req, mode, cb));
1261+
req_wrap->continuation_data()->PushPath(std::move(path));
1262+
}
12201263

1221-
while (continuation_data.paths().size() > 0) {
1222-
std::string next_path = continuation_data.PopPath();
1264+
while (req_wrap->continuation_data()->paths().size() > 0) {
1265+
std::string next_path = req_wrap->continuation_data()->PopPath();
12231266
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
12241267
while (true) {
12251268
switch (err) {
12261269
// Note: uv_fs_req_cleanup in terminal paths will be called by
12271270
// ~FSReqWrapSync():
12281271
case 0:
1229-
if (continuation_data.paths().size() == 0) {
1272+
req_wrap->continuation_data()->MaybeSetFirstPath(next_path);
1273+
if (req_wrap->continuation_data()->paths().size() == 0) {
12301274
return 0;
12311275
}
12321276
break;
@@ -1239,9 +1283,9 @@ int MKDirpSync(uv_loop_t* loop,
12391283
std::string dirname = next_path.substr(0,
12401284
next_path.find_last_of(kPathSeparator));
12411285
if (dirname != next_path) {
1242-
continuation_data.PushPath(std::move(next_path));
1243-
continuation_data.PushPath(std::move(dirname));
1244-
} else if (continuation_data.paths().size() == 0) {
1286+
req_wrap->continuation_data()->PushPath(std::move(next_path));
1287+
req_wrap->continuation_data()->PushPath(std::move(dirname));
1288+
} else if (req_wrap->continuation_data()->paths().size() == 0) {
12451289
err = UV_EEXIST;
12461290
continue;
12471291
}
@@ -1253,7 +1297,8 @@ int MKDirpSync(uv_loop_t* loop,
12531297
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
12541298
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) {
12551299
uv_fs_req_cleanup(req);
1256-
if (orig_err == UV_EEXIST && continuation_data.paths().size() > 0) {
1300+
if (orig_err == UV_EEXIST &&
1301+
req_wrap->continuation_data()->paths().size() > 0) {
12571302
return UV_ENOTDIR;
12581303
}
12591304
return UV_EEXIST;
@@ -1298,8 +1343,10 @@ int MKDirpAsync(uv_loop_t* loop,
12981343
// FSReqAfterScope::~FSReqAfterScope()
12991344
case 0: {
13001345
if (req_wrap->continuation_data()->paths().size() == 0) {
1346+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
13011347
req_wrap->continuation_data()->Done(0);
13021348
} else {
1349+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
13031350
uv_fs_req_cleanup(req);
13041351
MKDirpAsync(loop, req, path.c_str(),
13051352
req_wrap->continuation_data()->mode(), nullptr);
@@ -1362,6 +1409,25 @@ int MKDirpAsync(uv_loop_t* loop,
13621409
return err;
13631410
}
13641411

1412+
int CallMKDirpSync(Environment* env, const FunctionCallbackInfo<Value>& args,
1413+
FSReqWrapSync* req_wrap, const char* path, int mode) {
1414+
env->PrintSyncTrace();
1415+
int err = MKDirpSync(env->event_loop(), &req_wrap->req, path, mode,
1416+
nullptr);
1417+
if (err < 0) {
1418+
v8::Local<v8::Context> context = env->context();
1419+
v8::Local<v8::Object> ctx_obj = args[4].As<v8::Object>();
1420+
v8::Isolate* isolate = env->isolate();
1421+
ctx_obj->Set(context,
1422+
env->errno_string(),
1423+
v8::Integer::New(isolate, err)).Check();
1424+
ctx_obj->Set(context,
1425+
env->syscall_string(),
1426+
OneByteString(isolate, "mkdir")).Check();
1427+
}
1428+
return err;
1429+
}
1430+
13651431
static void MKDir(const FunctionCallbackInfo<Value>& args) {
13661432
Environment* env = Environment::GetCurrent(args);
13671433

@@ -1380,14 +1446,29 @@ static void MKDir(const FunctionCallbackInfo<Value>& args) {
13801446
FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
13811447
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
13821448
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
1383-
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
1449+
mkdirp ? AfterMkdirp : AfterNoArgs,
1450+
mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
13841451
} else { // mkdir(path, mode, undefined, ctx)
13851452
CHECK_EQ(argc, 5);
13861453
FSReqWrapSync req_wrap_sync;
13871454
FS_SYNC_TRACE_BEGIN(mkdir);
13881455
if (mkdirp) {
1389-
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1390-
MKDirpSync, *path, mode);
1456+
int err = CallMKDirpSync(env, args, &req_wrap_sync, *path, mode);
1457+
if (err == 0 &&
1458+
!req_wrap_sync.continuation_data()->first_path().empty()) {
1459+
Local<Value> error;
1460+
std::string first_path(req_wrap_sync.continuation_data()->first_path());
1461+
FromNamespacedPath(&first_path);
1462+
MaybeLocal<Value> path = StringBytes::Encode(env->isolate(),
1463+
first_path.c_str(),
1464+
UTF8, &error);
1465+
if (path.IsEmpty()) {
1466+
Local<Object> ctx = args[4].As<Object>();
1467+
ctx->Set(env->context(), env->error_string(), error).Check();
1468+
return;
1469+
}
1470+
args.GetReturnValue().Set(path.ToLocalChecked());
1471+
}
13911472
} else {
13921473
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
13931474
uv_fs_mkdir, *path, mode);

src/node_file.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ class FSContinuationData : public MemoryRetainer {
1919
inline void PushPath(std::string&& path);
2020
inline void PushPath(const std::string& path);
2121
inline std::string PopPath();
22+
// Used by mkdirp to track the first path created:
23+
inline void MaybeSetFirstPath(const std::string& path);
2224
inline void Done(int result);
2325

2426
int mode() const { return mode_; }
2527
const std::vector<std::string>& paths() const { return paths_; }
28+
const std::string& first_path() const { return first_path_; }
2629

2730
void MemoryInfo(MemoryTracker* tracker) const override;
2831
SET_MEMORY_INFO_NAME(FSContinuationData)
@@ -33,6 +36,7 @@ class FSContinuationData : public MemoryRetainer {
3336
uv_fs_t* req_;
3437
int mode_;
3538
std::vector<std::string> paths_;
39+
std::string first_path_;
3640
};
3741

3842
class FSReqBase : public ReqWrap<uv_fs_t> {
@@ -306,8 +310,18 @@ class FSReqWrapSync {
306310
~FSReqWrapSync() { uv_fs_req_cleanup(&req); }
307311
uv_fs_t req;
308312

313+
FSContinuationData* continuation_data() const {
314+
return continuation_data_.get();
315+
}
316+
void set_continuation_data(std::unique_ptr<FSContinuationData> data) {
317+
continuation_data_ = std::move(data);
318+
}
319+
309320
FSReqWrapSync(const FSReqWrapSync&) = delete;
310321
FSReqWrapSync& operator=(const FSReqWrapSync&) = delete;
322+
323+
private:
324+
std::unique_ptr<FSContinuationData> continuation_data_;
311325
};
312326

313327
// TODO(addaleax): Currently, callers check the return value and assume

test/parallel/test-fs-mkdir.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,100 @@ if (common.isMainThread && (common.isLinux || common.isOSX)) {
245245
});
246246
}
247247

248+
// `mkdirp` returns first folder created, when all folders are new.
249+
{
250+
const dir1 = nextdir();
251+
const dir2 = nextdir();
252+
const firstPathCreated = path.join(tmpdir.path, dir1);
253+
const pathname = path.join(tmpdir.path, dir1, dir2);
254+
255+
fs.mkdir(pathname, { recursive: true }, common.mustCall(function(err, path) {
256+
assert.strictEqual(err, null);
257+
assert.strictEqual(fs.existsSync(pathname), true);
258+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
259+
assert.strictEqual(path, firstPathCreated);
260+
}));
261+
}
262+
263+
// `mkdirp` returns first folder created, when last folder is new.
264+
{
265+
const dir1 = nextdir();
266+
const dir2 = nextdir();
267+
const pathname = path.join(tmpdir.path, dir1, dir2);
268+
fs.mkdirSync(path.join(tmpdir.path, dir1));
269+
fs.mkdir(pathname, { recursive: true }, common.mustCall(function(err, path) {
270+
assert.strictEqual(err, null);
271+
assert.strictEqual(fs.existsSync(pathname), true);
272+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
273+
assert.strictEqual(path, pathname);
274+
}));
275+
}
276+
277+
// `mkdirp` returns undefined, when no new folders are created.
278+
{
279+
const dir1 = nextdir();
280+
const dir2 = nextdir();
281+
const pathname = path.join(tmpdir.path, dir1, dir2);
282+
fs.mkdirSync(path.join(tmpdir.path, dir1, dir2), { recursive: true });
283+
fs.mkdir(pathname, { recursive: true }, common.mustCall(function(err, path) {
284+
assert.strictEqual(err, null);
285+
assert.strictEqual(fs.existsSync(pathname), true);
286+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
287+
assert.strictEqual(path, undefined);
288+
}));
289+
}
290+
291+
// `mkdirp.sync` returns first folder created, when all folders are new.
292+
{
293+
const dir1 = nextdir();
294+
const dir2 = nextdir();
295+
const firstPathCreated = path.join(tmpdir.path, dir1);
296+
const pathname = path.join(tmpdir.path, dir1, dir2);
297+
const p = fs.mkdirSync(pathname, { recursive: true });
298+
assert.strictEqual(fs.existsSync(pathname), true);
299+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
300+
assert.strictEqual(p, firstPathCreated);
301+
}
302+
303+
// `mkdirp.sync` returns first folder created, when last folder is new.
304+
{
305+
const dir1 = nextdir();
306+
const dir2 = nextdir();
307+
const pathname = path.join(tmpdir.path, dir1, dir2);
308+
fs.mkdirSync(path.join(tmpdir.path, dir1), { recursive: true });
309+
const p = fs.mkdirSync(pathname, { recursive: true });
310+
assert.strictEqual(fs.existsSync(pathname), true);
311+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
312+
assert.strictEqual(p, pathname);
313+
}
314+
315+
// `mkdirp.sync` returns undefined, when no new folders are created.
316+
{
317+
const dir1 = nextdir();
318+
const dir2 = nextdir();
319+
const pathname = path.join(tmpdir.path, dir1, dir2);
320+
fs.mkdirSync(path.join(tmpdir.path, dir1, dir2), { recursive: true });
321+
const p = fs.mkdirSync(pathname, { recursive: true });
322+
assert.strictEqual(fs.existsSync(pathname), true);
323+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
324+
assert.strictEqual(p, undefined);
325+
}
326+
327+
// `mkdirp.promises` returns first folder created, when all folders are new.
328+
{
329+
const dir1 = nextdir();
330+
const dir2 = nextdir();
331+
const firstPathCreated = path.join(tmpdir.path, dir1);
332+
const pathname = path.join(tmpdir.path, dir1, dir2);
333+
async function testCase() {
334+
const p = await fs.promises.mkdir(pathname, { recursive: true });
335+
assert.strictEqual(fs.existsSync(pathname), true);
336+
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
337+
assert.strictEqual(p, firstPathCreated);
338+
}
339+
testCase();
340+
}
341+
248342
// Keep the event loop alive so the async mkdir() requests
249343
// have a chance to run (since they don't ref the event loop).
250344
process.nextTick(() => {});

0 commit comments

Comments
 (0)