diff --git a/index.js b/index.js index f4cdb46..fabac5c 100644 --- a/index.js +++ b/index.js @@ -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 => { @@ -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); @@ -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; } @@ -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; } diff --git a/package.json b/package.json index fdddaf1..b607b20 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "file-system" ], "dependencies": { - "pify": "^4.0.1" + "pify": "^4.0.1", + "semver": "^5.6.0" }, "devDependencies": { "ava": "^1.0.1", diff --git a/readme.md b/readme.md index 6cf1ec7..be0f104 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -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 @@ -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 diff --git a/test/async.js b/test/async.js index e7279fd..c64cdd0 100644 --- a/test/async.js +++ b/test/async.js @@ -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 => { @@ -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; @@ -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 => { @@ -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/ }); }); } diff --git a/test/helpers/util.js b/test/helpers/util.js index a3be322..b09acf4 100644 --- a/test/helpers/util.js +++ b/test/helpers/util.js @@ -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) + } +}; diff --git a/test/sync.js b/test/sync.js index c1db97b..f57edb0 100644 --- a/test/sync.js +++ b/test/sync.js @@ -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 => { @@ -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; @@ -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 => { @@ -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/ }); }); }