Skip to content

Commit 4b15227

Browse files
erikkempermanphated
authored andcommitted
Breaking: Improve symlink/junction behaviour
1 parent e0e4b4b commit 4b15227

19 files changed

+347
-191
lines changed

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,11 @@ Default: `false`
211211
##### `options.useJunctions`
212212

213213
When creating a symlink, whether or not a directory symlink should be created as a `junction`.
214+
This option is only relevant on Windows and ignored elsewhere.
214215

215216
Type: `Boolean`
216217

217-
Default: `true` on Windows, `false` on all other platforms
218+
Default: `true`
218219

219220
### `symlink(folder[, options])`
220221

@@ -239,14 +240,6 @@ Type: `String`
239240

240241
Default: `process.cwd()`
241242

242-
##### `options.mode`
243-
244-
The mode the symlinks should be created with.
245-
246-
Type: `Number`
247-
248-
Default: The `mode` of the input file (`file.stat.mode`) if any, or the process mode if the input file has no `mode` property.
249-
250243
##### `options.dirMode`
251244

252245
The mode the directory should be created with.
@@ -274,10 +267,11 @@ Default: `false`
274267
##### `options.useJunctions`
275268

276269
Whether or not a directory symlink should be created as a `junction`.
270+
This option is only relevant on Windows and ignored elsewhere.
277271

278272
Type: `Boolean`
279273

280-
Default: `true` on Windows, `false` on all other platforms
274+
Default: `true`
281275

282276
[glob-stream]: https://github.com/gulpjs/glob-stream
283277
[node-glob]: https://github.com/isaacs/node-glob

lib/dest/options.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
'use strict';
22

