diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index f2b00f3f82bb..ecd06dfd6002 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -179,6 +179,9 @@ function copyDir(src, dest, opts, mkDir, srcMode) { } finally { dir.closeSync(); + if (opts.preserveTimestamps) { + setDestTimestamps(src, dest); + } if (srcMode !== undefined) { setDestMode(dest, srcMode); } diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 2eb4f0ffdd83..35fcefb12d10 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -311,12 +311,18 @@ async function onDir(srcStat, destStat, src, dest, opts) { code: 'EEXIST', }); } - return copyDir(src, dest, opts); + await copyDir(src, dest, opts); + if (opts.preserveTimestamps) { + await setDestTimestamps(src, dest); + } } async function mkDirAndCopy(srcMode, src, dest, opts) { await mkdir(dest); await copyDir(src, dest, opts); + if (opts.preserveTimestamps) { + await setDestTimestamps(src, dest); + } return setDestMode(dest, srcMode); } diff --git a/test/parallel/test-fs-cp-async-preserve-timestamps-dir.mjs b/test/parallel/test-fs-cp-async-preserve-timestamps-dir.mjs new file mode 100644 index 000000000000..ae93a7427e8a --- /dev/null +++ b/test/parallel/test-fs-cp-async-preserve-timestamps-dir.mjs @@ -0,0 +1,43 @@ +// This tests that cp() preserves directory timestamps +// when preserveTimestamps is true. +import { mustCall } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync, writeFileSync, utimesSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +// Build a source tree with a known past timestamp on directories. +const src = nextdir(); +mkdirSync(join(src, 'subdir'), { recursive: true }); +writeFileSync(join(src, 'subdir', 'file.txt'), 'hello'); + +const pastDate = new Date('2020-01-01T00:00:00Z'); +utimesSync(join(src, 'subdir', 'file.txt'), pastDate, pastDate); +utimesSync(join(src, 'subdir'), pastDate, pastDate); +utimesSync(src, pastDate, pastDate); + +// Copy with preserveTimestamps. +const dest = nextdir(); +cp(src, dest, { + recursive: true, + preserveTimestamps: true, +}, mustCall((err) => { + assert.strictEqual(err, null); + + // Verify file timestamps are preserved (existing behaviour). + const srcFileStat = statSync(join(src, 'subdir', 'file.txt')); + const destFileStat = statSync(join(dest, 'subdir', 'file.txt')); + assert.strictEqual(srcFileStat.mtime.getTime(), destFileStat.mtime.getTime()); + + // Verify directory timestamps are preserved (the bug fix). + const srcDirStat = statSync(join(src, 'subdir')); + const destDirStat = statSync(join(dest, 'subdir')); + assert.strictEqual(srcDirStat.mtime.getTime(), destDirStat.mtime.getTime()); + + const srcRootStat = statSync(src); + const destRootStat = statSync(dest); + assert.strictEqual(srcRootStat.mtime.getTime(), destRootStat.mtime.getTime()); +})); diff --git a/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs b/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs new file mode 100644 index 000000000000..20d889075988 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs @@ -0,0 +1,42 @@ +// This tests that cpSync with a filter preserves directory timestamps +// when preserveTimestamps is true. +import '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, utimesSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +// Build a source tree with a known past timestamp on directories. +const src = nextdir(); +mkdirSync(join(src, 'subdir'), { recursive: true }); +writeFileSync(join(src, 'subdir', 'file.txt'), 'hello'); + +const pastDate = new Date('2020-01-01T00:00:00Z'); +utimesSync(join(src, 'subdir', 'file.txt'), pastDate, pastDate); +utimesSync(join(src, 'subdir'), pastDate, pastDate); +utimesSync(src, pastDate, pastDate); + +// Copy with preserveTimestamps and a filter (to exercise the JS fallback path). +const dest = nextdir(); +cpSync(src, dest, { + recursive: true, + preserveTimestamps: true, + filter: () => true, +}); + +// Verify file timestamps are preserved (existing behaviour). +const srcFileStat = statSync(join(src, 'subdir', 'file.txt')); +const destFileStat = statSync(join(dest, 'subdir', 'file.txt')); +assert.strictEqual(srcFileStat.mtime.getTime(), destFileStat.mtime.getTime()); + +// Verify directory timestamps are preserved (the bug fix). +const srcDirStat = statSync(join(src, 'subdir')); +const destDirStat = statSync(join(dest, 'subdir')); +assert.strictEqual(srcDirStat.mtime.getTime(), destDirStat.mtime.getTime()); + +const srcRootStat = statSync(src); +const destRootStat = statSync(dest); +assert.strictEqual(srcRootStat.mtime.getTime(), destRootStat.mtime.getTime());