From 01f90eb61a19b64ba5089c27e4a3f6324724fa38 Mon Sep 17 00:00:00 2001 From: Mark Riedesel Date: Thu, 13 Aug 2015 22:00:21 -0500 Subject: [PATCH] New: Support copying symlinks & add `followSymlinks` option --- README.md | 5 ++- lib/dest/writeContents/index.js | 8 +++- lib/dest/writeContents/writeSymbolicLink.js | 15 +++++++ lib/src/getContents/index.js | 6 +++ lib/src/getContents/readSymbolicLink.js | 18 +++++++++ lib/src/index.js | 5 ++- lib/src/resolveSymlinks.js | 45 +++++++++++---------- test/dest.js | 34 ++++++++++++++++ test/src.js | 36 +++++++++++++++++ 9 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 lib/dest/writeContents/writeSymbolicLink.js create mode 100644 lib/src/getContents/readSymbolicLink.js diff --git a/README.md b/README.md index 0cfc69aa..9cb0e9f9 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ fs.src(['*.js', '!b*.js']) - sourcemaps - `true` or `false` if you want files to have sourcemaps enabled. - Default is `false`. - + - followSymlinks - `true` if you want to recursively resolve symlinks to their targets; set to `false` to preserve them as symlinks. + - Default is `true`. + - `false` will make `file.symlink` equal the original symlink's target path. - Any glob-related options are documented in [glob-stream] and [node-glob]. - Returns a Readable stream by default, or a Duplex stream if the `passthrough` option is set to `true`. @@ -99,6 +101,7 @@ _Note:_ UTF-8 BOM will be stripped from all UTF-8 files read with `.src`. - Returns a Readable/Writable stream. - On write the stream will save the [vinyl] File to disk at the folder/cwd specified. - After writing the file to disk, it will be emitted from the stream so you can keep piping these around. +- If the file has a `symlink` attribute specifying a target path, then a symlink will be created. - The file will be modified after being written to this stream: - `cwd`, `base`, and `path` will be overwritten to match the folder. - `stat.mode` will be overwritten if you used a mode parameter. diff --git a/lib/dest/writeContents/index.js b/lib/dest/writeContents/index.js index 24f726e5..206ece6f 100644 --- a/lib/dest/writeContents/index.js +++ b/lib/dest/writeContents/index.js @@ -4,6 +4,7 @@ var fs = require('fs'); var writeDir = require('./writeDir'); var writeStream = require('./writeStream'); var writeBuffer = require('./writeBuffer'); +var writeSymbolicLink = require('./writeSymbolicLink'); function writeContents(writePath, file, cb) { // if directory then mkdirp it @@ -16,6 +17,11 @@ function writeContents(writePath, file, cb) { return writeStream(writePath, file, written); } + // write it as a symlink + if (file.symlink) { + return writeSymbolicLink(writePath, file, written); + } + // write it like normal if (file.isBuffer()) { return writeBuffer(writePath, file, written); @@ -36,7 +42,7 @@ function writeContents(writePath, file, cb) { return complete(err); } - if (!file.stat || typeof file.stat.mode !== 'number') { + if (!file.stat || typeof file.stat.mode !== 'number' || file.symlink) { return complete(); } diff --git a/lib/dest/writeContents/writeSymbolicLink.js b/lib/dest/writeContents/writeSymbolicLink.js new file mode 100644 index 00000000..a6a630e4 --- /dev/null +++ b/lib/dest/writeContents/writeSymbolicLink.js @@ -0,0 +1,15 @@ +'use strict'; + +var fs = require('graceful-fs'); + +function writeSymbolicLink(writePath, file, cb) { + fs.symlink(file.symlink, writePath, function (err) { + if (err && err.code !== 'EEXIST') { + return cb(err); + } + + cb(null, file); + }); +} + +module.exports = writeSymbolicLink; diff --git a/lib/src/getContents/index.js b/lib/src/getContents/index.js index 1bec72fc..53d51302 100644 --- a/lib/src/getContents/index.js +++ b/lib/src/getContents/index.js @@ -2,6 +2,7 @@ var through2 = require('through2'); var readDir = require('./readDir'); +var readSymbolicLink = require('./readSymbolicLink'); var bufferFile = require('./bufferFile'); var streamFile = require('./streamFile'); @@ -12,6 +13,11 @@ function getContents(opt) { return readDir(file, opt, cb); } + // process symbolic links included with `followSymlinks` option + if (file.stat && file.stat.isSymbolicLink()) { + return readSymbolicLink(file, opt, cb); + } + // read and pass full contents if (opt.buffer !== false) { return bufferFile(file, opt, cb); diff --git a/lib/src/getContents/readSymbolicLink.js b/lib/src/getContents/readSymbolicLink.js new file mode 100644 index 00000000..ee342aa6 --- /dev/null +++ b/lib/src/getContents/readSymbolicLink.js @@ -0,0 +1,18 @@ +'use strict'; + +var fs = require('graceful-fs'); + +function readLink(file, opt, cb) { + fs.readlink(file.path, function (err, target) { + if (err) { + return cb(err); + } + + // store the link target path + file.symlink = target; + + return cb(null, file); + }); +} + +module.exports = readLink; diff --git a/lib/src/index.js b/lib/src/index.js index 192163a5..136b8ced 100644 --- a/lib/src/index.js +++ b/lib/src/index.js @@ -22,7 +22,8 @@ function src(glob, opt) { read: true, buffer: true, sourcemaps: false, - passthrough: false + passthrough: false, + followSymlinks: true }, opt); var inputPass; @@ -34,7 +35,7 @@ function src(glob, opt) { var globStream = gs.create(glob, options); var outputStream = globStream - .pipe(resolveSymlinks()) + .pipe(resolveSymlinks(options)) .pipe(through.obj(createFile)); if (options.since != null) { diff --git a/lib/src/resolveSymlinks.js b/lib/src/resolveSymlinks.js index 1b81b30d..450d691c 100644 --- a/lib/src/resolveSymlinks.js +++ b/lib/src/resolveSymlinks.js @@ -4,35 +4,36 @@ var through2 = require('through2'); var fs = require('graceful-fs'); var path = require('path'); -function resolveSymlinks() { - return through2.obj(resolveFile); -} - -// a stat property is exposed on file objects as a (wanted) side effect -function resolveFile(globFile, enc, cb) { - fs.lstat(globFile.path, function (err, stat) { - if (err) { - return cb(err); - } +function resolveSymlinks(options) { - globFile.stat = stat; - - if (!stat.isSymbolicLink()) { - return cb(null, globFile); - } - - fs.realpath(globFile.path, function (err, filePath) { + // a stat property is exposed on file objects as a (wanted) side effect + function resolveFile(globFile, enc, cb) { + fs.lstat(globFile.path, function (err, stat) { if (err) { return cb(err); } - globFile.base = path.dirname(filePath); - globFile.path = filePath; + globFile.stat = stat; - // recurse to get real file stat - resolveFile(globFile, enc, cb); + if (!stat.isSymbolicLink() || !options.followSymlinks) { + return cb(null, globFile); + } + + fs.realpath(globFile.path, function (err, filePath) { + if (err) { + return cb(err); + } + + globFile.base = path.dirname(filePath); + globFile.path = filePath; + + // recurse to get real file stat + resolveFile(globFile, enc, cb); + }); }); - }); + } + + return through2.obj(resolveFile); } module.exports = resolveSymlinks; diff --git a/test/dest.js b/test/dest.js index 9591302d..7b41146c 100644 --- a/test/dest.js +++ b/test/dest.js @@ -903,6 +903,40 @@ describe('dest stream', function() { stream.end(); }); + it('should create symlinks when the `symlink` attribute is set on the file', function (done) { + var inputPath = path.join(__dirname, './fixtures/test-create-dir-symlink'); + var inputBase = path.join(__dirname, './fixtures/'); + var inputRelativeSymlinkPath = 'wow'; + + var expectedPath = path.join(__dirname, './out-fixtures/test-create-dir-symlink'); + + var inputFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: null, //'' + }); + + // `src()` adds this side-effect with `keepSymlinks` option set to false + inputFile.symlink = inputRelativeSymlinkPath; + + var onEnd = function(){ + fs.readlink(buffered[0].path, function (err, link) { + buffered[0].symlink.should.equal(inputFile.symlink); + buffered[0].path.should.equal(expectedPath); + done(); + }); + }; + + var stream = vfs.dest('./out-fixtures/', {cwd: __dirname}); + + var buffered = []; + bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); + stream.pipe(bufferStream); + stream.write(inputFile); + stream.end(); + }); + it('should emit finish event', function(done) { var srcPath = path.join(__dirname, './fixtures/test.coffee'); var stream = vfs.dest('./out-fixtures/', {cwd: __dirname}); diff --git a/test/src.js b/test/src.js index f0ff83d3..2dc3c606 100644 --- a/test/src.js +++ b/test/src.js @@ -383,4 +383,40 @@ describe('source stream', function() { }); }); + it('should preserve file symlinks with followSymlinks option set to false', function (done) { + var sourcePath = path.join(__dirname, './fixtures/test-symlink'); + var expectedPath = sourcePath; + + fs.readlink(sourcePath, function (err, expectedRelativeSymlinkPath) { + if (err) { + throw err; + } + + var stream = vfs.src('./fixtures/test-symlink', {cwd: __dirname, followSymlinks: false}); + stream.on('data', function(file) { + file.path.should.equal(expectedPath); + file.symlink.should.equal(expectedRelativeSymlinkPath); + done(); + }); + }); + }); + + it('should preserve dir symlinks with followSymlinks option set to false', function (done) { + var sourcePath = path.join(__dirname, './fixtures/test-symlink-dir'); + var expectedPath = sourcePath; + + fs.readlink(sourcePath, function (err, expectedRelativeSymlinkPath) { + if (err) { + throw err; + } + + var stream = vfs.src(sourcePath, {cwd: __dirname, followSymlinks: false}); + stream.on('data', function (file) { + file.path.should.equal(expectedPath); + file.symlink.should.equal(expectedRelativeSymlinkPath); + done(); + }); + }); + }); + });