3-
var os = require('os');
4-
5-
var isWindows = (os.platform() === 'win32');
6-
73
var config = {
84
cwd: {
95
type: 'string',
@@ -34,14 +30,15 @@ var config = {
3430
default: false,
3531
},
3632
// Symlink options
37-
useJunctions: {
38-
type: 'boolean',
39-
default: isWindows,
40-
},
4133
relativeSymlinks: {
4234
type: 'boolean',
4335
default: false,
4436
},
37+
// This option is ignored on non-Windows platforms
38+
useJunctions: {
39+
type: 'boolean',
40+
default: true,
41+
},
4542
};
4643

4744
module.exports = config;

lib/dest/prepare.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@ function prepareWrite(folderResolver, optResolver) {
1111
}
1212

1313
function normalize(file, enc, cb) {
14-
var mode = optResolver.resolve('mode', file);
15-
var cwd = path.resolve(optResolver.resolve('cwd', file));
16-
1714
var outFolderPath = folderResolver.resolve('outFolder', file);
1815
if (!outFolderPath) {
1916
return cb(new Error('Invalid output folder'));
2017
}
18+
var cwd = path.resolve(optResolver.resolve('cwd', file));
2119
var basePath = path.resolve(cwd, outFolderPath);
2220
var writePath = path.resolve(basePath, file.relative);
2321

2422
// Wire up new properties
25-
file.stat = (file.stat || new fs.Stats());
26-
file.stat.mode = mode;
2723
file.cwd = cwd;
2824
file.base = basePath;
2925
file.path = writePath;
26+
if (!file.isSymbolic()) {
27+
var mode = optResolver.resolve('mode', file);
28+
file.stat = (file.stat || new fs.Stats());
29+
file.stat.mode = mode;
30+
}
3031

3132
cb(null, file);
3233
}
Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,74 @@
11
'use strict';
22

3+
var os = require('os');
34
var path = require('path');
45

56
var fo = require('../../file-operations');
67

8+
var isWindows = (os.platform() === 'win32');
9+
710
function writeSymbolicLink(file, optResolver, onWritten) {
8-
var isDirectory = file.isDirectory();
9-
10-
// This option provides a way to create a Junction instead of a
11-
// Directory symlink on Windows. This comes with the following caveats:
12-
// * NTFS Junctions cannot be relative.
13-
// * NTFS Junctions MUST be directories.
14-
// * NTFS Junctions must be on the same file system.
15-
// * Most products CANNOT detect a directory is a Junction:
16-
// This has the side effect of possibly having a whole directory
17-
// deleted when a product is deleting the Junction directory.
18-
// For example, JetBrains product lines will delete the entire
19-
// contents of the TARGET directory because the product does not
20-
// realize it's a symlink as the JVM and Node return false for isSymlink.
21-
var useJunctions = optResolver.resolve('useJunctions', file);
22-
23-
var symDirType = useJunctions ? 'junction' : 'dir';
24-
var symType = isDirectory ? symDirType : 'file';
11+
if (!file.symlink) {
12+
return onWritten(new Error('Missing symlink property on symbolic vinyl'));
13+
}
14+
2515
var isRelative = optResolver.resolve('relativeSymlinks', file);
16+
var flag = optResolver.resolve('flag', file);
2617

27-
// This is done after prepare() to use the adjusted file.base property
28-
if (isRelative && symType !== 'junction') {
29-
file.symlink = path.relative(file.base, file.symlink);
18+
if (!isWindows) {
19+
// On non-Windows, just use 'file'
20+
return createLinkWithType('file');
3021
}
3122

32-
var flag = optResolver.resolve('flag', file);
23+
fo.reflectStat(file.symlink, file, onReflect);
24+
25+
function onReflect(statErr) {
26+
if (statErr && statErr.code !== 'ENOENT') {
27+
return onWritten(statErr);
28+
}
29+
30+
// This option provides a way to create a Junction instead of a
31+
// Directory symlink on Windows. This comes with the following caveats:
32+
// * NTFS Junctions cannot be relative.
33+
// * NTFS Junctions MUST be directories.
34+
// * NTFS Junctions must be on the same file system.
35+
// * Most products CANNOT detect a directory is a Junction:
36+
// This has the side effect of possibly having a whole directory
37+
// deleted when a product is deleting the Junction directory.
38+
// For example, JetBrains product lines will delete the entire contents
39+
// of the TARGET directory because the product does not realize it's
40+
// a symlink as the JVM and Node return false for isSymlink.
3341

34-
var opts = {
35-
flag: flag,
36-
type: symType,
37-
};
42+
// This function is Windows only, so we don't need to check again
43+
var useJunctions = optResolver.resolve('useJunctions', file);
3844

39-
fo.symlink(file.symlink, file.path, opts, onWritten);
45+
var dirType = useJunctions ? 'junction' : 'dir';
46+
// Dangling links are always 'file'
47+
var type = !statErr && file.isDirectory() ? dirType : 'file';
48+
49+
createLinkWithType(type);
50+
}
51+
52+
function createLinkWithType(type) {
53+
// This is done after prepare() to use the adjusted file.base property
54+
if (isRelative && type !== 'junction') {
55+
file.symlink = path.relative(file.base, file.symlink);
56+
}
57+
58+
var opts = {
59+
flag: flag,
60+
type: type,
61+
};
62+
fo.symlink(file.symlink, file.path, opts, onSymlink);
63+
64+
function onSymlink(symlinkErr) {
65+
if (symlinkErr) {
66+
return onWritten(symlinkErr);
67+
}
68+
69+
fo.reflectLinkStat(file.path, file, onWritten);
70+
}
71+
}
4072
}
4173

4274
module.exports = writeSymbolicLink;

lib/file-operations.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,34 @@ function isOwner(fsStat) {
155155
return true;
156156
}
157157

158+
function reflectStat(path, file, callback) {
159+
// Set file.stat to the reflect current state on disk
160+
fs.stat(path, onStat);
161+
162+
function onStat(statErr, stat) {
163+
if (statErr) {
164+
return callback(statErr);
165+
}
166+
167+
file.stat = stat;
168+
callback();
169+
}
170+
}
171+
172+
function reflectLinkStat(path, file, callback) {
173+
// Set file.stat to the reflect current state on disk
174+
fs.lstat(path, onLstat);
175+
176+
function onLstat(lstatErr, stat) {
177+
if (lstatErr) {
178+
return callback(lstatErr);
179+
}
180+
181+
file.stat = stat;
182+
callback();
183+
}
184+
}
185+
158186
function updateMetadata(fd, file, callback) {
159187

160188
fs.fstat(fd, onStat);
@@ -413,6 +441,8 @@ module.exports = {
413441
getTimesDiff: getTimesDiff,
414442
getOwnerDiff: getOwnerDiff,
415443
isOwner: isOwner,
444+
reflectStat: reflectStat,
445+
reflectLinkStat: reflectLinkStat,
416446
updateMetadata: updateMetadata,
417447
symlink: symlink,
418448
writeFile: writeFile,

lib/src/resolve-symlinks.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
'use strict';
22

33
var through = require('through2');
4-
var fs = require('graceful-fs');
4+
var fo = require('../file-operations');
55

66
function resolveSymlinks(optResolver) {
77

88
// A stat property is exposed on file objects as a (wanted) side effect
99
function resolveFile(file, enc, callback) {
1010

11-
fs.lstat(file.path, onStat);
11+
fo.reflectLinkStat(file.path, file, onReflect);
1212

13-
function onStat(statErr, stat) {
13+
function onReflect(statErr) {
1414
if (statErr) {
1515
return callback(statErr);
1616
}
1717

18-
file.stat = stat;
19-
20-
if (!stat.isSymbolicLink()) {
18+
if (!file.stat.isSymbolicLink()) {
2119
return callback(null, file);
2220
}
2321

@@ -27,8 +25,8 @@ function resolveSymlinks(optResolver) {
2725
return callback(null, file);
2826
}
2927

30-
// Recurse to get real file stat
31-
fs.stat(file.path, onStat);
28+
// Get target's stats
29+
fo.reflectStat(file.path, file, onReflect);
3230
}
3331
}
3432

lib/symlink/link-file.js

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,81 @@
11
'use strict';
22

3+
var os = require('os');
34
var path = require('path');
45

56
var through = require('through2');
67

78
var fo = require('../file-operations');
89

10+
var isWindows = (os.platform() === 'win32');
11+
912
function linkStream(optResolver) {
1013

1114
function linkFile(file, enc, callback) {
12-
var isDirectory = file.isDirectory();
13-
14-
// This option provides a way to create a Junction instead of a
15-
// Directory symlink on Windows. This comes with the following caveats:
16-
// * NTFS Junctions cannot be relative.
17-
// * NTFS Junctions MUST be directories.
18-
// * NTFS Junctions must be on the same file system.
19-
// * Most products CANNOT detect a directory is a Junction:
20-
// This has the side effect of possibly having a whole directory
21-
// deleted when a product is deleting the Junction directory.
22-
// For example, JetBrains product lines will delete the entire
23-
// contents of the TARGET directory because the product does not
24-
// realize it's a symlink as the JVM and Node return false for isSymlink.
25-
var useJunctions = optResolver.resolve('useJunctions', file);
26-
27-
var symDirType = useJunctions ? 'junction' : 'dir';
28-
var symType = isDirectory ? symDirType : 'file';
2915
var isRelative = optResolver.resolve('relativeSymlinks', file);
16+
var flag = optResolver.resolve('flag', file);
3017

31-
// This is done after prepare() to use the adjusted file.base property
32-
if (isRelative && symType !== 'junction') {
33-
file.symlink = path.relative(file.base, file.symlink);
18+
if (!isWindows) {
19+
// On non-Windows, just use 'file'
20+
return createLinkWithType('file');
3421
}
3522

36-
var flag = optResolver.resolve('flag', file);
23+
fo.reflectStat(file.symlink, file, onReflectTarget);
24+
25+
function onReflectTarget(statErr) {
26+
if (statErr && statErr.code !== 'ENOENT') {
27+
return onWritten(statErr);
28+
}
29+
// If target doesn't exist, the vinyl will still carry the target stats.
30+
// Let's use those to determine which kind of dangling link to create.
31+
32+
// This option provides a way to create a Junction instead of a
33+
// Directory symlink on Windows. This comes with the following caveats:
34+
// * NTFS Junctions cannot be relative.
35+
// * NTFS Junctions MUST be directories.
36+
// * NTFS Junctions must be on the same file system.
37+
// * Most products CANNOT detect a directory is a Junction:
38+
// This has the side effect of possibly having a whole directory
39+
// deleted when a product is deleting the Junction directory.
40+
// For example, JetBrains product lines will delete the entire contents
41+
// of the TARGET directory because the product does not realize it's
42+
// a symlink as the JVM and Node return false for isSymlink.
43+
44+
// This function is Windows only, so we don't need to check again
45+
var useJunctions = optResolver.resolve('useJunctions', file);
46+
47+
var dirType = useJunctions ? 'junction' : 'dir';
48+
var type = !statErr && file.isDirectory() ? dirType : 'file';
3749

38-
var opts = {
39-
flag: flag,
40-
type: symType,
41-
};
50+
createLinkWithType(type);
51+
}
52+
53+
function createLinkWithType(type) {
54+
// This is done after prepare() to use the adjusted file.base property
55+
if (isRelative && type !== 'junction') {
56+
file.symlink = path.relative(file.base, file.symlink);
57+
}
4258

43-
fo.symlink(file.symlink, file.path, opts, onSymlink);
59+
var opts = {
60+
flag: flag,
61+
type: type,
62+
};
63+
fo.symlink(file.symlink, file.path, opts, onSymlink);
64+
}
4465

4566
function onSymlink(symlinkErr) {
4667
if (symlinkErr) {
4768
return callback(symlinkErr);
4869
}
4970

71+
fo.reflectLinkStat(file.path, file, onReflectLink);
72+
}
73+
74+
function onReflectLink(reflectErr) {
75+
if (reflectErr) {
76+
return callback(reflectErr);
77+
}
78+
5079
callback(null, file);
5180
}
5281
}

0 commit comments

Comments
 (0)