Skip to content

Commit 9f5f6e8

Browse files
committed
Add symlink copy with followSymlinks option
The current behavior is always to resolve symbolic links to actual files. This change introduces a `followSymlinks` option to `src()` (default: `true`). When the option is set to `false`, then each encountered symlink is stored in `file.symlink`. When `dest()` is invoked, the `symlink` value is used to reconstruct an identical symlink. It is important to note that the symbolic link path remains completely unmodified, so care should be taken when sourcing symlinks with absolute paths. This patch also includes related unit tests
1 parent fa184c4 commit 9f5f6e8

File tree

9 files changed

+146
-25
lines changed

9 files changed

+146
-25
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ fs.src(['*.js', '!b*.js'])
6666
- Default is `false`.
6767
- sourcemaps - `true` or `false` if you want files to have sourcemaps enabled.
6868
- Default is `false`.
69+
- followSymlinks - `true` if you want to recursively resolve symlinks to their targets; set to `false` to preserve them as symlinks.
70+
- Default is `true`.
71+
- `false` will make `file.symlink` equal the original symlink's target path.
6972
- Any glob-related options are documented in [glob-stream] and [node-glob].
7073
- Returns a Readable stream by default, or a Duplex stream if the `passthrough` option is set to `true`.
7174
- This stream emits matching [vinyl] File objects.
@@ -103,6 +106,7 @@ This is just [glob-watcher].
103106
- Returns a Readable/Writable stream.
104107
- On write the stream will save the [vinyl] File to disk at the folder/cwd specified.
105108
- After writing the file to disk, it will be emitted from the stream so you can keep piping these around.
109+
- If the file has a `symlink` attribute specifying a target path, then a symlink will be created.
106110
- The file will be modified after being written to this stream:
107111
- `cwd`, `base`, and `path` will be overwritten to match the folder.
108112
- `stat.mode` will be overwritten if you used a mode parameter.

lib/dest/writeContents/index.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var fs = require('fs');
44
var writeDir = require('./writeDir');
55
var writeStream = require('./writeStream');
66
var writeBuffer = require('./writeBuffer');
7+
var writeSymbolicLink = require('./writeSymbolicLink');
78

