Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symlink/Junction improvements #254

Merged
merged 3 commits into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,12 @@ Default: `false`
##### `options.useJunctions`

When creating a symlink, whether or not a directory symlink should be created as a `junction`.
This option is only relevant on Windows and ignored elsewhere. Please refer to
the caveats section below.

Type: `Boolean`

Default: `true` on Windows, `false` on all other platforms
Default: `true`

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

Expand All @@ -239,14 +241,6 @@ Type: `String`

Default: `process.cwd()`

##### `options.mode`

The mode the symlinks should be created with.

Type: `Number`

Default: The `mode` of the input file (`file.stat.mode`) if any, or the process mode if the input file has no `mode` property.

##### `options.dirMode`

The mode the directory should be created with.
Expand Down Expand Up @@ -274,10 +268,37 @@ Default: `false`
##### `options.useJunctions`

Whether or not a directory symlink should be created as a `junction`.
This option is only relevant on Windows and ignored elsewhere. Please refer to
the caveats section below.

Type: `Boolean`

Default: `true` on Windows, `false` on all other platforms
Default: `true`

#### Symbolic links on Windows

When creating symbolic links on Windows, we pass a `type` argument to Node's
`fs` module which specifies the kind of target we link to (one of `'file'`,
`'dir'` or `'junction'`). Specifically, this will be `'file'` when the target
is a regular file, `'junction'` if the target is a directory, or `'dir'` if
the target is a directory and the user overrides the `useJunctions` option
default.

However if the user tries to make a "dangling" link, pointing to a non-existent
target, we won't be able to determine automatically which type we should use.
In these cases, `vinyl-fs` will behave slightly differently depending on
whether the dangling link is being created via `symlink()` or via `dest()`.

For dangling links created via `symlink()`, the incoming vinyl represents the
target and so we will look to its stats to guess the desired type. In
particular, if `isDirectory()` returns false then we'll create a `'file'` type
link, otherwise we will create a `'junction'` or a `'dir'` type link depending
on the value of the `useJunctions` option.

For dangling links created via `dest()`, the incoming vinyl represents the link,
typically read off disk via `src()` with the `resolveSymlinks` option set to
false. In this case we won't be able to make any reasonable guess as to the
type of link and we default to using `'file'`.

[glob-stream]: https://github.com/gulpjs/glob-stream
[node-glob]: https://github.com/isaacs/node-glob
Expand Down
13 changes: 5 additions & 8 deletions lib/dest/options.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
'use strict';

var os = require('os');

var isWindows = (os.platform() === 'win32');

