Skip to content

Commit ff58854

Browse files
bcoeMylesBorins
authored andcommittedMar 10, 2020
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 607ac90 commit ff58854

File tree

7 files changed

+225
-23
lines changed

7 files changed

+225
-23
lines changed
 

‎doc/api/fs.md

+9-5
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).
@@ -4815,8 +4819,8 @@ added: v10.0.0
48154819
* `mode` {string|integer} Not supported on Windows. **Default:** `0o777`.
48164820
* Returns: {Promise}
48174821

4818-
Asynchronously creates a directory then resolves the `Promise` with no
4819-
arguments upon success.
4822+
Asynchronously creates a directory then resolves the `Promise` with either no
4823+
arguments, or the first folder path created if `recursive` is `true`.
48204824

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

‎lib/fs.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -855,10 +855,13 @@ function mkdirSync(path, options) {
855855
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);
856856

857857
const ctx = { path };
858-
binding.mkdir(pathModule.toNamespacedPath(path),
859-
parseMode(mode, 'mode', 0o777), recursive, undefined,
860-
ctx);
858+
const result = binding.mkdir(pathModule.toNamespacedPath(path),
859+
parseMode(mode, 'mode', 0o777), recursive,
860+
undefined, ctx);
861861
handleErrorFromBinding(ctx);
862+
if (recursive) {
863+
return result;
864+
}
862865
}
863866

864867
function readdir(path, options, callback) {

‎src/inspector_profiler.cc

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

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

+93-12
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);
@@ -1213,18 +1250,25 @@ int MKDirpSync(uv_loop_t* loop,
12131250
const std::string& path,
12141251
int mode,
12151252
uv_fs_cb cb) {
1216-
FSContinuationData continuation_data(req, mode, cb);
1217-
continuation_data.PushPath(std::move(path));
1253+
FSReqWrapSync* req_wrap = ContainerOf(&FSReqWrapSync::req, req);
1254+
1255+
// on the first iteration of algorithm, stash state information.
1256+
if (req_wrap->continuation_data() == nullptr) {
1257+
req_wrap->set_continuation_data(
1258+
std::make_unique<FSContinuationData>(req, mode, cb));
1259+
req_wrap->continuation_data()->PushPath(std::move(path));
1260+
}
12181261

1219-
while (continuation_data.paths().size() > 0) {
1220-
std::string next_path = continuation_data.PopPath();
1262+
while (req_wrap->continuation_data()->paths().size() > 0) {
1263+
std::string next_path = req_wrap->continuation_data()->PopPath();
12211264
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
12221265
while (true) {
12231266
switch (err) {
12241267
// Note: uv_fs_req_cleanup in terminal paths will be called by
12251268
// ~FSReqWrapSync():
12261269
case 0:
1227-
if (continuation_data.paths().size() == 0) {
1270+
req_wrap->continuation_data()->MaybeSetFirstPath(next_path);
1271+
if (req_wrap->continuation_data()->paths().size() == 0) {
12281272
return 0;
12291273
}
12301274
break;
@@ -1237,9 +1281,9 @@ int MKDirpSync(uv_loop_t* loop,
12371281
std::string dirname = next_path.substr(0,
12381282
next_path.find_last_of(kPathSeparator));
12391283
if (dirname != next_path) {
1240-
continuation_data.PushPath(std::move(next_path));
1241-
continuation_data.PushPath(std::move(dirname));
1242-
} else if (continuation_data.paths().size() == 0) {
1284+
req_wrap->continuation_data()->PushPath(std::move(next_path));
1285+
req_wrap->continuation_data()->PushPath(std::move(dirname));
1286+
} else if (req_wrap->continuation_data()->paths().size() == 0) {
12431287
err = UV_EEXIST;
12441288
continue;
12451289
}
@@ -1251,7 +1295,8 @@ int MKDirpSync(uv_loop_t* loop,
12511295
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
12521296
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) {
12531297
uv_fs_req_cleanup(req);
1254-
if (orig_err == UV_EEXIST && continuation_data.paths().size() > 0) {
1298+
if (orig_err == UV_EEXIST &&
1299+
req_wrap->continuation_data()->paths().size() > 0) {
12551300
return UV_ENOTDIR;
12561301
}
12571302
return UV_EEXIST;
@@ -1296,8 +1341,10 @@ int MKDirpAsync(uv_loop_t* loop,
12961341
// FSReqAfterScope::~FSReqAfterScope()
12971342
case 0: {
12981343
if (req_wrap->continuation_data()->paths().size() == 0) {
1344+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
12991345
req_wrap->continuation_data()->Done(0);
13001346
} else {
1347+
req_wrap->continuation_data()->MaybeSetFirstPath(path);
13011348
uv_fs_req_cleanup(req);
13021349
MKDirpAsync(loop, req, path.c_str(),
13031350
req_wrap->continuation_data()->mode(), nullptr);
@@ -1360,6 +1407,25 @@ int MKDirpAsync(uv_loop_t* loop,
13601407
return err;
13611408
}
13621409

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

@@ -1378,14 +1444,29 @@ static void MKDir(const FunctionCallbackInfo<Value>& args) {
13781444
FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
13791445
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
13801446
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
1381-
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
1447+
mkdirp ? AfterMkdirp : AfterNoArgs,
1448+
mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
13821449
} else { // mkdir(path, mode, undefined, ctx)
13831450
CHECK_EQ(argc, 5);
13841451
FSReqWrapSync req_wrap_sync;
13851452
FS_SYNC_TRACE_BEGIN(mkdir);
13861453
if (mkdirp) {
1387-
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
1388-
MKDirpSync, *path, mode);
1454+
int err = CallMKDirpSync(env, args, &req_wrap_sync, *path, mode);
1455+
if (err == 0 &&
1456+
!req_wrap_sync.continuation_data()->first_path().empty()) {
1457+
Local<Value> error;
1458+
std::string first_path(req_wrap_sync.continuation_data()->first_path());
1459+
FromNamespacedPath(&first_path);
1460+
MaybeLocal<Value> path = StringBytes::Encode(env->isolate(),
1461+
first_path.c_str(),
1462+
UTF8, &error);
1463+
if (path.IsEmpty()) {
1464+
Local<Object> ctx = args[4].As<Object>();
1465+
ctx->Set(env->context(), env->error_string(), error).Check();
1466+
return;
1467+
}
1468+
args.GetReturnValue().Set(path.ToLocalChecked());
1469+
}
13891470
} else {
13901471
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
13911472
uv_fs_mkdir, *path, mode);

‎src/node_file.h

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

+94
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)
Please sign in to comment.