Skip to content

Commit

Permalink
Fix #210 Windows symbolic links requiring admin
Browse files Browse the repository at this point in the history
  • Loading branch information
phated committed Apr 27, 2017
1 parent 2f82adb commit 9648fc6
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 2 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
src: require('./lib/src'),
dest: require('./lib/dest'),
symlink: require('./lib/symlink'),
hardlink: require('./lib/hardlink'),
};
2 changes: 1 addition & 1 deletion lib/dest/write-contents/write-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function writeStream(file, onWritten) {
file.contents.removeListener('error', onComplete);

// TODO: this is doing sync stuff & the callback seems unnecessary
// TODO: do we really want to replace the contents stream or should we use a clone
// TODO: Replace the contents stream or use a clone?
readStream(file, complete);

function complete() {
Expand Down
86 changes: 86 additions & 0 deletions lib/hardlink/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Create a hard-link on the File System
// This function is better suited for operating
// systems such as Windows due to the restrictions
// placed on creating a symlink directory as a User
// with Administrative rights.
'use strict';

var path = require('path');
var fs = require('graceful-fs');
var through2 = require('through2');
var valueOrFunction = require('value-or-function');
var koalas = require('koalas');

var sink = require('../sink');
var prepareWrite = require('../prepare-write');

var boolean = valueOrFunction.boolean;

// When creating a hard-link the type is determined
// automatically, thus the type is not required when
// invoking fs#link(). A hard link only works when the
// paths are on the same file system. However, a hard link
// points to the i-node instead of the filename and supports
// easier dependency management when (link)ing assets such as a
// node_module into a sub directory.
function hardlink(dest, opt) {
if (!opt) {
opt = {};
}

function linkFile(file, enc, callback) {
var srcPath = file.path;

var isRelative = koalas(boolean(opt.relative, file), false);

prepareWrite(dest, file, opt, onPrepare);

function onPrepare(prepareErr) {
if (prepareErr) {
return callback(prepareErr);
}

// This is done inside prepareWrite to use the adjusted file.base property
if (isRelative) {
srcPath = path.relative(file.base, srcPath);
}

// As a hardlink points to the file system i-node
// the idea of relative or absolute does not exist.
// However, to ensure the source and destination are
// linked correctly, the above code is still used.
fs.link(srcPath, file.path, onLink);
}

function onLink(linkErr) {
if (isErrorFatal(linkErr)) {
return callback(linkErr);
}
callback(null, file);
}
}

var stream = through2.obj(opt, linkFile);

// Sink the stream to start flowing
// Do this on nextTick, it will flow at slowest speed of piped streams
process.nextTick(sink(stream));
return stream;
}

function isErrorFatal(err) {
if (!err) {
return false;
}

// TODO: should we check file.flag like .dest()?
if (err.code === 'EEXIST') {
// Handle scenario for file overwrite failures.
return false;
}

// Otherwise, this is a fatal error
return true;
}

module.exports = hardlink;
20 changes: 19 additions & 1 deletion lib/symlink/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

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

var fs = require('graceful-fs');
var through2 = require('through2');
Expand All @@ -12,14 +13,31 @@ var prepareWrite = require('../prepare-write');

var boolean = valueOrFunction.boolean;

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

function symlink(outFolder, opt) {
if (!opt) {
opt = {};
}

// 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, IntelliJ 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 forceJunctionDir = (isWindows && opt.forceJunctionDir === true);

var symDirType = forceJunctionDir ? 'junction' : 'dir';

function linkFile(file, enc, callback) {
var srcPath = file.path;
var symType = (file.isDirectory() ? 'dir' : 'file');
var symType = file.isDirectory() ? symDirType : 'file';
var isRelative = koalas(boolean(opt.relative, file), false);

prepareWrite(outFolder, file, opt, onPrepare);
Expand Down
90 changes: 90 additions & 0 deletions test/src-hardlinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';

var fs = require('graceful-fs');
var expect = require('expect');
var miss = require('mississippi');

var vfs = require('../');

var cleanup = require('./utils/cleanup');
var testConstants = require('./utils/test-constants');
var isWin = require('./utils/is-windows');

var pipe = miss.pipe;
var concat = miss.concat;

var outputBase = testConstants.outputBase;
var inputDirpath = testConstants.inputDirpath;
var outputDirpath = testConstants.outputDirpath;
var linkDirpath = testConstants.linkDirpath;
var linkNestedTarget = testConstants.linkNestedTarget;
var linkNestedFirst = testConstants.linkNestedFirst;
var linkNestedSecond = testConstants.linkNestedSecond;

var clean = cleanup([outputBase]);

describe('.src() with hard symlinks', function() {

beforeEach(clean);
afterEach(clean);

beforeEach(function(done) {
fs.mkdirSync(outputBase);
fs.mkdirSync(outputDirpath);
// Directories cannot be hardlinked on any OS, use a softlink instead
fs.symlinkSync(inputDirpath, linkDirpath, isWin ? 'junction' : 'dir');
// Hardlink the test file to our 'test' directory file
fs.linkSync(linkNestedTarget, linkNestedSecond);
fs.linkSync(linkNestedSecond, linkNestedFirst);
done();
});

it('follows hardlinks correctly', function(done) {
function assert(files) {
expect(files.length).toEqual(1);
// The path should be the link itself
expect(files[0].path).toEqual(linkNestedFirst);
// But the content should be what's in the actual file
expect(files[0].contents.toString()).toEqual('symlink works\n');
// And the stats should not report it as a symlink
expect(files[0].stat.isSymbolicLink()).toEqual(false);
expect(files[0].stat.isFile()).toEqual(true);
}

pipe([
vfs.src(linkNestedFirst),
concat(assert),
], done);
});

it('preserves original file contents', function(done) {
function assert(files) {
expect(files.length).toEqual(1);
// Remove both linked files
fs.unlink(linkNestedFirst);
fs.unlink(linkNestedSecond);
// Content should remain when referenced by 'original'.
expect(files[0].contents.toString()).toEqual('symlink works\n');
}

pipe([
vfs.src(linkNestedTarget),
concat(assert),
], done);
});

it('preserves first hardlinked file contents', function(done) {
function assert(files) {
expect(files.length).toEqual(1);
// Remove one hardlinked file
fs.unlink(linkNestedSecond);
// Check first hardlinked file
expect(files[0].contents.toString()).toEqual('symlink works\n');
}

pipe([
vfs.src(linkNestedFirst),
concat(assert),
], done);
});
});
9 changes: 9 additions & 0 deletions test/utils/test-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ var symlinkPath = path.join(outputBase, './test-symlink');
var symlinkDirpath = path.join(outputBase, './test-symlink-dir');
var symlinkNestedFirst = path.join(outputBase, './test-multi-layer-symlink');
var symlinkNestedSecond = path.join(outputBase, './foo/baz-link.txt');
// Used for hardlink tests
var linkNestedTarget = path.join(inputBase, './foo/bar/baz.txt');
var linkDirpath = path.join(outputBase, './test-link-dir');
var linkNestedFirst = path.join(outputBase, './foo/baz-link-1.txt');
var linkNestedSecond = path.join(outputBase, './foo/baz-link-2.txt');
// Used for contents of files
var contents = 'Hello World!';

Expand Down Expand Up @@ -62,5 +67,9 @@ module.exports = {
symlinkDirpath: symlinkDirpath,
symlinkNestedFirst: symlinkNestedFirst,
symlinkNestedSecond: symlinkNestedSecond,
linkNestedTarget: linkNestedTarget,
linkDirpath: linkDirpath,
linkNestedFirst: linkNestedFirst,
linkNestedSecond: linkNestedSecond,
contents: contents,
};

0 comments on commit 9648fc6

Please sign in to comment.