var config = {
cwd: {
type: 'string',
Expand Down Expand Up @@ -34,14 +30,15 @@ var config = {
default: false,
},
// Symlink options
useJunctions: {
type: 'boolean',
default: isWindows,
},
relativeSymlinks: {
type: 'boolean',
default: false,
},
// This option is ignored on non-Windows platforms
useJunctions: {
type: 'boolean',
default: true,
},
};

module.exports = config;
11 changes: 6 additions & 5 deletions lib/dest/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ function prepareWrite(folderResolver, optResolver) {
}

function normalize(file, enc, cb) {
var mode = optResolver.resolve('mode', file);
var cwd = path.resolve(optResolver.resolve('cwd', file));

var outFolderPath = folderResolver.resolve('outFolder', file);
if (!outFolderPath) {
return cb(new Error('Invalid output folder'));
}
var cwd = path.resolve(optResolver.resolve('cwd', file));
var basePath = path.resolve(cwd, outFolderPath);
var writePath = path.resolve(basePath, file.relative);

// Wire up new properties
file.stat = (file.stat || new fs.Stats());
file.stat.mode = mode;
file.cwd = cwd;
file.base = basePath;
file.path = writePath;
if (!file.isSymbolic()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to document this in the options

var mode = optResolver.resolve('mode', file);
file.stat = (file.stat || new fs.Stats());
file.stat.mode = mode;
}

cb(null, file);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/dest/write-contents/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function writeContents(optResolver) {

function writeFile(file, enc, callback) {
// Write it as a symlink
if (file.symlink) {
if (file.isSymbolic()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the documents talk about .symlink but if so, it'll need to be updated

return writeSymbolicLink(file, optResolver, onWritten);
}

Expand Down
84 changes: 58 additions & 26 deletions lib/dest/write-contents/write-symbolic-link.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,74 @@
'use strict';

var os = require('os');
var path = require('path');

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

var isWindows = (os.platform() === 'win32');

function writeSymbolicLink(file, optResolver, onWritten) {
var isDirectory = file.isDirectory();

// This option provides a way to create a Junction instead of a
// Directory symlink on Windows. This comes with the following caveats:
// * NTFS Junctions cannot be relative.
// * NTFS Junctions MUST be directories.
// * NTFS Junctions must be on the same file system.
// * Most products CANNOT detect a directory is a Junction:
// This has the side effect of possibly having a whole directory
// deleted when a product is deleting the Junction directory.
// For example, JetBrains product lines will delete the entire
// contents of the TARGET directory because the product does not
// realize it's a symlink as the JVM and Node return false for isSymlink.
var useJunctions = optResolver.resolve('useJunctions', file);

var symDirType = useJunctions ? 'junction' : 'dir';
var symType = isDirectory ? symDirType : 'file';
if (!file.symlink) {
return onWritten(new Error('Missing symlink property on symbolic vinyl'));
}

var isRelative = optResolver.resolve('relativeSymlinks', file);
var flag = optResolver.resolve('flag', file);

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

var flag = optResolver.resolve('flag', file);
fo.reflectStat(file.symlink, file, onReflect);

function onReflect(statErr) {
if (statErr && statErr.code !== 'ENOENT') {
return onWritten(statErr);
}

// This option provides a way to create a Junction instead of a
// Directory symlink on Windows. This comes with the following caveats:
// * NTFS Junctions cannot be relative.
// * NTFS Junctions MUST be directories.
// * NTFS Junctions must be on the same file system.
// * Most products CANNOT detect a directory is a Junction:
// This has the side effect of possibly having a whole directory
// deleted when a product is deleting the Junction directory.
// For example, JetBrains product lines will delete the entire contents
// of the TARGET directory because the product does not realize it's
// a symlink as the JVM and Node return false for isSymlink.

var opts = {
flag: flag,
type: symType,
};
// This function is Windows only, so we don't need to check again
var useJunctions = optResolver.resolve('useJunctions', file);

fo.symlink(file.symlink, file.path, opts, onWritten);
var dirType = useJunctions ? 'junction' : 'dir';
// Dangling links are always 'file'
var type = !statErr && file.isDirectory() ? dirType : 'file';

createLinkWithType(type);
}

function createLinkWithType(type) {
// This is done after prepare() to use the adjusted file.base property
if (isRelative && type !== 'junction') {
file.symlink = path.relative(file.base, file.symlink);
}

var opts = {
flag: flag,
type: type,
};
fo.symlink(file.symlink, file.path, opts, onSymlink);

function onSymlink(symlinkErr) {
if (symlinkErr) {
return onWritten(symlinkErr);
}

fo.reflectLinkStat(file.path, file, onWritten);
}
}
}

module.exports = writeSymbolicLink;
30 changes: 30 additions & 0 deletions lib/file-operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ function isOwner(fsStat) {
return true;
}

function reflectStat(path, file, callback) {
// Set file.stat to the reflect current state on disk
fs.stat(path, onStat);

function onStat(statErr, stat) {
if (statErr) {
return callback(statErr);
}

file.stat = stat;
callback();
}
}

function reflectLinkStat(path, file, callback) {
// Set file.stat to the reflect current state on disk
fs.lstat(path, onLstat);

function onLstat(lstatErr, stat) {
if (lstatErr) {
return callback(lstatErr);
}

file.stat = stat;
callback();
}
}

function updateMetadata(fd, file, callback) {

fs.fstat(fd, onStat);
Expand Down Expand Up @@ -413,6 +441,8 @@ module.exports = {
getTimesDiff: getTimesDiff,
getOwnerDiff: getOwnerDiff,
isOwner: isOwner,
reflectStat: reflectStat,
reflectLinkStat: reflectLinkStat,
updateMetadata: updateMetadata,
symlink: symlink,
writeFile: writeFile,
Expand Down
14 changes: 6 additions & 8 deletions lib/src/resolve-symlinks.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
'use strict';

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

function resolveSymlinks(optResolver) {

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

fs.lstat(file.path, onStat);
fo.reflectLinkStat(file.path, file, onReflect);

function onStat(statErr, stat) {
function onReflect(statErr) {
if (statErr) {
return callback(statErr);
}

file.stat = stat;

if (!stat.isSymbolicLink()) {
if (!file.stat.isSymbolicLink()) {
return callback(null, file);
}

Expand All @@ -27,8 +25,8 @@ function resolveSymlinks(optResolver) {
return callback(null, file);
}

// Recurse to get real file stat
fs.stat(file.path, onStat);
// Get target's stats
fo.reflectStat(file.path, file, onReflect);
}
}

Expand Down
Loading