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

Use native recursive option when available/appropriate #7

Merged
merged 1 commit into from
Feb 4, 2019
Merged
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
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/
});
});
}