Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: implement recursive (mkdirp) functionality #21875

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions benchmark/fs/bench-mkdirp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const common = require('../common');
const fs = require('fs');
const tmpdir = require('../../test/common/tmpdir');
tmpdir.refresh();
let dirc = 0;

const bench = common.createBenchmark(main, {
n: [1e4],
});

function main({ n }) {
bench.start();
(function r(cntr) {
if (cntr-- <= 0)
return bench.end(n);
const pathname = `${tmpdir.path}/${++dirc}/${++dirc}/${++dirc}/${++dirc}`;
fs.mkdir(pathname, { createParents: true }, (err) => {
r(cntr);
});
}(n));
}
35 changes: 28 additions & 7 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2047,7 +2047,7 @@ changes:

Synchronous lstat(2).

## fs.mkdir(path[, mode], callback)
## fs.mkdir(path[, options], callback)
<!-- YAML
added: v0.1.8
changes:
Expand All @@ -2066,16 +2066,29 @@ changes:
-->

* `path` {string|Buffer|URL}
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
* `options` {Object|integer}
* `recursive` {boolean} **Default:** `false`
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
* `callback` {Function}
* `err` {Error}

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

The optional `options` argument can be an integer specifying mode (permission
and sticky bits), or an object with a `mode` property and a `recursive`
property indicating whether parent folders should be created.

```js
// Creates /tmp/a/apple, regardless of whether `/tmp` and /tmp/a exist.
fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
if (err) throw err;
});
```

See also: mkdir(2).

## fs.mkdirSync(path[, mode])
## fs.mkdirSync(path[, options])
<!-- YAML
added: v0.1.21
changes:
Expand All @@ -2086,7 +2099,9 @@ changes:
-->

* `path` {string|Buffer|URL}
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
* `options` {Object|integer}
* `recursive` {boolean} **Default:** `false`
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.

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

### fsPromises.mkdir(path[, mode])
### fsPromises.mkdir(path[, options])
<!-- YAML
added: v10.0.0
-->

* `path` {string|Buffer|URL}
* `mode` {integer} **Default:** `0o777`
* `options` {Object|integer}
* `recursive` {boolean} **Default:** `false`
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
* Returns: {Promise}

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

The optional `options` argument can be an integer specifying mode (permission
and sticky bits), or an object with a `mode` property and a `recursive`
property indicating whether parent folders should be created.

### fsPromises.mkdtemp(prefix[, options])
<!-- YAML
added: v10.0.0
Expand Down Expand Up @@ -4627,7 +4648,7 @@ the file contents.
[`fs.ftruncate()`]: #fs_fs_ftruncate_fd_len_callback
[`fs.futimes()`]: #fs_fs_futimes_fd_atime_mtime_callback
[`fs.lstat()`]: #fs_fs_lstat_path_options_callback
[`fs.mkdir()`]: #fs_fs_mkdir_path_mode_callback
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
Expand Down
45 changes: 32 additions & 13 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -721,29 +721,48 @@ function fsyncSync(fd) {
handleErrorFromBinding(ctx);
}

function mkdir(path, mode, callback) {
function mkdir(path, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'number' || typeof options === 'string') {
options = { mode: options };
}
const {
recursive = false,
mode = 0o777
} = options || {};
callback = makeCallback(callback);
path = getPathFromURL(path);
validatePath(path);

if (arguments.length < 3) {
callback = makeCallback(mode);
mode = 0o777;
} else {
callback = makeCallback(callback);
mode = validateMode(mode, 'mode', 0o777);
}
validatePath(path);
if (typeof recursive !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);

const req = new FSReqCallback();
req.oncomplete = callback;
binding.mkdir(pathModule.toNamespacedPath(path), mode, req);
binding.mkdir(pathModule.toNamespacedPath(path),
validateMode(mode, 'mode', 0o777), recursive, req);
}

