From 321146b9bf8ad909304aeb24872be4723bf25ea1 Mon Sep 17 00:00:00 2001 From: Abhinandan Kumar Date: Tue, 25 Aug 2026 17:12:15 +0530 Subject: [PATCH] fs: preserve directory timestamps in cp This fixes an issue where fs.cp and fs.cpSync (when a filter is provided) correctly restore mtime and atime on copied files when preserveTimestamps: true is passed, but skip restoring them for directories. The fix applies the existing setDestTimestamps helper to directories, ensuring it is called after directory creation/copying but before any modes are restored (since restoring a read-only mode would prevent timestamp modification). Note: This fix covers fs.cp() (async, all cases) and fs.cpSync() when a filter is provided. The native fast path used by cpSync() without a filter (CpSyncCopyDir in src/node_file.cc) has the identical gap and is left for a separate follow-up contribution, since it requires native code changes and a different review path. Signed-off-by: Abhinandan Kumar --- lib/internal/fs/cp/cp-sync.js | 3 ++ lib/internal/fs/cp/cp.js | 8 +++- ...st-fs-cp-async-preserve-timestamps-dir.mjs | 43 +++++++++++++++++++ ...est-fs-cp-sync-preserve-timestamps-dir.mjs | 42 ++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-fs-cp-async-preserve-timestamps-dir.mjs create mode 100644 test/parallel/test-fs-cp-sync-preserve-timestamps-dir.mjs 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());