89
function writeContents(writePath, file, cb) {
910
// if directory then mkdirp it
@@ -16,6 +17,11 @@ function writeContents(writePath, file, cb) {
1617
return writeStream(writePath, file, written);
1718
}
1819

20+
// write it as a symlink
21+
if (file.symlink) {
22+
return writeSymbolicLink(writePath, file, written);
23+
}
24+
1925
// write it like normal
2026
if (file.isBuffer()) {
2127
return writeBuffer(writePath, file, written);
@@ -36,7 +42,7 @@ function writeContents(writePath, file, cb) {
3642
return complete(err);
3743
}
3844

39-
if (!file.stat || typeof file.stat.mode !== 'number') {
45+
if (!file.stat || typeof file.stat.mode !== 'number' || file.symlink) {
4046
return complete();
4147
}
4248

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
var fs = require('graceful-fs');
4+
5+
function writeSymbolicLink(writePath, file, cb) {
6+
fs.symlink(file.symlink, writePath, function (err) {
7+
if (err && err.code !== 'EEXIST') {
8+
return cb(err);
9+
}
10+
11+
cb(null, file);
12+
});
13+
}
14+
15+
module.exports = writeSymbolicLink;

lib/src/getContents/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var through2 = require('through2');
44
var readDir = require('./readDir');
5+
var readSymbolicLink = require('./readSymbolicLink');
56
var bufferFile = require('./bufferFile');
67
var streamFile = require('./streamFile');
78

@@ -12,6 +13,11 @@ function getContents(opt) {
1213
return readDir(file, opt, cb);
1314
}
1415

16+
// process symbolic links included with `followSymlinks` option
17+
if (file.stat && file.stat.isSymbolicLink()) {
18+
return readSymbolicLink(file, opt, cb);
19+
}
20+
1521
// read and pass full contents
1622
if (opt.buffer !== false) {
1723
return bufferFile(file, opt, cb);
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
var fs = require('graceful-fs');
4+
5+
function readLink(file, opt, cb) {
6+
fs.readlink(file.path, function (err, target) {
7+
if (err) {
8+
return cb(err);
9+
}
10+
11+
// store the link target path
12+
file.symlink = target;
13+
14+
return cb(null, file);
15+
});
16+
}
17+
18+
module.exports = readLink;

lib/src/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function src(glob, opt) {
2222
read: true,
2323
buffer: true,
2424
sourcemaps: false,
25-
passthrough: false
25+
passthrough: false,
26+
followSymlinks: true
2627
}, opt);
2728

2829
var inputPass;
@@ -34,7 +35,7 @@ function src(glob, opt) {
3435
var globStream = gs.create(glob, options);
3536

3637
var outputStream = globStream
37-
.pipe(resolveSymlinks())
38+
.pipe(resolveSymlinks(options))
3839
.pipe(through.obj(createFile));
3940

4041
if (options.since != null) {

lib/src/resolveSymlinks.js

+23-22
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,36 @@ var through2 = require('through2');
44
var fs = require('graceful-fs');
55
var path = require('path');
66

7-
function resolveSymlinks() {
8-
return through2.obj(resolveFile);
9-
}
10-
11-
// a stat property is exposed on file objects as a (wanted) side effect
12-
function resolveFile(globFile, enc, cb) {
13-
fs.lstat(globFile.path, function (err, stat) {
14-
if (err) {
15-
return cb(err);
16-
}
7+
function resolveSymlinks(options) {
178

18-
globFile.stat = stat;
19-
20-
if (!stat.isSymbolicLink()) {
21-
return cb(null, globFile);
22-
}
23-
24-
fs.realpath(globFile.path, function (err, filePath) {
9+
// a stat property is exposed on file objects as a (wanted) side effect
10+
function resolveFile(globFile, enc, cb) {
11+
fs.lstat(globFile.path, function (err, stat) {
2512
if (err) {
2613
return cb(err);
2714
}
2815

29-
globFile.base = path.dirname(filePath);
30-
globFile.path = filePath;
16+
globFile.stat = stat;
3117

32-
// recurse to get real file stat
33-
resolveFile(globFile, enc, cb);
18+
if (!stat.isSymbolicLink() || !options.followSymlinks) {
19+
return cb(null, globFile);
20+
}
21+
22+
fs.realpath(globFile.path, function (err, filePath) {
23+
if (err) {
24+
return cb(err);
25+
}
26+
27+
globFile.base = path.dirname(filePath);
28+
globFile.path = filePath;
29+
30+
// recurse to get real file stat
31+
resolveFile(globFile, enc, cb);
32+
});
3433
});
35-
});
34+
}
35+
36+
return through2.obj(resolveFile);
3637
}
3738

3839
module.exports = resolveSymlinks;

test/dest.js

+34
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,40 @@ describe('dest stream', function() {
903903
stream.end();
904904
});
905905

906+
it('should create symlinks when the `symlink` attribute is set on the file', function (done) {
907+
var inputPath = path.join(__dirname, './fixtures/test-create-dir-symlink');
908+
var inputBase = path.join(__dirname, './fixtures/');
909+
var inputRelativeSymlinkPath = 'wow';
910+
911+
var expectedPath = path.join(__dirname, './out-fixtures/test-create-dir-symlink');
912+
913+
var inputFile = new File({
914+
base: inputBase,
915+
cwd: __dirname,
916+
path: inputPath,
917+
contents: null, //''
918+
});
919+
920+
// `src()` adds this side-effect with `keepSymlinks` option set to false
921+
inputFile.symlink = inputRelativeSymlinkPath;
922+
923+
var onEnd = function(){
924+
fs.readlink(buffered[0].path, function (err, link) {
925+
buffered[0].symlink.should.equal(inputFile.symlink);
926+
buffered[0].path.should.equal(expectedPath);
927+
done();
928+
});
929+
};
930+
931+
var stream = vfs.dest('./out-fixtures/', {cwd: __dirname});
932+
933+
var buffered = [];
934+
bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd);
935+
stream.pipe(bufferStream);
936+
stream.write(inputFile);
937+
stream.end();
938+
});
939+
906940
it('should emit finish event', function(done) {
907941
var srcPath = path.join(__dirname, './fixtures/test.coffee');
908942
var stream = vfs.dest('./out-fixtures/', {cwd: __dirname});

test/src.js

+36
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,40 @@ describe('source stream', function() {
383383
});
384384
});
385385

386+
it('should preserve file symlinks with followSymlinks option set to false', function (done) {
387+
var sourcePath = path.join(__dirname, './fixtures/test-symlink');
388+
var expectedPath = sourcePath;
389+
390+
fs.readlink(sourcePath, function (err, expectedRelativeSymlinkPath) {
391+
if (err) {
392+
throw err;
393+
}
394+
395+
var stream = vfs.src('./fixtures/test-symlink', {cwd: __dirname, followSymlinks: false});
396+
stream.on('data', function(file) {
397+
file.path.should.equal(expectedPath);
398+
file.symlink.should.equal(expectedRelativeSymlinkPath);
399+
done();
400+
});
401+
});
402+
});
403+
404+
it('should preserve dir symlinks with followSymlinks option set to false', function (done) {
405+
var sourcePath = path.join(__dirname, './fixtures/test-symlink-dir');
406+
var expectedPath = sourcePath;
407+
408+
fs.readlink(sourcePath, function (err, expectedRelativeSymlinkPath) {
409+
if (err) {
410+
throw err;
411+
}
412+
413+
var stream = vfs.src(sourcePath, {cwd: __dirname, followSymlinks: false});
414+
stream.on('data', function (file) {
415+
file.path.should.equal(expectedPath);
416+
file.symlink.should.equal(expectedRelativeSymlinkPath);
417+
done();
418+
});
419+
});
420+
});
421+
386422
});

0 commit comments

Comments
 (0)