Skip to content

Commit d7e15e0

Browse files
committed
feat(cp): -P option, plus better handling of symlinks
1 parent 193efa7 commit d7e15e0

16 files changed

Lines changed: 216 additions & 51 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ Available options:
198198
+ `-f`: force (default behavior)
199199
+ `-n`: no-clobber
200200
+ `-r`, `-R`: recursive
201-
+ `-L`: followsymlink
201+
+ `-L`: follow symlinks
202+
+ `-P`: don't follow symlinks
202203

203204
Examples:
204205

src/cp.js

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,51 @@ var os = require('os');
66
// Buffered file copy, synchronous
77
// (Using readFileSync() + writeFileSync() could easily cause a memory overflow
88
// with large files)
9-
function copyFileSync(srcFile, destFile) {
9+
function copyFileSync(srcFile, destFile, options) {
1010
if (!fs.existsSync(srcFile))
1111
common.error('copyFileSync: no such file or directory: ' + srcFile);
1212

13-
var BUF_LENGTH = 64*1024,
14-
buf = new Buffer(BUF_LENGTH),
15-
bytesRead = BUF_LENGTH,
16-
pos = 0,
17-
fdr = null,
18-
fdw = null;
13+
if (fs.lstatSync(srcFile).isSymbolicLink() && !options.followsymlink) {
14+
try {
15+
fs.lstatSync(destFile);
16+
common.unlinkSync(destFile); // re-link it
17+
} catch (e) {
18+
// it doesn't exist, so no work needs to be done
19+
}
1920

20-
try {
21-
fdr = fs.openSync(srcFile, 'r');
22-
} catch(e) {
23-
common.error('copyFileSync: could not read src file ('+srcFile+')');
24-
}
21+
var symlinkFull = fs.readlinkSync(srcFile);
22+
fs.symlinkSync(symlinkFull, destFile, os.platform() === "win32" ? "junction" : null);
23+
} else {
24+
var BUF_LENGTH = 64*1024,
25+
buf = new Buffer(BUF_LENGTH),
26+
bytesRead = BUF_LENGTH,
27+
pos = 0,
28+
fdr = null,
29+
fdw = null;
30+
31+
try {
32+
fdr = fs.openSync(srcFile, 'r');
33+
} catch(e) {
34+
common.error('copyFileSync: could not read src file ('+srcFile+')');
35+
}
2536

26-
try {
27-
fdw = fs.openSync(destFile, 'w');
28-
} catch(e) {
29-
common.error('copyFileSync: could not write to dest file (code='+e.code+'):'+destFile);
30-
}
37+
try {
38+
fdw = fs.openSync(destFile, 'w');
39+
} catch(e) {
40+
common.error('copyFileSync: could not write to dest file (code='+e.code+'):'+destFile);
41+
}
3142

32-
while (bytesRead === BUF_LENGTH) {
33-
bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
34-
fs.writeSync(fdw, buf, 0, bytesRead);
35-
pos += bytesRead;
36-
}
43+
while (bytesRead === BUF_LENGTH) {
44+
bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
45+
fs.writeSync(fdw, buf, 0, bytesRead);
46+
pos += bytesRead;
47+
}
3748

38-
fs.closeSync(fdr);
39-
fs.closeSync(fdw);
49+
fs.closeSync(fdr);
50+
fs.closeSync(fdw);
4051

41-
fs.chmodSync(destFile, fs.statSync(srcFile).mode);
52+
fs.chmodSync(destFile, fs.statSync(srcFile).mode);
53+
}
4254
}
4355

4456
// Recursively copies 'sourceDir' into 'destDir'
@@ -83,7 +95,7 @@ function cpdirSyncRecursive(sourceDir, destDir, opts) {
8395
if (opts.followsymlink) {
8496
if (cpcheckcycle(sourceDir, srcFile)) {
8597
// Cycle link found.
86-
console.log('Cycle link found.');
98+
console.error('Cycle link found.');
8799
symlinkFull = fs.readlinkSync(srcFile);
88100
fs.symlinkSync(symlinkFull, destFile, os.platform() === "win32" ? "junction" : null);
89101
continue;
@@ -94,20 +106,26 @@ function cpdirSyncRecursive(sourceDir, destDir, opts) {
94106
cpdirSyncRecursive(srcFile, destFile, opts);
95107
} else if (srcFileStat.isSymbolicLink() && !opts.followsymlink) {
96108
symlinkFull = fs.readlinkSync(srcFile);
109+
try {
110+
fs.lstatSync(destFile);
111+
common.unlinkSync(destFile); // re-link it
112+
} catch (e) {
113+
// it doesn't exist, so no work needs to be done
114+
}
97115
fs.symlinkSync(symlinkFull, destFile, os.platform() === "win32" ? "junction" : null);
98116
} else if (srcFileStat.isSymbolicLink() && opts.followsymlink) {
99117
srcFileStat = fs.statSync(srcFile);
100118
if (srcFileStat.isDirectory()) {
101119
cpdirSyncRecursive(srcFile, destFile, opts);
102120
} else {
103-
copyFileSync(srcFile, destFile);
121+
copyFileSync(srcFile, destFile, opts);
104122
}
105123
} else {
106124
/* At this point, we've hit a file actually worth copying... so copy it on over. */
107125
if (fs.existsSync(destFile) && opts.no_force) {
108126
common.log('skipping existing file: ' + files[i]);
109127
} else {
110-
copyFileSync(srcFile, destFile);
128+
copyFileSync(srcFile, destFile, opts);
111129
}
112130
}
113131

@@ -139,7 +157,8 @@ function cpcheckcycle(sourceDir, srcFile) {
139157
//@ + `-f`: force (default behavior)
140158
//@ + `-n`: no-clobber
141159
//@ + `-r`, `-R`: recursive
142-
//@ + `-L`: followsymlink
160+
//@ + `-L`: follow symlinks
161+
//@ + `-P`: don't follow symlinks
143162
//@
144163
//@ Examples:
145164
//@
@@ -158,8 +177,15 @@ function _cp(options, sources, dest) {
158177
'R': 'recursive',
159178
'r': 'recursive',
160179
'L': 'followsymlink',
180+
'P': 'noFollowsymlink',
161181
});
162182

183+
// If we're missing -R, it actually implies -L (unless -P is explicit)
184+
if (options.followsymlink)
185+
options.noFollowsymlink = false;
186+
if (!options.recursive && !options.noFollowsymlink)
187+
options.followsymlink = true;
188+
163189
// Get sources, dest
164190
if (arguments.length < 3) {
165191
common.error('missing <source> and/or <dest>');
@@ -185,7 +211,7 @@ function _cp(options, sources, dest) {
185211
return; // skip file
186212
}
187213
var srcStat = fs.statSync(src);
188-
if (srcStat.isDirectory()) {
214+
if (!options.noFollowsymlink && srcStat.isDirectory()) {
189215
if (!options.recursive) {
190216
// Non-Recursive
191217
common.error("omitting directory '" + src + "'", true);
@@ -218,7 +244,7 @@ function _cp(options, sources, dest) {
218244
return; // skip file
219245
}
220246

221-
copyFileSync(src, thisDest);
247+
copyFileSync(src, thisDest, options);
222248
}
223249
}); // forEach(src)
224250
return new common.ShellString('', common.state.error, common.state.errorCode);

src/mkdir.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ function _mkdir(options, dirs) {
4646
// if it's array leave it as it is
4747

4848
dirs.forEach(function(dir) {
49-
if (fs.existsSync(dir)) {
49+
try {
50+
fs.lstatSync(dir);
5051
if (!options.fullpath)
51-
common.error('path already exists: ' + dir, true);
52+
common.error('path already exists: ' + dir, true);
5253
return; // skip dir
54+
} catch (e) {
55+
// do nothing
5356
}
5457

5558
// Base dir does not exist, and no -p option given

src/rm.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,17 @@ function _rm(options, files) {
107107
files = [].slice.call(arguments, 1);
108108

109109
files.forEach(function(file) {
110-
if (!fs.existsSync(file)) {
110+
var stats;
111+
try {
112+
stats = fs.lstatSync(file); // test for existence
113+
} catch (e) {
111114
// Path does not exist, no force flag given
112115
if (!options.force)
113116
common.error('no such file or directory: '+file, true);
114-
115117
return; // skip file
116118
}
117119

118120
// If here, path exists
119-
120-
var stats = fs.lstatSync(file);
121121
if (stats.isFile() || stats.isSymbolicLink()) {
122122

123123
// Do not check for file writing permissions

test/common.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ var result = common.expand(['**/file*.js']);
4646
assert.equal(shell.error(), null);
4747
assert.deepEqual(result.sort(), ["resources/file1.js","resources/file2.js","resources/ls/file1.js","resources/ls/file2.js"].sort());
4848

49+
// broken links still expand
50+
var result = common.expand(['resources/b*dlink']);
51+
assert.equal(shell.error(), null);
52+
assert.deepEqual(result, ['resources/badlink']);
53+
4954
// common.parseOptions (normal case)
5055
var result = common.parseOptions('-Rf', {
5156
'R': 'recursive',

test/cp.js

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,39 @@ assert.equal(shell.error(), null); // crash test only
231231
assert.ok(!result.stderr);
232232
assert.equal(result.code, 0);
233233

234+
if (process.platform !== 'win32') {
235+
// Recursive, everything exists, overwrite a real file with a link (if same name)
236+
// Because -R implies to not follow links!
237+
shell.rm('-rf', 'tmp/*');
238+
shell.cp('-R', 'resources/cp/*', 'tmp');
239+
assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link
240+
assert.ok(!(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink())); // this one isn't
241+
assert.notEqual(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString());
242+
result = shell.cp('-R', 'tmp/links/*', 'tmp/fakeLinks');
243+
assert.equal(shell.error(), null);
244+
assert.ok(!result.stderr);
245+
assert.equal(result.code, 0);
246+
assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link
247+
assert.ok(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink()); // this one is now a link
248+
assert.equal(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString());
249+
250+
// Recursive, everything exists, overwrite a real file *by following a link*
251+
// Because missing the -R implies -L.
252+
shell.rm('-rf', 'tmp/*');
253+
shell.cp('-R', 'resources/cp/*', 'tmp');
254+
assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link
255+
assert.ok(!(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink())); // this one isn't
256+
assert.notEqual(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString());
257+
result = shell.cp('tmp/links/*', 'tmp/fakeLinks'); // don't use -R
258+
assert.equal(shell.error(), null);
259+
assert.ok(!result.stderr);
260+
assert.equal(result.code, 0);
261+
assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link
262+
assert.ok(!fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink()); // this one is still not a link
263+
// But it still follows the link
264+
assert.equal(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString());
265+
}
266+
234267
//recursive, everything exists, with force flag
235268
shell.rm('-rf', 'tmp/*');
236269
result = shell.cp('-R', 'resources/cp', 'tmp');
@@ -275,12 +308,12 @@ assert.equal(fs.existsSync('tmp/dest/z'), true);
275308

276309
// On Windows, permission bits are quite different so skip those tests for now
277310
if (common.platform !== 'win') {
278-
//preserve mode bits
279-
shell.rm('-rf', 'tmp/*');
280-
var execBit = parseInt('001', 8);
281-
assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode & execBit, execBit);
282-
shell.cp('resources/cp-mode-bits/executable', 'tmp/executable');
283-
assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode, fs.statSync('tmp/executable').mode);
311+
//preserve mode bits
312+
shell.rm('-rf', 'tmp/*');
313+
var execBit = parseInt('001', 8);
314+
assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode & execBit, execBit);
315+
shell.cp('resources/cp-mode-bits/executable', 'tmp/executable');
316+
assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode, fs.statSync('tmp/executable').mode);
284317
}
285318

286319
// Make sure hidden files are copied recursively
@@ -304,7 +337,7 @@ assert.ok(fs.existsSync('tmp/file1.txt'));
304337
shell.rm('-rf', 'tmp/');
305338
shell.mkdir('tmp/');
306339
result = shell.cp('resources/file1.txt', 'resources/file2.txt', 'resources/cp',
307-
'resources/ls/', 'tmp/');
340+
'resources/ls/', 'tmp/');
308341
assert.ok(shell.error());
309342
assert.ok(!fs.existsSync('tmp/.hidden_file')); // doesn't copy dir contents
310343
assert.ok(!fs.existsSync('tmp/ls')); // doesn't copy dir itself
@@ -313,13 +346,67 @@ assert.ok(!fs.existsSync('tmp/cp')); // doesn't copy dir itself
313346
assert.ok(fs.existsSync('tmp/file1.txt'));
314347
assert.ok(fs.existsSync('tmp/file2.txt'));
315348

316-
// Recursive, copies entire directory with no symlinks and -L option does not cause change in behavior.
349+
if (process.platform !== 'win32') {
350+
// -R implies -P
351+
shell.rm('-rf', 'tmp/*');
352+
shell.cp('-R', 'resources/cp/links/sym.lnk', 'tmp');
353+
assert.ok(fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
354+
355+
// using -P explicitly works
356+
shell.rm('-rf', 'tmp/*');
357+
shell.cp('-P', 'resources/cp/links/sym.lnk', 'tmp');
358+
assert.ok(fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
359+
360+
// using -PR on a link to a folder does not follow the link
361+
shell.rm('-rf', 'tmp/*');
362+
shell.cp('-PR', 'resources/cp/symFolder', 'tmp');
363+
assert.ok(fs.lstatSync('tmp/symFolder').isSymbolicLink());
364+
365+
// Recursive, copies entire directory with no symlinks and -L option does not cause change in behavior.
366+
shell.rm('-rf', 'tmp/*');
367+
result = shell.cp('-rL', 'resources/cp/dir_a', 'tmp/dest');
368+
assert.equal(shell.error(), null);
369+
assert.ok(!result.stderr);
370+
assert.equal(result.code, 0);
371+
assert.equal(fs.existsSync('tmp/dest/z'), true);
372+
}
373+
374+
// using -R on a link to a folder *does* follow the link
317375
shell.rm('-rf', 'tmp/*');
318-
result = shell.cp('-rL', 'resources/cp/dir_a', 'tmp/dest');
319-
assert.equal(shell.error(), null);
320-
assert.ok(!result.stderr);
321-
assert.equal(result.code, 0);
322-
assert.equal(fs.existsSync('tmp/dest/z'), true);
376+
shell.cp('-R', 'resources/cp/symFolder', 'tmp');
377+
assert.ok(!fs.lstatSync('tmp/symFolder').isSymbolicLink());
378+
379+
// Without -R, -L is implied
380+
shell.rm('-rf', 'tmp/*');
381+
shell.cp('resources/cp/links/sym.lnk', 'tmp');
382+
assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
383+
384+
// -L explicitly works
385+
shell.rm('-rf', 'tmp/*');
386+
shell.cp('-L', 'resources/cp/links/sym.lnk', 'tmp');
387+
assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
388+
389+
// using -LR does not imply -P
390+
shell.rm('-rf', 'tmp/*');
391+
shell.cp('-LR', 'resources/cp/links/sym.lnk', 'tmp');
392+
assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
393+
394+
// using -LR also works recursively on directories containing links
395+
shell.rm('-rf', 'tmp/*');
396+
shell.cp('-LR', 'resources/cp/links', 'tmp');
397+
assert.ok(!fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink());
398+
399+
// -L always overrides a -P
400+
shell.rm('-rf', 'tmp/*');
401+
shell.cp('-LP', 'resources/cp/links/sym.lnk', 'tmp');
402+
assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
403+
shell.rm('-rf', 'tmp/*');
404+
shell.cp('-LPR', 'resources/cp/links/sym.lnk', 'tmp');
405+
assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink());
406+
shell.rm('-rf', 'tmp/*');
407+
shell.cp('-LPR', 'resources/cp/symFolder', 'tmp');
408+
assert.ok(!fs.lstatSync('tmp/symFolder').isSymbolicLink());
409+
assert.ok(!fs.lstatSync('tmp/symFolder/sym.lnk').isSymbolicLink());
323410

324411
// Test max depth.
325412
shell.rm('-rf', 'tmp/');

test/ls.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ assert.ok(result.atime); // check that these keys exist
370370
assert.ok(result.ctime); // check that these keys exist
371371
assert.ok(result.toString().match(/^(\d+ +){5}.*$/));
372372

373+
// still lists broken links
374+
result = shell.ls('resources/badlink');
375+
assert.equal(shell.error(), null);
376+
assert.equal(result.code, 0);
377+
assert.equal(result.indexOf('resources/badlink') > -1, true);
378+
assert.equal(result.length, 1);
379+
373380
// Test new ShellString-like attributes
374381
result = shell.ls('resources/ls');
375382
assert.equal(shell.error(), null);

test/mkdir.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ assert.equal(result.code, 1);
2525
assert.equal(result.stderr, 'mkdir: path already exists: tmp');
2626
assert.equal(fs.statSync('tmp').mtime.toString(), mtime); // didn't mess with dir
2727

28+
// Can't overwrite a broken link
29+
mtime = fs.lstatSync('resources/badlink').mtime.toString();
30+
result = shell.mkdir('resources/badlink');
31+
assert.ok(shell.error());
32+
assert.equal(result.code, 1);
33+
assert.equal(result.stderr, 'mkdir: path already exists: resources/badlink');
34+
assert.equal(fs.lstatSync('resources/badlink').mtime.toString(), mtime); // didn't mess with file
35+
2836
assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check
2937
result = shell.mkdir('/asdfasdf/foobar'); // root path does not exist
3038
assert.ok(shell.error());
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is not a link

0 commit comments

Comments
 (0)