- Operating System: macOS 14.2.1
- Node.js version: v20.11.1
fs-extra version: 11.2.0
Issue
- create a target directory with a file in it
- create a symbolic link from somewhere else to the target directory
- do this a second time
This works if the reference to the target is an absolute path, but fails if the reference is a relative path.
Example
This is a mocha test that shows the issue. The first test ensures the symbolic link a second time with an absolute path, which succeeds as expected. The second test ensures the symbolic link a second time with a relative path, which is rejected with ENOENT, but should succeed too.
/* eslint-env mocha */
require('should')
const { resolve, join, relative, dirname } = require('node:path')
const { ensureFile, remove, pathExists, ensureSymlink } = require('fs-extra')
const testBaseDirectory = resolve('fs-extra-test-base-directory')
describe('fs-extra ensureSymlink fails when ensuring a symbolic link with a relative path if it already exists', function () {
beforeEach(async function () {
// a directory with a file, as `destination` or `target`
this.targetDirectory = join(testBaseDirectory, 'target-directory')
const targetFileName = 'target-file'
this.targetDirectoryFile = join(this.targetDirectory, targetFileName)
await ensureFile(this.targetDirectoryFile)
// a directory to put the symbolic link in (the `source`)
this.linkDirectory = join(testBaseDirectory, 'link-directory')
this.symbolicLinkPath = join(this.linkDirectory, 'link')
this.targetFileViaSymbolicLink = join(this.symbolicLinkPath, targetFileName)
this.relativeSymbolicLinkReference = relative(dirname(this.symbolicLinkPath), this.targetDirectory)
})
afterEach(async function () {
return remove(testBaseDirectory)
})
it('can ensure a symbolic link a second time with an absolute path', async function () {
await pathExists(this.targetDirectoryFile).should.be.resolvedWith(true)
// first time, setting up with a relative reference
await ensureSymlink(this.relativeSymbolicLinkReference, this.symbolicLinkPath, 'dir').should.be.resolved()
await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true)
await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true)
// second time, setting up with an absolute reference
await ensureSymlink(this.targetDirectory, this.symbolicLinkPath, 'dir').should.be.resolved()
await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true)
await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true)
})
it('can ensure a symbolic link a second time with a relative path', async function () {
await pathExists(this.targetDirectoryFile).should.be.resolvedWith(true)
// first time, setting up with a relative reference
await ensureSymlink(this.relativeSymbolicLinkReference, this.symbolicLinkPath, 'dir').should.be.resolved()
await pathExists(this.symbolicLinkPath).should.be.resolvedWith(true)
await pathExists(this.targetFileViaSymbolicLink).should.be.resolvedWith(true)
// second time, setting up with a relative reference SHOULD ALSO RESOLVE, BUT REJECTS
const error = await ensureSymlink(
this.relativeSymbolicLinkReference,
this.symbolicLinkPath,
'dir'
).should.be.rejected()
error.code.should.equal('ENOENT')
// YET THE TARGET FILE EXISTS VIA THE ABSOLUTE PATH
await pathExists(this.targetDirectory).should.be.resolvedWith(true)
// AND THE RELATIVE PATH RESOLVES TO THE ABSOLUTE PATH
join(dirname(this.symbolicLinkPath), this.relativeSymbolicLinkReference).should.equal(this.targetDirectory)
})
})
Analysis
The issue is clear in fs-extra/lib/ensure/symlink.js, line 24, versus line 32.
When there is no symbolic link yet at dstpath, the if of line 22 is skipped
let stats
try {
stats = await fs.lstat(dstpath)
} catch { }
if (stats && stats.isSymbolicLink()) {
…
}
and we arrive at line 31—32 where work is done to deal with relative srcpaths:
const relative = await symlinkPaths(srcpath, dstpath)
srcpath = relative.toDst
When there is a symbolic link at dstpath, the if–branch at line 22 is executed. Here, the status of the srcpath is requested as is:
This evaluates a relative srcpath relative to the cwd, not to the dstpath. At that location the source does not exist, which results in ENOENT.
fs-extraversion: 11.2.0Issue
This works if the reference to the
targetis an absolute path, but fails if the reference is a relative path.Example
This is a
mochatest that shows the issue. The first test ensures the symbolic link a second time with an absolute path, which succeeds as expected. The second test ensures the symbolic link a second time with a relative path, which is rejected withENOENT, but should succeed too.Analysis
The issue is clear in
fs-extra/lib/ensure/symlink.js, line 24, versus line 32.When there is no symbolic link yet at
dstpath, theifof line 22 is skippedand we arrive at line 31—32 where work is done to deal with relative
srcpaths:When there is a symbolic link at
dstpath, theif–branch at line 22 is executed. Here, the status of thesrcpathis requested as is:This evaluates a relative
srcpathrelative to thecwd, not to thedstpath. At that location the source does not exist, which results inENOENT.