From f7c3832a7996893131b955989fdd461db481b169 Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Mon, 1 May 2017 16:48:00 -0500 Subject: [PATCH] Breaking: Default to using Junctions on Windows (fixes #210) (#231) --- README.md | 12 +- lib/dest/write-contents/write-stream.js | 2 +- lib/symlink/index.js | 24 ++- test/symlink.js | 202 +++++++++++++++++++++++- 4 files changed, 234 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f618658..ae107642 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ Returns a stream that accepts [vinyl] `File` objects, create a symbolic link (i. __Note: The file will be modified after being written to this stream.__ - `cwd`, `base`, and `path` will be overwritten to match the folder. +__Note: On Windows, directory links are created using Junctions by default. Use the `useJunctions` option to disable this behavior.__ + #### Options - Values passed to the options must be of the right type, otherwise they will be ignored. @@ -260,7 +262,15 @@ Whether or not the symlink should be relative or absolute. Type: `Boolean` -Default: `false`. +Default: `false` + +##### `options.useJunctions` + +Whether or not a directory symlink should be created as a `junction`. + +Type: `Boolean` + +Default: `true` on Windows, `false` on all other platforms ##### other diff --git a/lib/dest/write-contents/write-stream.js b/lib/dest/write-contents/write-stream.js index 4541b132..8fff529c 100644 --- a/lib/dest/write-contents/write-stream.js +++ b/lib/dest/write-contents/write-stream.js @@ -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() { diff --git a/lib/symlink/index.js b/lib/symlink/index.js index 9b452a69..eaa3200c 100644 --- a/lib/symlink/index.js +++ b/lib/symlink/index.js @@ -1,6 +1,7 @@ 'use strict'; var path = require('path'); +var os = require('os'); var fs = require('graceful-fs'); var through2 = require('through2'); @@ -12,6 +13,8 @@ var prepareWrite = require('../prepare-write'); var boolean = valueOrFunction.boolean; +var isWindows = (os.platform() === 'win32'); + function symlink(outFolder, opt) { if (!opt) { opt = {}; @@ -19,7 +22,24 @@ function symlink(outFolder, opt) { function linkFile(file, enc, callback) { var srcPath = file.path; - var symType = (file.isDirectory() ? 'dir' : 'file'); + + 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, 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 useJunctions = koalas(boolean(opt.useJunctions, file), (isWindows && isDirectory)); + + var symDirType = useJunctions ? 'junction' : 'dir'; + var symType = isDirectory ? symDirType : 'file'; var isRelative = koalas(boolean(opt.relative, file), false); prepareWrite(outFolder, file, opt, onPrepare); @@ -30,7 +50,7 @@ function symlink(outFolder, opt) { } // This is done inside prepareWrite to use the adjusted file.base property - if (isRelative) { + if (isRelative && !useJunctions) { srcPath = path.relative(file.base, srcPath); } diff --git a/test/symlink.js b/test/symlink.js index 982bd71b..9c0dcb6b 100644 --- a/test/symlink.js +++ b/test/symlink.js @@ -225,7 +225,12 @@ describe('symlink stream', function() { ], done); }); - it('creates a link for a directory', function(done) { + it('(*nix) creates a link for a directory', function(done) { + if (isWindows) { + this.skip(); + return; + } + var file = new File({ base: inputBase, path: inputDirpath, @@ -256,7 +261,127 @@ describe('symlink stream', function() { ], done); }); - it('can create relative links for directories', function(done) { + it('(windows) creates a junction for a directory', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: inputBase, + path: inputDirpath, + contents: null, + stat: { + isDirectory: isDirectory, + }, + }); + + function assert(files) { + var stats = fs.statSync(outputDirpath); + var lstats = fs.lstatSync(outputDirpath); + var outputLink = fs.readlinkSync(outputDirpath); + + expect(files.length).toEqual(1); + expect(files).toInclude(file); + expect(files[0].base).toEqual(outputBase, 'base should have changed'); + expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); + // When creating a junction, it seems Windows appends a separator + expect(outputLink).toEqual(inputDirpath + path.sep); + expect(stats.isDirectory()).toEqual(true); + expect(lstats.isDirectory()).toEqual(false); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase), + concat(assert), + ], done); + }); + + it('(windows) options can disable junctions for a directory', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: inputBase, + path: inputDirpath, + contents: null, + stat: { + isDirectory: isDirectory, + }, + }); + + function assert(files) { + var stats = fs.statSync(outputDirpath); + var lstats = fs.lstatSync(outputDirpath); + var outputLink = fs.readlinkSync(outputDirpath); + + expect(files.length).toEqual(1); + expect(files).toInclude(file); + expect(files[0].base).toEqual(outputBase, 'base should have changed'); + expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); + expect(outputLink).toEqual(inputDirpath); + expect(stats.isDirectory()).toEqual(true); + expect(lstats.isDirectory()).toEqual(false); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase, { useJunctions: false }), + concat(assert), + ], done); + }); + + it('(windows) options can disable junctions for a directory (as a function)', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: inputBase, + path: inputDirpath, + contents: null, + stat: { + isDirectory: isDirectory, + }, + }); + + function useJunctions(f) { + expect(f).toExist(); + expect(f).toBe(file); + return false; + } + + function assert(files) { + var stats = fs.statSync(outputDirpath); + var lstats = fs.lstatSync(outputDirpath); + var outputLink = fs.readlinkSync(outputDirpath); + + expect(files.length).toEqual(1); + expect(files).toInclude(file); + expect(files[0].base).toEqual(outputBase, 'base should have changed'); + expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); + expect(outputLink).toEqual(inputDirpath); + expect(stats.isDirectory()).toEqual(true); + expect(lstats.isDirectory()).toEqual(false); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase, { useJunctions: useJunctions }), + concat(assert), + ], done); + }); + + it('(*nix) can create relative links for directories', function(done) { + if (isWindows) { + this.skip(); + return; + } + var file = new File({ base: inputBase, path: inputDirpath, @@ -287,6 +412,79 @@ describe('symlink stream', function() { ], done); }); + it('(windows) relative option is ignored when junctions are used', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: inputBase, + path: inputDirpath, + contents: null, + stat: { + isDirectory: isDirectory, + }, + }); + + function assert(files) { + var stats = fs.statSync(outputDirpath); + var lstats = fs.lstatSync(outputDirpath); + var outputLink = fs.readlinkSync(outputDirpath); + + expect(files.length).toEqual(1); + expect(files).toInclude(file); + expect(files[0].base).toEqual(outputBase, 'base should have changed'); + expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); + // When creating a junction, it seems Windows appends a separator + expect(outputLink).toEqual(inputDirpath + path.sep); + expect(stats.isDirectory()).toEqual(true); + expect(lstats.isDirectory()).toEqual(false); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase, { useJunctions: true, relative: true }), + concat(assert), + ], done); + }); + + it('(windows) can create relative links for directories when junctions are disabled', function(done) { + if (!isWindows) { + this.skip(); + return; + } + + var file = new File({ + base: inputBase, + path: inputDirpath, + contents: null, + stat: { + isDirectory: isDirectory, + }, + }); + + function assert(files) { + var stats = fs.statSync(outputDirpath); + var lstats = fs.lstatSync(outputDirpath); + var outputLink = fs.readlinkSync(outputDirpath); + + expect(files.length).toEqual(1); + expect(files).toInclude(file); + expect(files[0].base).toEqual(outputBase, 'base should have changed'); + expect(files[0].path).toEqual(outputDirpath, 'path should have changed'); + expect(outputLink).toEqual(path.normalize('../fixtures/foo')); + expect(stats.isDirectory()).toEqual(true); + expect(lstats.isDirectory()).toEqual(false); + } + + pipe([ + from.obj([file]), + vfs.symlink(outputBase, { useJunctions: false, relative: true }), + concat(assert), + ], done); + }); + it('uses different modes for files and directories', function(done) { // Changing the mode of a file is not supported by node.js in Windows. if (isWindows) {