function mkdirSync(path, mode) {
function mkdirSync(path, options) {
if (typeof options === 'number' || typeof options === 'string') {
options = { mode: options };
}
path = getPathFromURL(path);
const {
recursive = false,
mode = 0o777
} = options || {};

validatePath(path);
mode = validateMode(mode, 'mode', 0o777);
if (typeof recursive !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);

const ctx = { path };
binding.mkdir(pathModule.toNamespacedPath(path), mode, undefined, ctx);
binding.mkdir(pathModule.toNamespacedPath(path),
validateMode(mode, 'mode', 0o777), recursive, undefined,
ctx);
handleErrorFromBinding(ctx);
}

Expand Down
18 changes: 15 additions & 3 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,23 @@ async function fsync(handle) {
return binding.fsync(handle.fd, kUsePromises);
}

async function mkdir(path, mode) {
async function mkdir(path, options) {
if (typeof options === 'number' || typeof options === 'string') {
options = { mode: options };
}
const {
recursive = false,
mode = 0o777
} = options || {};
path = getPathFromURL(path);

validatePath(path);
mode = validateMode(mode, 'mode', 0o777);
return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises);
if (typeof recursive !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', recursive);

return binding.mkdir(pathModule.toNamespacedPath(path),
validateMode(mode, 'mode', 0o777), recursive,
kUsePromises);
}

async function readdir(path, options) {
Expand Down
158 changes: 151 additions & 7 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ using v8::Value;
# define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif

#ifndef S_ISDIR
# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
#endif

#ifdef __POSIX__
const char* kPathSeparator = "/";
#else
const char* kPathSeparator = "\\/";
#endif

#define GET_OFFSET(a) ((a)->IsNumber() ? (a).As<Integer>()->Value() : -1)
#define TRACE_NAME(name) "fs.sync." #name
#define GET_TRACE_ENABLED \
Expand Down Expand Up @@ -1148,28 +1158,162 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
}
}

int MKDirpSync(uv_loop_t* loop, uv_fs_t* req, const std::string& path, int mode,

This comment was marked as resolved.

This comment was marked as resolved.

uv_fs_cb cb = nullptr) {
FSContinuationData continuation_data(req, mode, cb);
continuation_data.PushPath(std::move(path));

while (continuation_data.paths.size() > 0) {
std::string next_path = continuation_data.PopPath();
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode, nullptr);
while (true) {
switch (err) {
case 0:
if (continuation_data.paths.size() == 0) {
return 0;
Copy link
Member

@TimothyGu TimothyGu Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe uv_fs_req_cleanup(req) is needed here too?

}
break;
case UV_ENOENT: {
std::string dirname = next_path.substr(0,
next_path.find_last_of(kPathSeparator));
if (dirname != next_path) {
continuation_data.PushPath(std::move(next_path));
continuation_data.PushPath(std::move(dirname));
} else if (continuation_data.paths.size() == 0) {
err = UV_EEXIST;
continue;
}
break;
}
case UV_EPERM: {
return err;
}
default:
uv_fs_req_cleanup(req);
err = uv_fs_stat(loop, req, next_path.c_str(), nullptr);
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) return UV_EEXIST;
if (err < 0) return err;
break;
}
break;
}
uv_fs_req_cleanup(req);
}

return 0;
}

int MKDirpAsync(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int mode,
uv_fs_cb cb) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
// on the first iteration of algorithm, stash state information.
if (req_wrap->continuation_data == nullptr) {
req_wrap->continuation_data = std::unique_ptr<FSContinuationData>{
new FSContinuationData(req, mode, cb)};
req_wrap->continuation_data->PushPath(std::move(path));
}

