Skip to content

Commit cda6b20

Browse files
bzozTrott
authored andcommitted
win, fs: detect if symlink target is a directory
On Windows creating a symlink to a directory will not work unless extra 'dir' parameter is passed. This adds a check if link target is a directory, and if so automatically use 'dir' when creating symlink. PR-URL: #23724 Refs: #23691 Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Rod Vagg <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 83d6cb9 commit cda6b20

File tree

3 files changed

+95
-8
lines changed

3 files changed

+95
-8
lines changed

doc/api/fs.md

+17-7
Original file line numberDiff line numberDiff line change
@@ -3028,20 +3028,26 @@ changes:
30283028
description: The `target` and `path` parameters can be WHATWG `URL` objects
30293029
using `file:` protocol. Support is currently still
30303030
*experimental*.
3031+
- version: REPLACEME
3032+
pr-url: https://github.com/nodejs/node/pull/23724
3033+
description: If the `type` argument is left undefined, Node will autodetect
3034+
`target` type and automatically select `dir` or `file`
30313035
-->
30323036

30333037
* `target` {string|Buffer|URL}
30343038
* `path` {string|Buffer|URL}
3035-
* `type` {string} **Default:** `'file'`
3039+
* `type` {string}
30363040
* `callback` {Function}
30373041
* `err` {Error}
30383042

30393043
Asynchronous symlink(2). No arguments other than a possible exception are given
3040-
to the completion callback. The `type` argument can be set to `'dir'`,
3041-
`'file'`, or `'junction'` and is only available on
3042-
Windows (ignored on other platforms). Windows junction points require the
3043-
destination path to be absolute. When using `'junction'`, the `target` argument
3044-
will automatically be normalized to absolute path.
3044+
to the completion callback. The `type` argument is only available on Windows
3045+
and ignored on other platforms. It can be set to `'dir'`, `'file'`, or
3046+
`'junction'`. If the `type` argument is not set, Node will autodetect `target`
3047+
type and use `'file'` or `'dir'`. If the `target` does not exist, `'file'` will
3048+
be used. Windows junction points require the destination path to be absolute.
3049+
When using `'junction'`, the `target` argument will automatically be normalized
3050+
to absolute path.
30453051

30463052
Here is an example below:
30473053

@@ -3060,11 +3066,15 @@ changes:
30603066
description: The `target` and `path` parameters can be WHATWG `URL` objects
30613067
using `file:` protocol. Support is currently still
30623068
*experimental*.
3069+
- version: REPLACEME
3070+
pr-url: https://github.com/nodejs/node/pull/23724
3071+
description: If the `type` argument is left undefined, Node will autodetect
3072+
`target` type and automatically select `dir` or `file`
30633073
-->
30643074

30653075
* `target` {string|Buffer|URL}
30663076
* `path` {string|Buffer|URL}
3067-
* `type` {string} **Default:** `'file'`
3077+
* `type` {string}
30683078

30693079
Returns `undefined`.
30703080

lib/fs.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -905,16 +905,47 @@ function symlink(target, path, type_, callback_) {
905905
validatePath(target, 'target');
906906
validatePath(path);
907907

908-
const flags = stringToSymlinkType(type);
909908
const req = new FSReqCallback();
910909
req.oncomplete = callback;
911910

911+
if (isWindows && type === null) {
912+
let absoluteTarget;
913+
try {
914+
// Symlinks targets can be relative to the newly created path.
915+
// Calculate absolute file name of the symlink target, and check
916+
// if it is a directory. Ignore resolve error to keep symlink
917+
// errors consistent between platforms if invalid path is
918+
// provided.
919+
absoluteTarget = pathModule.resolve(path, '..', target);
920+
} catch { }
921+
if (absoluteTarget !== undefined) {
922+
stat(absoluteTarget, (err, stat) => {
923+
const resolvedType = !err && stat.isDirectory() ? 'dir' : 'file';
924+
const resolvedFlags = stringToSymlinkType(resolvedType);
925+
binding.symlink(preprocessSymlinkDestination(target,
926+
resolvedType,
927+
path),
928+
pathModule.toNamespacedPath(path), resolvedFlags, req);
929+
});
930+
return;
931+
}
932+
}
933+
934+
const flags = stringToSymlinkType(type);
912935
binding.symlink(preprocessSymlinkDestination(target, type, path),
913936
pathModule.toNamespacedPath(path), flags, req);
914937
}
915938

916939
function symlinkSync(target, path, type) {
917940
type = (typeof type === 'string' ? type : null);
941+
if (isWindows && type === null) {
942+
try {
943+
const absoluteTarget = pathModule.resolve(path, '..', target);
944+
if (statSync(absoluteTarget).isDirectory()) {
945+
type = 'dir';
946+
}
947+
} catch { }
948+
}
918949
target = toPathIfFileURL(target);
919950
path = toPathIfFileURL(path);
920951
validatePath(target, 'target');

test/parallel/test-fs-symlink-dir.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// Test creating a symbolic link pointing to a directory.
5+
// Ref: https://github.com/nodejs/node/pull/23724
6+
// Ref: https://github.com/nodejs/node/issues/23596
7+
8+
9+
if (!common.canCreateSymLink())
10+
common.skip('insufficient privileges');
11+
12+
const assert = require('assert');
13+
const path = require('path');
14+
const fs = require('fs');
15+
16+
const tmpdir = require('../common/tmpdir');
17+
tmpdir.refresh();
18+
19+
const linkTargets = [
20+
'relative-target',
21+
path.join(tmpdir.path, 'absolute-target')
22+
];
23+
const linkPaths = [
24+
path.relative(process.cwd(), path.join(tmpdir.path, 'relative-path')),
25+
path.join(tmpdir.path, 'absolute-path')
26+
];
27+
28+
function testSync(target, path) {
29+
fs.symlinkSync(target, path);
30+
fs.readdirSync(path);
31+
}
32+
33+
function testAsync(target, path) {
34+
fs.symlink(target, path, common.mustCall((err) => {
35+
assert.ifError(err);
36+
fs.readdirSync(path);
37+
}));
38+
}
39+
40+
for (const linkTarget of linkTargets) {
41+
fs.mkdirSync(path.resolve(tmpdir.path, linkTarget));
42+
for (const linkPath of linkPaths) {
43+
testSync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-sync`);
44+
testAsync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-async`);
45+
}
46+
}

0 commit comments

Comments
 (0)