Skip to content

Commit d1e4153

Browse files
coreyfarrellsindresorhus
authored andcommitted
Use native recursive option when available/appropriate (#7)
1 parent f38a1b7 commit d1e4153

File tree

6 files changed

+119
-19
lines changed

6 files changed

+119
-19
lines changed

index.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
const fs = require('fs');
33
const path = require('path');
44
const pify = require('pify');
5+
const semver = require('semver');
56

67
const defaults = {
78
mode: 0o777 & (~process.umask()),
89
fs
910
};
1011

12+
const mkdirOptsObj = semver.satisfies(process.version, '>=10.12.0');
13+
1114
// https://github.com/nodejs/node/issues/8987
1215
// https://github.com/libuv/libuv/pull/1088
1316
const checkPath = pth => {
@@ -22,6 +25,18 @@ const checkPath = pth => {
2225
}
2326
};
2427

28+
const permissionError = pth => {
29+
// This replicates the exception of mkdir with native recusive option when run on
30+
// an invalid drive under Windows.
31+
const error = new Error('operation not permitted, mkdir \'' + pth + '\'');
32+
error.code = 'EPERM';
33+
error.errno = -4048;
34+
error.path = pth;
35+
error.syscall = 'mkdir';
36+
37+
return error;
38+
};
39+
2540
module.exports = (input, options) => Promise.resolve().then(() => {
2641
checkPath(input);
2742
options = Object.assign({}, defaults, options);
@@ -30,12 +45,29 @@ module.exports = (input, options) => Promise.resolve().then(() => {
3045
const mkdir = pify(options.fs.mkdir);
3146
const stat = pify(options.fs.stat);
3247

48+
if (mkdirOptsObj && options.fs.mkdir === fs.mkdir) {
49+
const pth = path.resolve(input);
50+
51+
return mkdir(pth, {
52+
mode: options.mode,
53+
recursive: true
54+
}).then(() => pth);
55+
}
56+
3357
const make = pth => {
3458
return mkdir(pth, options.mode)
3559
.then(() => pth)
3660
.catch(error => {
61+
if (error.code === 'EPERM') {
62+
throw error;
63+
}
64+
3765
if (error.code === 'ENOENT') {
38-
if (error.message.includes('null bytes') || path.dirname(pth) === pth) {
66+
if (path.dirname(pth) === pth) {
67+
throw permissionError(pth);
68+
}
69+
70+
if (error.message.includes('null bytes')) {
3971
throw error;
4072
}
4173

@@ -57,12 +89,31 @@ module.exports.sync = (input, options) => {
5789
checkPath(input);
5890
options = Object.assign({}, defaults, options);
5991

92+
if (mkdirOptsObj && options.fs.mkdirSync === fs.mkdirSync) {
93+
const pth = path.resolve(input);
94+
95+
fs.mkdirSync(pth, {
96+
mode: options.mode,
97+
recursive: true
98+
});
99+
100+
return pth;
101+
}
102+
60103
const make = pth => {
61104
try {
62105
options.fs.mkdirSync(pth, options.mode);
63106
} catch (error) {
107+
if (error.code === 'EPERM') {
108+
throw error;
109+
}
110+
64111
if (error.code === 'ENOENT') {
65-
if (error.message.includes('null bytes') || path.dirname(pth) === pth) {
112+
if (path.dirname(pth) === pth) {
113+
throw permissionError(pth);
114+
}
115+
116+
if (error.message.includes('null bytes')) {
66117
throw error;
67118
}
68119

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"file-system"
4141
],
4242
"dependencies": {
43-
"pify": "^4.0.1"
43+
"pify": "^4.0.1",
44+
"semver": "^5.6.0"
4445
},
4546
"devDependencies": {
4647
"ava": "^1.0.1",

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- CI-tested on macOS, Linux, and Windows
1212
- Actively maintained
1313
- Doesn't bundle a CLI
14+
- Uses native `fs.mkdir` or `fs.mkdirSync` with [recursive option](https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_mkdir_path_options_callback) in node.js >= 10.12.0 unless [overridden](#fs)
1415

1516

1617
## Install
@@ -104,6 +105,9 @@ Default: `require('fs')`
104105

105106
Use a custom `fs` implementation. For example [`graceful-fs`](https://github.com/isaacs/node-graceful-fs).
106107

108+
A custom `fs` implementation will block use of the `recursive` option if `fs.mkdir` or `fs.mkdirSync`
109+
is not the native function.
110+
107111

108112
## Related
109113

@@ -113,6 +117,8 @@ Use a custom `fs` implementation. For example [`graceful-fs`](https://github.com
113117
- [cpy](https://github.com/sindresorhus/cpy) - Copy files
114118
- [cpy-cli](https://github.com/sindresorhus/cpy-cli) - Copy files on the command-line
115119
- [move-file](https://github.com/sindresorhus/move-file) - Move a file
120+
- [fs.mkdir](https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_mkdir_path_options_callback) - native fs.mkdir
121+
- [fs.mkdirSync](https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_mkdirsync_path_options) - native fs.mkdirSync
116122

117123

118124
## License

test/async.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import test from 'ava';
44
import tempy from 'tempy';
55
import gracefulFs from 'graceful-fs';
6-
import {getFixture, assertDir} from './helpers/util';
6+
import {getFixture, assertDir, customFsOpt} from './helpers/util';
77
import makeDir from '..';
88

99
test('main', async t => {
@@ -13,12 +13,19 @@ test('main', async t => {
1313
assertDir(t, madeDir);
1414
});
1515

16-
test('`fs` option', async t => {
16+
test('`fs` option graceful-fs', async t => {
1717
const dir = getFixture();
1818
await makeDir(dir, {fs: gracefulFs});
1919
assertDir(t, dir);
2020
});
2121

22+
test('`fs` option custom', async t => {
23+
const dir = getFixture();
24+
const madeDir = await makeDir(dir, customFsOpt);
25+
t.true(madeDir.length > 0);
26+
assertDir(t, madeDir);
27+
});
28+
2229
test('`mode` option', async t => {
2330
const dir = getFixture();
2431
const mode = 0o744;
@@ -43,10 +50,18 @@ test('file exits', async t => {
4350
});
4451

4552
test('root dir', async t => {
46-
const mode = fs.statSync('/').mode & 0o777;
47-
const dir = await makeDir('/');
48-
t.true(dir.length > 0);
49-
assertDir(t, dir, mode);
53+
if (process.platform === 'win32') {
54+
// Do not assume that C: is current drive.
55+
await t.throwsAsync(makeDir('/'), {
56+
code: 'EPERM',
57+
message: /operation not permitted, mkdir '[A-Za-z]:\\'/
58+
});
59+
} else {
60+
const mode = fs.statSync('/').mode & 0o777;
61+
const dir = await makeDir('/');
62+
t.true(dir.length > 0);
63+
assertDir(t, dir, mode);
64+
}
5065
});
5166

5267
test('race two', async t => {
@@ -99,8 +114,8 @@ if (process.platform === 'win32') {
99114
test('handles non-existent root', async t => {
100115
// We assume the `o:\` drive doesn't exist on Windows
101116
await t.throwsAsync(makeDir('o:\\foo'), {
102-
code: 'ENOENT',
103-
message: /no such file or directory/
117+
code: 'EPERM',
118+
message: /operation not permitted, mkdir/
104119
});
105120
});
106121
}

test/helpers/util.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,13 @@ export const assertDir = (t, dir, mode = 0o777 & (~process.umask())) => {
1414
t.true(pathType.dirSync(dir));
1515
t.is(fs.statSync(dir).mode & 0o777, mode);
1616
};
17+
18+
/* Using this forces test coverage of legacy method on latest versions of node. */
19+
export const customFsOpt = {
20+
fs: {
21+
mkdir: (...args) => fs.mkdir(...args),
22+
stat: (...args) => fs.stat(...args),
23+
mkdirSync: (...args) => fs.mkdirSync(...args),
24+
statSync: (...args) => fs.statSync(...args)
25+
}
26+
};

test/sync.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import test from 'ava';
44
import tempy from 'tempy';
55
import gracefulFs from 'graceful-fs';
6-
import {getFixture, assertDir} from './helpers/util';
6+
import {getFixture, assertDir, customFsOpt} from './helpers/util';
77
import makeDir from '..';
88

99
test('main', t => {
@@ -13,12 +13,19 @@ test('main', t => {
1313
assertDir(t, madeDir);
1414
});
1515

16-
test('`fs` option', t => {
16+
test('`fs` option graceful-fs', t => {
1717
const dir = getFixture();
1818
makeDir.sync(dir, {fs: gracefulFs});
1919
assertDir(t, dir);
2020
});
2121

22+
test('`fs` option custom', t => {
23+
const dir = getFixture();
24+
const madeDir = makeDir.sync(dir, customFsOpt);
25+
t.true(madeDir.length > 0);
26+
assertDir(t, madeDir);
27+
});
28+
2229
test('`mode` option', t => {
2330
const dir = getFixture();
2431
const mode = 0o744;
@@ -45,10 +52,20 @@ test('file exits', t => {
4552
});
4653

4754
test('root dir', t => {
48-
const mode = fs.statSync('/').mode & 0o777;
49-
const dir = makeDir.sync('/');
50-
t.true(dir.length > 0);
51-
assertDir(t, dir, mode);
55+
if (process.platform === 'win32') {
56+
// Do not assume that C: is current drive.
57+
t.throws(() => {
58+
makeDir.sync('/');
59+
}, {
60+
code: 'EPERM',
61+
message: /operation not permitted, mkdir '[A-Za-z]:\\'/
62+
});
63+
} else {
64+
const mode = fs.statSync('/').mode & 0o777;
65+
const dir = makeDir.sync('/');
66+
t.true(dir.length > 0);
67+
assertDir(t, dir, mode);
68+
}
5269
});
5370

5471
test('race two', t => {
@@ -83,8 +100,8 @@ if (process.platform === 'win32') {
83100
t.throws(() => {
84101
makeDir.sync('o:\\foo');
85102
}, {
86-
code: 'ENOENT',
87-
message: /no such file or directory/
103+
code: 'EPERM',
104+
message: /operation not permitted, mkdir/
88105
});
89106
});
90107
}

0 commit comments

Comments
 (0)