// on each iteration of algorithm, mkdir directory on top of stack.
std::string next_path = req_wrap->continuation_data->PopPath();
int err = uv_fs_mkdir(loop, req, next_path.c_str(), mode,
uv_fs_callback_t{[](uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
Environment* env = req_wrap->env();
uv_loop_t* loop = env->event_loop();
std::string path = req->path;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating a copy of the path was necessary here, otherwise the pointer to the path is freed when uv_fs_req_cleanup is called prior to uv_fs_stat.

int err = req->result;

while (true) {
switch (err) {
case 0: {
if (req_wrap->continuation_data->paths.size() == 0) {
req_wrap->continuation_data->Done(0);
} else {
uv_fs_req_cleanup(req);
MKDirpAsync(loop, req, path.c_str(),
req_wrap->continuation_data->mode, nullptr);
}
break;
}
case UV_ENOENT: {
std::string dirname = path.substr(0,
path.find_last_of(kPathSeparator));
if (dirname != path) {
req_wrap->continuation_data->PushPath(std::move(path));
req_wrap->continuation_data->PushPath(std::move(dirname));
} else if (req_wrap->continuation_data->paths.size() == 0) {
err = UV_EEXIST;
continue;
}
uv_fs_req_cleanup(req);
MKDirpAsync(loop, req, path.c_str(),
req_wrap->continuation_data->mode, nullptr);
break;
}
case UV_EPERM: {
req_wrap->continuation_data->Done(err);
break;
}
default:
if (err == UV_EEXIST &&
req_wrap->continuation_data->paths.size() > 0) {
uv_fs_req_cleanup(req);
MKDirpAsync(loop, req, path.c_str(),
req_wrap->continuation_data->mode, nullptr);
} else {
// verify that the path pointed to is actually a directory.
uv_fs_req_cleanup(req);
int err = uv_fs_stat(loop, req, path.c_str(),
uv_fs_callback_t{[](uv_fs_t* req) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the uv_fs_callback_t{} necessary here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req_wrap calls uv_fs_req_cleanup when control is returned back from the MKDir functions, so we should be okay in both the flows you brought up -- will have a follow on fix for your nits, thanks for the review.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we should be okay in both the flows you brought up

Ah oops, didn't realize this is all in a lambda function. Got it.

Also to clarify, I meant that the explicit uv_fs_callback_t cast seems to be unnecessary.

FSReqBase* req_wrap = FSReqBase::from_req(req);
int err = req->result;
if (err == 0 && !S_ISDIR(req->statbuf.st_mode)) err = UV_EEXIST;
req_wrap->continuation_data->Done(err);
}});
if (err < 0) req_wrap->continuation_data->Done(err);
}
break;
}
break;
}
}});

return err;
}

static void MKDir(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

const int argc = args.Length();
CHECK_GE(argc, 3);
CHECK_GE(argc, 4);

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

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

FSReqBase* req_wrap_async = GetReqWrap(env, args[2]);
CHECK(args[2]->IsBoolean());
bool mkdirp = args[2]->IsTrue();

FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
if (req_wrap_async != nullptr) { // mkdir(path, mode, req)
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8, AfterNoArgs,
uv_fs_mkdir, *path, mode);
AsyncCall(env, req_wrap_async, args, "mkdir", UTF8,
AfterNoArgs, mkdirp ? MKDirpAsync : uv_fs_mkdir, *path, mode);
} else { // mkdir(path, mode, undefined, ctx)
CHECK_EQ(argc, 4);
CHECK_EQ(argc, 5);
FSReqWrapSync req_wrap_sync;
FS_SYNC_TRACE_BEGIN(mkdir);
SyncCall(env, args[3], &req_wrap_sync, "mkdir",
uv_fs_mkdir, *path, mode);
if (mkdirp) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you move this if to be a ternary like the AsyncCall one above? I believe it would look like SyncCall(env, args[4], &req_wrap_sync, "mkdir", mkdirp ? MKDirpSync : uv_fs_mkdir, *path, mode);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because of the slightly different function call for uv_fs_mkdir (which uses const char *), vs., MKDirpSync which uses std:string &, the compiler seems to hiccup on the ternary operation. For now leaving this as an if statement.

SyncCall(env, args[4], &req_wrap_sync, "mkdir",
MKDirpSync, *path, mode);
} else {
SyncCall(env, args[4], &req_wrap_sync, "mkdir",
uv_fs_mkdir, *path, mode);
}
FS_SYNC_TRACE_END(mkdir);
}
}
Expand Down
Loading