Skip to content

Commit 9a0e5f6

Browse files
nfischerdwi2
andauthored
Add preserve option to cp (shelljs#869)
Co-authored-by: dwi2 <[email protected]>
1 parent a329b49 commit 9a0e5f6

3 files changed

Lines changed: 134 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ Available options:
206206
+ `-r`, `-R`: recursive
207207
+ `-L`: follow symlinks
208208
+ `-P`: don't follow symlinks
209+
+ `-p`: preserve file mode, ownership, and timestamps
209210

210211
Examples:
211212

src/cp.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ common.register('cp', _cp, {
1111
'r': 'recursive',
1212
'L': 'followsymlink',
1313
'P': 'noFollowsymlink',
14+
'p': 'preserve',
1415
},
1516
wrapOutput: false,
1617
});
@@ -51,6 +52,7 @@ function copyFileSync(srcFile, destFile, options) {
5152
var pos = 0;
5253
var fdr = null;
5354
var fdw = null;
55+
var srcStat = common.statFollowLinks(srcFile);
5456

5557
try {
5658
fdr = fs.openSync(srcFile, 'r');
@@ -60,7 +62,7 @@ function copyFileSync(srcFile, destFile, options) {
6062
}
6163

6264
try {
63-
fdw = fs.openSync(destFile, 'w');
65+
fdw = fs.openSync(destFile, 'w', srcStat.mode);
6466
} catch (e) {
6567
/* istanbul ignore next */
6668
common.error('copyFileSync: could not write to dest file (code=' + e.code + '):' + destFile);
@@ -72,10 +74,15 @@ function copyFileSync(srcFile, destFile, options) {
7274
pos += bytesRead;
7375
}
7476

77+
if (options.preserve) {
78+
fs.fchownSync(fdw, srcStat.uid, srcStat.gid);
79+
// Note: utimesSync does not work (rounds to seconds), but futimesSync has
80+
// millisecond precision.
81+
fs.futimesSync(fdw, srcStat.atime, srcStat.mtime);
82+
}
83+
7584
fs.closeSync(fdr);
7685
fs.closeSync(fdw);
77-
78-
fs.chmodSync(destFile, common.statFollowLinks(srcFile).mode);
7986
}
8087
}
8188

@@ -96,8 +103,9 @@ function cpdirSyncRecursive(sourceDir, destDir, currentDepth, opts) {
96103

97104
var isWindows = process.platform === 'win32';
98105

99-
// Create the directory where all our junk is moving to; read the mode of the
100-
// source directory and mirror it
106+
// Create the directory where all our junk is moving to; read the mode/etc. of
107+
// the source directory (we'll set this on the destDir at the end).
108+
var checkDir = common.statFollowLinks(sourceDir);
101109
try {
102110
fs.mkdirSync(destDir);
103111
} catch (e) {
@@ -150,7 +158,10 @@ function cpdirSyncRecursive(sourceDir, destDir, currentDepth, opts) {
150158

151159
// finally change the mode for the newly created directory (otherwise, we
152160
// couldn't add files to a read-only directory).
153-
var checkDir = common.statFollowLinks(sourceDir);
161+
// var checkDir = common.statFollowLinks(sourceDir);
162+
if (opts.preserve) {
163+
fs.utimesSync(destDir, checkDir.atime, checkDir.mtime);
164+
}
154165
fs.chmodSync(destDir, checkDir.mode);
155166
} // cpdirSyncRecursive
156167

@@ -196,6 +207,7 @@ function cpcheckcycle(sourceDir, srcFile) {
196207
//@ + `-r`, `-R`: recursive
197208
//@ + `-L`: follow symlinks
198209
//@ + `-P`: don't follow symlinks
210+
//@ + `-p`: preserve file mode, ownership, and timestamps
199211
//@
200212
//@ Examples:
201213
//@
@@ -258,7 +270,7 @@ function _cp(options, sources, dest) {
258270

259271
try {
260272
common.statFollowLinks(path.dirname(dest));
261-
cpdirSyncRecursive(src, newDest, 0, { no_force: options.no_force, followsymlink: options.followsymlink, update: options.update });
273+
cpdirSyncRecursive(src, newDest, 0, options);
262274
} catch (e) {
263275
/* istanbul ignore next */
264276
common.error("cannot create directory '" + dest + "': No such file or directory");

test/cp.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,117 @@ test('cp -R should be able to copy a readonly src. issue #98; (Non window platfo
805805
shell.chmod('-R', '755', t.context.tmp);
806806
});
807807
});
808+
809+
test('cp -p should preserve mode, ownership, and timestamp (regular file)', t => {
810+
// Setup: copy to srcFile and modify mode and timestamp
811+
const srcFile = `${t.context.tmp}/srcFile`;
812+
shell.cp('test/resources/cp/file1', srcFile);
813+
// Make this a round number of seconds, since the underlying system may not
814+
// have millisecond precision.
815+
const newModifyTimeMs = 12345000;
816+
const newAccessTimeMs = 67890000;
817+
shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcFile);
818+
shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcFile);
819+
const mode = '444';
820+
shell.chmod(mode, srcFile);
821+
822+
// Now re-copy with '-p' and verify metadata.
823+
const result = shell.cp('-p', srcFile, `${t.context.tmp}/preservedFile1`);
824+
const stat = common.statFollowLinks(srcFile);
825+
const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedFile1`);
826+
827+
t.is(result.code, 0);
828+
829+
// Original file should be unchanged:
830+
t.is(stat.mtime.getTime(), newModifyTimeMs);
831+
// cp appears to update the atime, but only of the srcFile
832+
t.is(stat.mode.toString(8), '100' + mode);
833+
834+
// New file should keep same attributes
835+
t.is(statOfResult.mtime.getTime(), newModifyTimeMs);
836+
t.is(statOfResult.atime.getTime(), newAccessTimeMs);
837+
t.is(statOfResult.mode.toString(8), '100' + mode);
838+
839+
t.is(stat.uid, statOfResult.uid);
840+
t.is(stat.gid, statOfResult.gid);
841+
});
842+
843+
test('cp -p should preserve mode, ownership, and timestamp (directory)', t => {
844+
// Setup: copy to srcFile and modify mode and timestamp
845+
const srcDir = `${t.context.tmp}/srcDir`;
846+
const srcFile = `${srcDir}/srcFile`;
847+
shell.mkdir(srcDir);
848+
shell.cp('test/resources/cp/file1', srcFile);
849+
// Make this a round number of seconds, since the underlying system may not
850+
// have millisecond precision.
851+
const newModifyTimeMs = 12345000;
852+
const newAccessTimeMs = 67890000;
853+
shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcFile);
854+
shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcFile);
855+
fs.utimesSync(srcDir, new Date(newAccessTimeMs), new Date(newModifyTimeMs));
856+
const mode = '444';
857+
shell.chmod(mode, srcFile);
858+
859+
// Now re-copy (the whole dir) with '-p' and verify metadata of file contents.
860+
const result = shell.cp('-pr', srcDir, `${t.context.tmp}/preservedDir`);
861+
const stat = common.statFollowLinks(srcFile);
862+
const statDir = common.statFollowLinks(srcDir);
863+
const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedDir/srcFile`);
864+
const statOfResultDir = common.statFollowLinks(`${t.context.tmp}/preservedDir`);
865+
866+
t.is(result.code, 0);
867+
868+
// Both original file and original dir should be unchanged:
869+
t.is(statDir.mtime.getTime(), newModifyTimeMs);
870+
t.is(stat.mtime.getTime(), newModifyTimeMs);
871+
// cp appears to update the atime, but only of the srcFile & srcDir
872+
t.is(stat.mode.toString(8), '100' + mode);
873+
874+
// Both new file and new dir should keep same attributes
875+
t.is(statOfResultDir.mtime.getTime(), newModifyTimeMs);
876+
t.is(statOfResultDir.atime.getTime(), newAccessTimeMs);
877+
t.is(statOfResult.mtime.getTime(), newModifyTimeMs);
878+
t.is(statOfResult.atime.getTime(), newAccessTimeMs);
879+
t.is(statOfResult.mode.toString(8), '100' + mode);
880+
881+
t.is(stat.uid, statOfResult.uid);
882+
t.is(stat.gid, statOfResult.gid);
883+
});
884+
885+
test('cp -p should preserve mode, ownership, and timestamp (symlink)', t => {
886+
// Skip in Windows because symlinks require elevated permissions.
887+
utils.skipOnWin(t, () => {
888+
// Setup: copy to srcFile, create srcLink, and modify mode and timestamp
889+
shell.cp('test/resources/cp/file1', `${t.context.tmp}/srcFile`);
890+
const srcLink = `${t.context.tmp}/srcLink`;
891+
shell.ln('-s', 'srcFile', `${t.context.tmp}/srcLink`);
892+
// Make this a round number of seconds, since the underlying system may not
893+
// have millisecond precision.
894+
const newModifyTimeMs = 12345000;
895+
const newAccessTimeMs = 67890000;
896+
shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcLink);
897+
shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcLink);
898+
const mode = '444';
899+
shell.chmod(mode, srcLink);
900+
901+
// Now re-copy with '-p' and verify metadata.
902+
const result = shell.cp('-p', srcLink, `${t.context.tmp}/preservedLink`);
903+
const stat = common.statFollowLinks(srcLink);
904+
const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedLink`);
905+
906+
t.is(result.code, 0);
907+
908+
// Original file should be unchanged:
909+
t.is(stat.mtime.getTime(), newModifyTimeMs);
910+
// cp appears to update the atime, but only of the srcFile
911+
t.is(stat.mode.toString(8), '100' + mode);
912+
913+
// New file should keep same attributes
914+
t.is(statOfResult.mtime.getTime(), newModifyTimeMs);
915+
t.is(statOfResult.atime.getTime(), newAccessTimeMs);
916+
t.is(statOfResult.mode.toString(8), '100' + mode);
917+
918+
t.is(stat.uid, statOfResult.uid);
919+
t.is(stat.gid, statOfResult.gid);
920+
});
921+
});

0 commit comments

Comments
 (0)