Skip to content

Commit

Permalink
Use native recursive option when available/appropriate (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
coreyfarrell authored and sindresorhus committed Feb 4, 2019
1 parent f38a1b7 commit d1e4153
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 19 deletions.
55 changes: 53 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
const fs = require('fs');
const path = require('path');
const pify = require('pify');
const semver = require('semver');

const defaults = {
mode: 0o777 & (~process.umask()),
fs
};

const mkdirOptsObj = semver.satisfies(process.version, '>=10.12.0');

// https://github.com/nodejs/node/issues/8987
// https://github.com/libuv/libuv/pull/1088
const checkPath = pth => {
Expand All @@ -22,6 +25,18 @@ const checkPath = pth => {
}
};

const permissionError = pth => {
// This replicates the exception of mkdir with native recusive option when run on
// an invalid drive under Windows.
const error = new Error('operation not permitted, mkdir \'' + pth + '\'');
error.code = 'EPERM';
error.errno = -4048;
error.path = pth;
error.syscall = 'mkdir';

return error;
};

module.exports = (input, options) => Promise.resolve().then(() => {
checkPath(input);
options = Object.assign({}, defaults, options);
Expand All @@ -30,12 +45,29 @@ module.exports = (input, options) => Promise.resolve().then(() => {
const mkdir = pify(options.fs.mkdir);
const stat = pify(options.fs.stat);

if (mkdirOptsObj && options.fs.mkdir === fs.mkdir) {
const pth = path.resolve(input);

return mkdir(pth, {
mode: options.mode,
recursive: true
}).then(() => pth);
}

const make = pth => {
return mkdir(pth, options.mode)
.then(() => pth)
.catch(error => {
if (error.code === 'EPERM') {
throw error;
}

if (error.code === 'ENOENT') {
if (error.message.includes('null bytes') || path.dirname(pth) === pth) {
if (path.dirname(pth) === pth) {
throw permissionError(pth);
}

if (error.message.includes('null bytes')) {
throw error;
}

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

if (mkdirOptsObj && options.fs.mkdirSync === fs.mkdirSync) {
const pth = path.resolve(input);

fs.mkdirSync(pth, {
mode: options.mode,
recursive: true
});

return pth;
}

const make = pth => {
try {
options.fs.mkdirSync(pth, options.mode);
} catch (error) {
if (error.code === 'EPERM') {
throw error;
}

if (error.code === 'ENOENT') {
if (error.message.includes('null bytes') || path.dirname(pth) === pth) {
if (path.dirname(pth) === pth) {
throw permissionError(pth);
}

if (error.message.includes('null bytes')) {
throw error;
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"file-system"
],
"dependencies": {
"pify": "^4.0.1"
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"devDependencies": {
"ava": "^1.0.1",
Expand Down
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- CI-tested on macOS, Linux, and Windows
- Actively maintained
- Doesn't bundle a CLI
- 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)


## Install
Expand Down Expand Up @@ -104,6 +105,9 @@ Default: `require('fs')`

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

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


## Related

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


## License
Expand Down
31 changes: 23 additions & 8 deletions test/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import test from 'ava';
import tempy from 'tempy';
import gracefulFs from 'graceful-fs';
import {getFixture, assertDir} from './helpers/util';
import {getFixture, assertDir, customFsOpt} from './helpers/util';
import makeDir from '..';

test('main', async t => {
Expand All @@ -13,12 +13,19 @@ test('main', async t => {
assertDir(t, madeDir);
});

test('`fs` option', async t => {
test('`fs` option graceful-fs', async t => {
const dir = getFixture();
await makeDir(dir, {fs: gracefulFs});
assertDir(t, dir);
});

test('`fs` option custom', async t => {
const dir = getFixture();
const madeDir = await makeDir(dir, customFsOpt);
t.true(madeDir.length > 0);
assertDir(t, madeDir);
});

test('`mode` option', async t => {
const dir = getFixture();
const mode = 0o744;
Expand All @@ -43,10 +50,18 @@ test('file exits', async t => {
});

test('root dir', async t => {
const mode = fs.statSync('/').mode & 0o777;
const dir = await makeDir('/');
t.true(dir.length > 0);
assertDir(t, dir, mode);
if (process.platform === 'win32') {
// Do not assume that C: is current drive.
await t.throwsAsync(makeDir('/'), {
code: 'EPERM',
message: /operation not permitted, mkdir '[A-Za-z]:\\'/
});
} else {
const mode = fs.statSync('/').mode & 0o777;
const dir = await makeDir('/');
t.true(dir.length > 0);
assertDir(t, dir, mode);
}
});

test('race two', async t => {
Expand Down Expand Up @@ -99,8 +114,8 @@ if (process.platform === 'win32') {
test('handles non-existent root', async t => {
// We assume the `o:\` drive doesn't exist on Windows
await t.throwsAsync(makeDir('o:\\foo'), {
code: 'ENOENT',
message: /no such file or directory/
code: 'EPERM',
message: /operation not permitted, mkdir/
});
});
}
10 changes: 10 additions & 0 deletions test/helpers/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ export const assertDir = (t, dir, mode = 0o777 & (~process.umask())) => {
t.true(pathType.dirSync(dir));
t.is(fs.statSync(dir).mode & 0o777, mode);
};

/* Using this forces test coverage of legacy method on latest versions of node. */
export const customFsOpt = {
fs: {
mkdir: (...args) => fs.mkdir(...args),
stat: (...args) => fs.stat(...args),
mkdirSync: (...args) => fs.mkdirSync(...args),
statSync: (...args) => fs.statSync(...args)
}
};
33 changes: 25 additions & 8 deletions test/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import test from 'ava';
import tempy from 'tempy';
import gracefulFs from 'graceful-fs';
import {getFixture, assertDir} from './helpers/util';
import {getFixture, assertDir, customFsOpt} from './helpers/util';
import makeDir from '..';

test('main', t => {
Expand All @@ -13,12 +13,19 @@ test('main', t => {
assertDir(t, madeDir);
});

test('`fs` option', t => {
test('`fs` option graceful-fs', t => {
const dir = getFixture();
makeDir.sync(dir, {fs: gracefulFs});
assertDir(t, dir);
});

test('`fs` option custom', t => {
const dir = getFixture();
const madeDir = makeDir.sync(dir, customFsOpt);
t.true(madeDir.length > 0);
assertDir(t, madeDir);
});

test('`mode` option', t => {
const dir = getFixture();
const mode = 0o744;
Expand All @@ -45,10 +52,20 @@ test('file exits', t => {
});

test('root dir', t => {
const mode = fs.statSync('/').mode & 0o777;
const dir = makeDir.sync('/');
t.true(dir.length > 0);
assertDir(t, dir, mode);
if (process.platform === 'win32') {
// Do not assume that C: is current drive.
t.throws(() => {
makeDir.sync('/');
}, {
code: 'EPERM',
message: /operation not permitted, mkdir '[A-Za-z]:\\'/
});
} else {
const mode = fs.statSync('/').mode & 0o777;
const dir = makeDir.sync('/');
t.true(dir.length > 0);
assertDir(t, dir, mode);
}
});

test('race two', t => {
Expand Down Expand Up @@ -83,8 +100,8 @@ if (process.platform === 'win32') {
t.throws(() => {
makeDir.sync('o:\\foo');
}, {
code: 'ENOENT',
message: /no such file or directory/
code: 'EPERM',
message: /operation not permitted, mkdir/
});
});
}

0 comments on commit d1e4153

Please sign in to comment.