From fef5e8b26c113ea772291beef7d582046823ccf4 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Mon, 22 Jun 2015 13:22:53 +0200 Subject: [PATCH 1/4] Add option to preserve timestamps Signed-off-by: Joern Bernhardt --- README.md | 4 + lib/copy/copy-file-sync.js | 9 +- lib/copy/copy-sync.js | 7 +- lib/copy/ncp.js | 10 +- test/copy/copy.test.js | 91 +++++++++++++++++++ test/copy/fixtures/a-file | 1 + test/copy/fixtures/a-folder/another-file | 1 + .../fixtures/a-folder/another-folder/file3 | 1 + 8 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 test/copy/copy.test.js create mode 100644 test/copy/fixtures/a-file create mode 100644 test/copy/fixtures/a-folder/another-file create mode 100644 test/copy/fixtures/a-folder/another-folder/file3 diff --git a/README.md b/README.md index 2606a5f0..0e4a62b3 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,10 @@ Methods Copy a file or directory. The directory can have contents. Like `cp -r`. + +Options: +clobber (boolean): overwrite existing file or directory +preserveTimestamps (boolean): will set last modification and access times to the ones of the original source files, default is `false`. Sync: `copySync()` diff --git a/lib/copy/copy-file-sync.js b/lib/copy/copy-file-sync.js index c59e608e..04962158 100644 --- a/lib/copy/copy-file-sync.js +++ b/lib/copy/copy-file-sync.js @@ -3,7 +3,10 @@ var fs = require('graceful-fs') var BUF_LENGTH = 64 * 1024 var _buff = new Buffer(BUF_LENGTH) -function copyFileSync (srcFile, destFile, clobber) { +function copyFileSync (srcFile, destFile, options) { + var clobber = options.clobber + var preserveTimestamps = options.preserveTimestamps + if (fs.existsSync(destFile) && !clobber) { throw Error('EEXIST') } @@ -22,6 +25,10 @@ function copyFileSync (srcFile, destFile, clobber) { fs.closeSync(fdr) fs.closeSync(fdw) + + if (preserveTimestamps) { + fs.utimesSync(destFile, stat.atime, stat.mtime) + } } module.exports = copyFileSync diff --git a/lib/copy/copy-sync.js b/lib/copy/copy-sync.js index 0f3adcd1..a6869443 100644 --- a/lib/copy/copy-sync.js +++ b/lib/copy/copy-sync.js @@ -13,6 +13,7 @@ function copySync (src, dest, options) { // default to true for now options.clobber = 'clobber' in options ? !!options.clobber : true + options.preserveTimestamps = 'preserveTimestamps' in options ? !!options.preserveTimestamps : true options.filter = options.filter || function () { return true } @@ -27,13 +28,15 @@ function copySync (src, dest, options) { if (performCopy) { if (!destFolderExists) mkdir.mkdirsSync(destFolder) - copyFileSync(src, dest, options.clobber) + copyFileSync(src, dest, {clobber: options.clobber, preserveTimestamps: options.preserveTimestamps}) } } else if (stats.isDirectory()) { if (!fs.existsSync(dest)) mkdir.mkdirsSync(dest) var contents = fs.readdirSync(src) contents.forEach(function (content) { - copySync(path.join(src, content), path.join(dest, content), {filter: options.filter, recursive: true}) + var opts = options + opts.recursive = true + copySync(path.join(src, content), path.join(dest, content), opts) }) } else if (options.recursive && stats.isSymbolicLink()) { var srcPath = fs.readlinkSync(src) diff --git a/lib/copy/ncp.js b/lib/copy/ncp.js index 65451113..2985d575 100644 --- a/lib/copy/ncp.js +++ b/lib/copy/ncp.js @@ -17,6 +17,7 @@ function ncp (source, dest, options, callback) { var transform = options.transform var clobber = options.clobber !== false var dereference = options.dereference + var preserveTimestamps = options.preserveTimestamps === true var errs = null @@ -106,7 +107,14 @@ function ncp (source, dest, options, callback) { } writeStream.once('finish', function () { - doneOne() + if (preserveTimestamps) { + fs.utimes(target, file.atime, file.mtime, function (err) { + if (err) return onError(err) + return doneOne() + }) + } else { + doneOne() + } }) } diff --git a/test/copy/copy.test.js b/test/copy/copy.test.js new file mode 100644 index 00000000..455cd209 --- /dev/null +++ b/test/copy/copy.test.js @@ -0,0 +1,91 @@ +var assert = require('assert') +var fs = require('fs') +var path = require('path') +var os = require('os') +var fse = require(process.cwd()) + +/* global afterEach, beforeEach, describe, it */ + +describe('fs-extra', function () { + var TEST_DIR + var SRC_FIXTURES_DIR = path.join(__dirname, './fixtures') + var FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] + + beforeEach(function (done) { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy') + fse.emptyDir(TEST_DIR, done) + }) + + afterEach(function (done) { + fse.remove(TEST_DIR, done) + }) + + describe('+ copySync', function () { + describe('> when modified option is turned off', function () { + it('should have different timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + + fse.copySync(from, TEST_DIR, {preserveTimestamps: false}) + + FILES.forEach(testFile({preserveTimestamps: false})) + done() + }) + }) + + describe('> when modified option is turned on', function () { + it('should have the same timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + + fse.copySync(from, TEST_DIR, {preserveTimestamps: true}) + + FILES.forEach(testFile({preserveTimestamps: true})) + + done() + }) + }) + + }) + + describe('+ copy', function () { + describe('> when modified option is turned off', function () { + it('should have different timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + var to = path.join(TEST_DIR) + + fse.copy(from, to, {preserveTimestamps: false}, function () { + FILES.forEach(testFile({preserveTimestamps: false})) + done() + }) + }) + }) + + describe('> when modified option is turned on', function () { + it('should have the same timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + var to = path.join(TEST_DIR) + + fse.copy(from, to, {preserveTimestamps: true}, function () { + FILES.forEach(testFile({preserveTimestamps: true})) + done() + }) + }) + }) + + }) + + function testFile (options) { + return function (file) { + var a = path.join(SRC_FIXTURES_DIR, file) + var b = path.join(TEST_DIR, file) + var fromStat = fs.statSync(a) + var toStat = fs.statSync(b) + if (options.preserveTimestamps) { + assert.deepEqual(toStat.mtime, fromStat.mtime) + assert.deepEqual(toStat.atime, fromStat.atime) + } else { + assert.notEqual(toStat.mtime, fromStat.mtime) + assert.notEqual(toStat.atime, fromStat.atime) + } + } + } +}) diff --git a/test/copy/fixtures/a-file b/test/copy/fixtures/a-file new file mode 100644 index 00000000..94a709d0 --- /dev/null +++ b/test/copy/fixtures/a-file @@ -0,0 +1 @@ +sonic the hedgehog diff --git a/test/copy/fixtures/a-folder/another-file b/test/copy/fixtures/a-folder/another-file new file mode 100644 index 00000000..31340c77 --- /dev/null +++ b/test/copy/fixtures/a-folder/another-file @@ -0,0 +1 @@ +tails diff --git a/test/copy/fixtures/a-folder/another-folder/file3 b/test/copy/fixtures/a-folder/another-folder/file3 new file mode 100644 index 00000000..73a394da --- /dev/null +++ b/test/copy/fixtures/a-folder/another-folder/file3 @@ -0,0 +1 @@ +knuckles From 7574c3e2c4feb5eee2743000588b930555d19f93 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Mon, 22 Jun 2015 15:32:23 +0200 Subject: [PATCH 2/4] Move tests into lib/copy/__tests__ Signed-off-by: Joern Bernhardt --- lib/copy/__tests__/copy.test.js | 45 +++++++++ .../copy/__tests__}/fixtures/a-file | 0 .../__tests__}/fixtures/a-folder/another-file | 0 .../fixtures/a-folder/another-folder/file3 | 0 lib/copy/__tests__/sync/copy-sync.test.js | 44 +++++++++ test/copy/copy.test.js | 91 ------------------- 6 files changed, 89 insertions(+), 91 deletions(-) rename {test/copy => lib/copy/__tests__}/fixtures/a-file (100%) rename {test/copy => lib/copy/__tests__}/fixtures/a-folder/another-file (100%) rename {test/copy => lib/copy/__tests__}/fixtures/a-folder/another-folder/file3 (100%) delete mode 100644 test/copy/copy.test.js diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 93cee39e..664a2939 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -184,6 +184,51 @@ describe('fs-extra', function () { }) }) + describe('> modification option', function () { + var SRC_FIXTURES_DIR = path.join(__dirname, '/fixtures') + var FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] + + describe('> when modified option is turned off', function () { + it('should have different timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + var to = path.join(TEST_DIR) + + fse.copy(from, to, {preserveTimestamps: false}, function () { + FILES.forEach(testFile({preserveTimestamps: false})) + done() + }) + }) + }) + + describe('> when modified option is turned on', function () { + it('should have the same timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + var to = path.join(TEST_DIR) + + fse.copy(from, to, {preserveTimestamps: true}, function () { + FILES.forEach(testFile({preserveTimestamps: true})) + done() + }) + }) + }) + + function testFile (options) { + return function (file) { + var a = path.join(SRC_FIXTURES_DIR, file) + var b = path.join(TEST_DIR, file) + var fromStat = fs.statSync(a) + var toStat = fs.statSync(b) + if (options.preserveTimestamps) { + assert.deepEqual(toStat.mtime, fromStat.mtime) + assert.deepEqual(toStat.atime, fromStat.atime) + } else { + assert.notEqual(toStat.mtime, fromStat.mtime) + assert.notEqual(toStat.atime, fromStat.atime) + } + } + } + }) + describe.skip('> REGRESSIONS', function () { // pretty UNIX specific, may not pass on windows... only test on Mac OS X 10.9 it('should maintain file permissions and ownership', function (done) { diff --git a/test/copy/fixtures/a-file b/lib/copy/__tests__/fixtures/a-file similarity index 100% rename from test/copy/fixtures/a-file rename to lib/copy/__tests__/fixtures/a-file diff --git a/test/copy/fixtures/a-folder/another-file b/lib/copy/__tests__/fixtures/a-folder/another-file similarity index 100% rename from test/copy/fixtures/a-folder/another-file rename to lib/copy/__tests__/fixtures/a-folder/another-file diff --git a/test/copy/fixtures/a-folder/another-folder/file3 b/lib/copy/__tests__/fixtures/a-folder/another-folder/file3 similarity index 100% rename from test/copy/fixtures/a-folder/another-folder/file3 rename to lib/copy/__tests__/fixtures/a-folder/another-folder/file3 diff --git a/lib/copy/__tests__/sync/copy-sync.test.js b/lib/copy/__tests__/sync/copy-sync.test.js index f4be31ab..4dbcaca0 100644 --- a/lib/copy/__tests__/sync/copy-sync.test.js +++ b/lib/copy/__tests__/sync/copy-sync.test.js @@ -301,4 +301,48 @@ describe('+ copySync()', function () { }) }) }) + + describe('> modification option', function () { + var SRC_FIXTURES_DIR = path.join(__dirname, '/../fixtures') + var FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] + + describe('> when modified option is turned off', function () { + it('should have different timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + + fs.copySync(from, TEST_DIR, {preserveTimestamps: false}) + + FILES.forEach(testFile({preserveTimestamps: false})) + done() + }) + }) + + describe('> when modified option is turned on', function () { + it('should have the same timestamps on copy', function (done) { + var from = path.join(SRC_FIXTURES_DIR) + + fs.copySync(from, TEST_DIR, {preserveTimestamps: true}) + + FILES.forEach(testFile({preserveTimestamps: true})) + + done() + }) + }) + + function testFile (options) { + return function (file) { + var a = path.join(SRC_FIXTURES_DIR, file) + var b = path.join(TEST_DIR, file) + var fromStat = fs.statSync(a) + var toStat = fs.statSync(b) + if (options.preserveTimestamps) { + assert.deepEqual(toStat.mtime, fromStat.mtime) + assert.deepEqual(toStat.atime, fromStat.atime) + } else { + assert.notEqual(toStat.mtime, fromStat.mtime) + assert.notEqual(toStat.atime, fromStat.atime) + } + } + } + }) }) diff --git a/test/copy/copy.test.js b/test/copy/copy.test.js deleted file mode 100644 index 455cd209..00000000 --- a/test/copy/copy.test.js +++ /dev/null @@ -1,91 +0,0 @@ -var assert = require('assert') -var fs = require('fs') -var path = require('path') -var os = require('os') -var fse = require(process.cwd()) - -/* global afterEach, beforeEach, describe, it */ - -describe('fs-extra', function () { - var TEST_DIR - var SRC_FIXTURES_DIR = path.join(__dirname, './fixtures') - var FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] - - beforeEach(function (done) { - TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy') - fse.emptyDir(TEST_DIR, done) - }) - - afterEach(function (done) { - fse.remove(TEST_DIR, done) - }) - - describe('+ copySync', function () { - describe('> when modified option is turned off', function () { - it('should have different timestamps on copy', function (done) { - var from = path.join(SRC_FIXTURES_DIR) - - fse.copySync(from, TEST_DIR, {preserveTimestamps: false}) - - FILES.forEach(testFile({preserveTimestamps: false})) - done() - }) - }) - - describe('> when modified option is turned on', function () { - it('should have the same timestamps on copy', function (done) { - var from = path.join(SRC_FIXTURES_DIR) - - fse.copySync(from, TEST_DIR, {preserveTimestamps: true}) - - FILES.forEach(testFile({preserveTimestamps: true})) - - done() - }) - }) - - }) - - describe('+ copy', function () { - describe('> when modified option is turned off', function () { - it('should have different timestamps on copy', function (done) { - var from = path.join(SRC_FIXTURES_DIR) - var to = path.join(TEST_DIR) - - fse.copy(from, to, {preserveTimestamps: false}, function () { - FILES.forEach(testFile({preserveTimestamps: false})) - done() - }) - }) - }) - - describe('> when modified option is turned on', function () { - it('should have the same timestamps on copy', function (done) { - var from = path.join(SRC_FIXTURES_DIR) - var to = path.join(TEST_DIR) - - fse.copy(from, to, {preserveTimestamps: true}, function () { - FILES.forEach(testFile({preserveTimestamps: true})) - done() - }) - }) - }) - - }) - - function testFile (options) { - return function (file) { - var a = path.join(SRC_FIXTURES_DIR, file) - var b = path.join(TEST_DIR, file) - var fromStat = fs.statSync(a) - var toStat = fs.statSync(b) - if (options.preserveTimestamps) { - assert.deepEqual(toStat.mtime, fromStat.mtime) - assert.deepEqual(toStat.atime, fromStat.atime) - } else { - assert.notEqual(toStat.mtime, fromStat.mtime) - assert.notEqual(toStat.atime, fromStat.atime) - } - } - } -}) From be5c6bb361f3aa35b84ada33579f43821851f6d2 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Mon, 22 Jun 2015 15:50:25 +0200 Subject: [PATCH 3/4] Change time check assertions for modification and access times Signed-off-by: Joern Bernhardt --- lib/copy/__tests__/copy.test.js | 8 ++++---- lib/copy/__tests__/sync/copy-sync.test.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 664a2939..9320aabc 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -219,11 +219,11 @@ describe('fs-extra', function () { var fromStat = fs.statSync(a) var toStat = fs.statSync(b) if (options.preserveTimestamps) { - assert.deepEqual(toStat.mtime, fromStat.mtime) - assert.deepEqual(toStat.atime, fromStat.atime) + assert.strictEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + assert.strictEqual(toStat.atime.getTime(), fromStat.atime.getTime()) } else { - assert.notEqual(toStat.mtime, fromStat.mtime) - assert.notEqual(toStat.atime, fromStat.atime) + assert.notEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + // the access time might actually be the same, so check only modification time } } } diff --git a/lib/copy/__tests__/sync/copy-sync.test.js b/lib/copy/__tests__/sync/copy-sync.test.js index 4dbcaca0..d8d29dbb 100644 --- a/lib/copy/__tests__/sync/copy-sync.test.js +++ b/lib/copy/__tests__/sync/copy-sync.test.js @@ -336,11 +336,11 @@ describe('+ copySync()', function () { var fromStat = fs.statSync(a) var toStat = fs.statSync(b) if (options.preserveTimestamps) { - assert.deepEqual(toStat.mtime, fromStat.mtime) - assert.deepEqual(toStat.atime, fromStat.atime) + assert.strictEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + assert.strictEqual(toStat.atime.getTime(), fromStat.atime.getTime()) } else { - assert.notEqual(toStat.mtime, fromStat.mtime) - assert.notEqual(toStat.atime, fromStat.atime) + assert.notEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + // the access time might actually be the same, so check only modification time } } } From e5b4c45565ee1b6bac55198c46978ae234e3f0bf Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Mon, 22 Jun 2015 23:32:22 +0200 Subject: [PATCH 4/4] Use futimesSync and new utility function to preserve milliseconds Signed-off-by: Joern Bernhardt --- lib/copy/copy-file-sync.js | 8 ++++---- lib/copy/ncp.js | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/copy/copy-file-sync.js b/lib/copy/copy-file-sync.js index 04962158..2dc31ef3 100644 --- a/lib/copy/copy-file-sync.js +++ b/lib/copy/copy-file-sync.js @@ -23,12 +23,12 @@ function copyFileSync (srcFile, destFile, options) { pos += bytesRead } - fs.closeSync(fdr) - fs.closeSync(fdw) - if (preserveTimestamps) { - fs.utimesSync(destFile, stat.atime, stat.mtime) + fs.futimesSync(fdw, stat.atime, stat.mtime) } + + fs.closeSync(fdr) + fs.closeSync(fdw) } module.exports = copyFileSync diff --git a/lib/copy/ncp.js b/lib/copy/ncp.js index 2985d575..c19a7f2e 100644 --- a/lib/copy/ncp.js +++ b/lib/copy/ncp.js @@ -2,6 +2,7 @@ var fs = require('graceful-fs') var path = require('path') +var utimes = require('../util/utimes') function ncp (source, dest, options, callback) { if (!callback) { @@ -108,7 +109,7 @@ function ncp (source, dest, options, callback) { writeStream.once('finish', function () { if (preserveTimestamps) { - fs.utimes(target, file.atime, file.mtime, function (err) { + utimes.utimesMillis(target, file.atime, file.mtime, function (err) { if (err) return onError(err) return doneOne() })