Skip to content

Commit

Permalink
fix!: Rework errors surfaced when encountering files or symlinks (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
phated committed Aug 30, 2022
1 parent 93b37eb commit 3fc3dee
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 12 deletions.
71 changes: 67 additions & 4 deletions mkdirp.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ var fs = require('graceful-fs');

var MASK_MODE = parseInt('7777', 8);

// Utility for passing dirpath that was used with `fs.stat`
function stat(dirpath, cb) {
fs.stat(dirpath, onStat);

function onStat(err, stats) {
cb(err, dirpath, stats);
}
}

// Utility for passing dirpath that was used with `fs.lstat`
function lstat(dirpath, cb) {
fs.lstat(dirpath, onStat);

function onStat(err, stats) {
cb(err, dirpath, stats);
}
}

function mkdirp(dirpath, mode, callback) {
if (typeof mode === 'function') {
callback = mode;
Expand All @@ -22,7 +40,7 @@ function mkdirp(dirpath, mode, callback) {

function onMkdir(mkdirErr) {
if (!mkdirErr) {
return fs.stat(dirpath, onStat);
return stat(dirpath, onStat);
}

switch (mkdirErr.code) {
Expand All @@ -31,21 +49,40 @@ function mkdirp(dirpath, mode, callback) {
}

case 'EEXIST': {
return fs.stat(dirpath, onStat);
return stat(dirpath, onStat);
}

case 'ENOTDIR': {
// On ENOTDIR, this will traverse up the tree until it finds something it can stat
return stat(dirpath, onErrorRecurse);
}

default: {
return callback(mkdirErr);
}
}

function onStat(statErr, stats) {
function onErrorRecurse(err, dirpath, stats) {
if (err) {
return stat(path.dirname(dirpath), onErrorRecurse);
}

onStat(err, dirpath, stats);
}

function onStat(statErr, dirpath, stats) {
if (statErr) {
// If we have ENOENT here it might be a symlink,
// so we need to recurse to error with the target file name
if (statErr.code === 'ENOENT') {
return lstat(dirpath, onStat);
}

return callback(statErr);
}

if (!stats.isDirectory()) {
return callback(mkdirErr);
return lstat(dirpath, onNonDirectory);
}

if (!mode) {
Expand All @@ -58,6 +95,32 @@ function mkdirp(dirpath, mode, callback) {

fs.chmod(dirpath, mode, callback);
}

function onNonDirectory(err, dirpath, stats) {
if (err) {
// Just being cautious by bubbling the mkdir error
return callback(mkdirErr);
}

if (stats.isSymbolicLink()) {
return fs.readlink(dirpath, onReadlink);
}

// Trying to readdir will surface the ENOTDIR we want
// TODO: Use `opendir` when we support node >12
fs.readdir(dirpath, callback);
}

function onReadlink(err, link) {
if (err) {
// Just being cautious by bubbling the mkdir error
return callback(mkdirErr);
}

// Trying to readdir will surface the ENOTDIR we want
// TODO: Use `opendir` when we support node >12
fs.readdir(link, callback);
}
}

function onRecurse(recurseErr) {
Expand Down
217 changes: 209 additions & 8 deletions test/mkdirp.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ function suite() {

var mode = '777';

mkdirp(outputDirpath, function (err) {
fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();
expect(createdMode(outputDirpath)).toEqual(expectedDefaultMode());

Expand All @@ -272,16 +272,37 @@ function suite() {
});
});

it('errors with EEXIST if file in path', function (done) {
mkdirp(outputDirpath, function (err) {
it('errors with ENOTDIR if file in path', function (done) {
fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();

fs.writeFile(outputNestedPath, contents, function (err2) {
expect(err2).toBeFalsy();

mkdirp(outputNestedPath, function (err3) {
expect(err3).toBeDefined();
expect(err3.code).toEqual('EEXIST');
expect(err3.code).toEqual('ENOTDIR');
expect(err3.path).toEqual(outputNestedPath);

done();
});
});
});
});

it('errors with ENOTDIR if file in path of nested mkdirp', function (done) {
var nestedPastFile = path.join(outputNestedPath, './bar/baz/');

fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();

fs.writeFile(outputNestedPath, contents, function (err2) {
expect(err2).toBeFalsy();

mkdirp(nestedPastFile, function (err3) {
expect(err3).toBeDefined();
expect(err3.code).toEqual('ENOTDIR');
expect(err3.path).toEqual(outputNestedPath);

done();
});
Expand All @@ -297,7 +318,7 @@ function suite() {

var mode = '777';

mkdirp(outputDirpath, function (err) {
fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();

fs.writeFile(outputNestedPath, contents, function (err2) {
Expand Down Expand Up @@ -327,9 +348,10 @@ function suite() {
});

mkdirp(outputNestedDirpath, function (err) {
fs.mkdir.restore();

expect(err).toBeDefined();

fs.mkdir.restore();
done();
});
});
Expand All @@ -340,9 +362,10 @@ function suite() {
});

mkdirp(outputDirpath, function (err) {
fs.stat.restore();

expect(err).toBeDefined();

fs.stat.restore();
done();
});
});
Expand All @@ -361,14 +384,192 @@ function suite() {
var spy = sinon.spy(fs, 'chmod');

mkdirp(outputDirpath, mode, function (err) {
fs.chmod.restore();

expect(err).toBeFalsy();
expect(spy.callCount).toEqual(0);

fs.chmod.restore();
done();
});
});
});

describe('symlinks', function () {
before(function () {
if (isWindows) {
this.skip();
return;
}
});

it('succeeds with a directory at the target of a symlink', function (done) {
var target = path.join(outputBase, 'target');

fs.mkdir(target, function (err) {
expect(err).toBeFalsy();

fs.symlink(target, outputDirpath, function (err) {
expect(err).toBeFalsy();

mkdirp(outputDirpath, function (err) {
expect(err).toBeFalsy();
expect(createdMode(target)).toBeDefined();

done();
});
});
});
});

it('changes mode of existing directory at the target of a symlink', function (done) {
var target = path.join(outputBase, 'target');

var mode = '777';

fs.mkdir(target, function (err) {
expect(err).toBeFalsy();

fs.symlink(target, outputDirpath, function (err2) {
expect(err2).toBeFalsy();
expect(createdMode(target)).toEqual(expectedDefaultMode());

mkdirp(outputDirpath, mode, function (err3) {
expect(err3).toBeFalsy();
expect(createdMode(target)).toEqual(expectedMode(mode));
done();
});
});
});
});

it('creates nested directories at the target of a symlink', function (done) {
var target = path.join(outputBase, 'target');
var expected = path.join(target, './bar/baz/');

fs.mkdir(target, function (err) {
expect(err).toBeFalsy();

fs.symlink(target, outputDirpath, function (err2) {
expect(err2).toBeFalsy();

mkdirp(outputNestedDirpath, function (err3) {
expect(err3).toBeFalsy();
expect(createdMode(expected)).toBeDefined();
done();
});
});
});
});

it('errors with ENOTDIR if the target of a symlink is a file', function (done) {
var target = path.join(outputBase, 'test.txt');

fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();

fs.writeFile(target, contents, function (err2) {
expect(err2).toBeFalsy();

fs.symlink(target, outputNestedPath, function (err3) {
expect(err3).toBeFalsy();

mkdirp(outputNestedPath, function (err4) {
expect(err4).toBeDefined();
expect(err4.code).toEqual('ENOTDIR');
expect(err4.path).toEqual(target);
done();
});
});
});
});
});

it('errors with ENOTDIR if the target of a symlink is a file in a nested mkdirp', function (done) {
var target = path.join(outputBase, 'test.txt');

fs.writeFile(target, contents, function (err) {
expect(err).toBeFalsy();

fs.symlink(target, outputDirpath, function (err2) {
expect(err2).toBeFalsy();

mkdirp(outputNestedDirpath, function (err3) {
expect(err3).toBeDefined();
expect(err3.code).toEqual('ENOTDIR');
expect(err3.path).toEqual(target);
done();
});
});
});
});

it('errors with ENOENT if the target of a symlink is missing (a.k.a. dangling symlink)', function (done) {
var target = path.join(outputBase, 'dangling-link');

fs.symlink(target, outputDirpath, function (err) {
expect(err).toBeFalsy();

mkdirp(outputDirpath, function (err2) {
expect(err2).toBeDefined();
expect(err2.code).toEqual('ENOENT');
expect(err2.path).toEqual(target);
done();
});
});
});

it('properly surfaces top-level error if lstat fails', function (done) {
var target = path.join(outputBase, 'test.txt');

sinon.stub(fs, 'lstat').callsFake(function (dirpath, cb) {
cb(new Error('boom'));
});

fs.mkdir(outputDirpath, function (err) {
expect(err).toBeFalsy();

fs.writeFile(target, contents, function (err2) {
expect(err2).toBeFalsy();

fs.symlink(target, outputNestedPath, function (err3) {
expect(err3).toBeFalsy();

mkdirp(outputNestedPath, function (err4) {
fs.lstat.restore();

expect(err4).toBeDefined();
expect(err4.code).toEqual('EEXIST');
expect(err4.path).toEqual(outputNestedPath);

done();
});
});
});
});
});

it('properly surfaces top-level error if readlink fails', function (done) {
var target = path.join(outputBase, 'target');

sinon.stub(fs, 'readlink').callsFake(function (dirpath, cb) {
cb(new Error('boom'));
});

fs.symlink(target, outputDirpath, function (err) {
expect(err).toBeFalsy();

mkdirp(outputDirpath, function (err2) {
fs.readlink.restore();

expect(err2).toBeDefined();
expect(err2.code).toEqual('EEXIST');
expect(err2.path).toEqual(outputDirpath);

done();
});
});
});
});
}

describe('mkdirp', suite);
Expand Down

0 comments on commit 3fc3dee

Please sign in to comment.