From b1135e9405a2e1a2980723cb7035316de9be2f17 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Fri, 25 Sep 2026 18:09:06 -0300 Subject: [PATCH] fs: keep timestamps of files skipped by cpSync With `force: false` and `preserveTimestamps: true`, the native directory copy used by `cpSync()` left existing destination files in place but still copied the source timestamps onto them. The JavaScript walk, used by `cpSync()` with a `filter` and by `fs.cp()`, leaves those files untouched. Only copy the timestamps of files that were actually copied, as is already done for their mode. Signed-off-by: Christian Aurich Zanettini Martins --- src/node_file.cc | 2 +- ...p-sync-preserve-timestamps-force-false.mjs | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-fs-cp-sync-preserve-timestamps-force-false.mjs diff --git a/src/node_file.cc b/src/node_file.cc index b624e9da41b..1ebdd3351a7 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -5071,7 +5071,7 @@ CpError CopyDirRecursive(const std::filesystem::path& src_path, return CpError::Std(error, dest_str); } - if (options.preserve_timestamps) { + if (copied && options.preserve_timestamps) { CpError utimes = CopyUtimes(dir_entry.path(), dest_file_path); if (utimes.kind != CpError::kNone) { return utimes; diff --git a/test/parallel/test-fs-cp-sync-preserve-timestamps-force-false.mjs b/test/parallel/test-fs-cp-sync-preserve-timestamps-force-false.mjs new file mode 100644 index 00000000000..7f6ffe008ed --- /dev/null +++ b/test/parallel/test-fs-cp-sync-preserve-timestamps-force-false.mjs @@ -0,0 +1,34 @@ +// This tests that cpSync with force: false and preserveTimestamps: true leaves +// the timestamps of the destination files it skips untouched. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, { recursive: true }); +writeFileSync(join(src, 'file.txt'), 'src', 'utf8'); +utimesSync(join(src, 'file.txt'), 1000, 1000); + +// Without a filter the tree is copied in C++, with one it is walked in +// JavaScript. +for (const filter of [undefined, () => true]) { + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + writeFileSync(join(dest, 'file.txt'), 'dest', 'utf8'); + utimesSync(join(dest, 'file.txt'), 2000, 2000); + + cpSync(src, dest, mustNotMutateObjectDeep({ + filter, + force: false, + preserveTimestamps: true, + recursive: true, + })); + + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'dest'); + assert.strictEqual(statSync(join(dest, 'file.txt')).mtime.getTime(), 2000 * 1000); +}