Skip to content

Commit 13fe56b

Browse files
author
bcoe
committed
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 <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 43fb6ff commit 13fe56b

7 files changed

Lines changed: 225 additions & 23 deletions

File tree

doc/api/fs.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,8 +2452,10 @@ changes:
24522452
* `callback` {Function}
24532453
* `err` {Error}
24542454

2455-
Asynchronously creates a directory. No arguments other than a possible exception
2456-
are given to the completion callback.
2455+
Asynchronously creates a directory.
2456+
2457+
The callback is given a possible exception and, if `recursive` is `true`, the
2458+
first folder path created, `(err, [path])`.
24572459

24582460
The optional `options` argument can be an integer specifying `mode` (permission
24592461
and sticky bits), or an object with a `mode` property and a `recursive`
@@ -2497,8 +2499,10 @@ changes:
24972499
* `options` {Object|integer}
24982500
* `recursive` {boolean} **Default:** `false`
24992501
* `mode` {string|integer} Not supported on Windows. **Default:** `0o777`.
2502+
* Returns: {string|undefined}
25002503

2501-
Synchronously creates a directory. Returns `undefined`.
2504+
Synchronously creates a directory. Returns `undefined`, or if `recursive` is
2505+
`true`, the first folder path created.
25022506
This is the synchronous version of [`fs.mkdir()`][].
25032507

25042508
See also: mkdir(2).
@@ -4798,8 +4802,8 @@ added: v10.0.0
47984802
* `mode` {string|integer} Not supported on Windows. **Default:** `0o777`.
47994803
* Returns: {Promise}
48004804

4801-
Asynchronously creates a directory then resolves the `Promise` with no
4802-
arguments upon success.
4805+
Asynchronously creates a directory then resolves the `Promise` with either no
4806+
arguments, or the first folder path created if `recursive` is `true`.
48034807

48044808
The optional `options` argument can be an integer specifying `mode` (permission
48054809
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
@@ -856,10 +856,13 @@ function mkdirSync(path, options) {
856856
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
857857

858858
const ctx = { path };
859-
binding.mkdir(pathModule.toNamespacedPath(path),
860-
parseFileMode(mode, 'mode'), recursive, undefined,
861-
ctx);
859+
const result = binding.mkdir(pathModule.toNamespacedPath(path),
860+
parseFileMode(mode, 'mode'), recursive,
861+
undefined, ctx);
862862
handleErrorFromBinding(ctx);
863+
if (recursive) {
864+
return result;
865+
}
863866
}
864867

865868
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
@@ -586,6 +586,43 @@ void AfterOpenFileHandle(uv_fs_t* req) {
586586
}
587587
}
588588

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

1224-
while (continuation_data.paths().size() > 0) {
1225-
std::string next_path = continuation_data.PopPath();
1267+
while (req_wrap->continuation_data()->paths().size() > 0) {
1268+
std::string next_path = req_wrap->continuation_data()->PopPath();
12261269
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
12271270
while (true) {
12281271
switch (err) {
12291272
// Note: uv_fs_req_cleanup in terminal paths will be called by
12301273
// ~FSReqWrapSync():
12311274
case 0:
1232-
if (continuation_data.paths().size() == 0) {
1275+
req_wrap->continuation_data()->MaybeSetFirstPath(next_path);
1276+
if (req_wrap->continuation_data()->paths().size() == 0) {
12331277
return 0;
12341278
}
12351279
break;
@@ -1241,9 +1285,9 @@ int MKDirpSync(uv_loop_t* loop,
12411285
std::string dirname = next_path.substr(0,
12421286
next_path.find_last_of(kPathSeparator));
12431287
if (dirname != next_path) {
1244-
continuation_data.PushPath(std::move(next_path));
1245-
continuation_data.PushPath(std::move(dirname));
1246-
} else if (continuation_data.paths().size() == 0) {
1288+
req_wrap->continuation_data()->PushPath(std::move(next_path));
1289+
req_wrap->continuation_data()->PushPath(std::move(dirname));
1290+
} else if (req_wrap->continuation_data()->paths().size() == 0) {
12471291
err = UV_EEXIST;
12481292
continue;
12491293
}
@@ -1255,7 +1299,8 @@ int MKDirpSync(uv_loop_t* loop,
12551299
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
12561300
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) {
12571301
uv_fs_req_cleanup(req);
1258-
if (orig_err == UV_EEXIST && continuation_data.paths().size() > 0) {
1302+
if (orig_err == UV_EEXIST &&
1303+
req_wrap->continuation_data()->paths().size() > 0) {
12591304
return UV_ENOTDIR;
12601305
}
12611306
return UV_EEXIST;
@@ -1300,8 +1345,10 @@ int MKDirpAsync(uv_loop_t* loop,
13001345
// FSReqAfterScope::~FSReqAfterScope()
13011346
case 0: {
13021347
if (req_wrap->continuation_data()->paths().size() == 0) {
1348+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
13031349
req_wrap->continuation_data()->Done(0);
13041350
} else {
1351+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
13051352
uv_fs_req_cleanup(req);
13061353
MKDirpAsync(loop, req, path.c_str(),
13071354
req_wrap->continuation_data()->mode(), nullptr);
@@ -1364,6 +1411,25 @@ int MKDirpAsync(uv_loop_t* loop,
13641411
return err;
13651412
}
13661413

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

@@ -1382,14 +1448,29 @@ static void MKDir(const FunctionCallbackInfo<Value>& args) {
13821448
FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
13831449
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
13841450
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
1385-
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
1451+
mkdirp ? AfterMkdirp : AfterNoArgs,
1452+
mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
13861453
} else { // mkdir(path, mode, undefined, ctx)
13871454
CHECK_EQ(argc, 5);
13881455
FSReqWrapSync req_wrap_sync;
13891456
FS_SYNC_TRACE_BEGIN(mkdir);
13901457
if (mkdirp) {
1391-
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1392-
MKDirpSync, *path, mode);
1458+
int err = CallMKDirpSync(env, args, &req_wrap_sync, *path, mode);
1459+
if (err == 0 &&
1460+
!req_wrap_sync.continuation_data()->first_path().empty()) {
1461+
Local<Value> error;
1462+
std::string first_path(req_wrap_sync.continuation_data()->first_path());
1463+
FromNamespacedPath(&first_path);
1464+
MaybeLocal<Value> path = StringBytes::Encode(env->isolate(),
1465+
first_path.c_str(),
1466+
UTF8, &error);
1467+
if (path.IsEmpty()) {
1468+
Local<Object> ctx = args[4].As<Object>();
1469+
ctx->Set(env->context(), env->error_string(), error).Check();
1470+
return;
1471+
}
1472+
args.GetReturnValue().Set(path.ToLocalChecked());
1473+
}
13931474
} else {
13941475
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
13951476
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
@@ -239,6 +239,100 @@ if (common.isMainThread && (common.isLinux || common.isOSX)) {
239239
});
240240
}
241241

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

0 commit comments

Comments
 (0)