diff --git a/src/js/internal/fs/cp-sync.ts b/src/js/internal/fs/cp-sync.ts index be4de692fcbb..5b75f9eb2e73 100644 --- a/src/js/internal/fs/cp-sync.ts +++ b/src/js/internal/fs/cp-sync.ts @@ -421,17 +421,11 @@ function setDestTimestamps(src, dest) { return utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime); } +// `errorOnExist` is per-file here: an existing destination directory is merged +// into, and only a colliding entry raises ERR_FS_CP_EEXIST (see mayCopyFile). +// node's async cp refuses the directory itself; its cpSync does not. function onDir(srcStat, destStat, src, dest, opts) { if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); - if (opts.errorOnExist && !opts.force) { - throw fsCpEExistError({ - message: `${dest} already exists`, - path: dest, - syscall: "cp", - errno: EEXIST, - code: "EEXIST", - }); - } return copyDir(src, dest, opts); } diff --git a/src/js/internal/fs/cp.ts b/src/js/internal/fs/cp.ts index f7db620f191e..12e74b7d26bb 100644 --- a/src/js/internal/fs/cp.ts +++ b/src/js/internal/fs/cp.ts @@ -314,6 +314,9 @@ async function setDestTimestamps(src, dest) { return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime); } +// node's async cp refuses an existing destination directory outright. Its +// cpSync merges into one and only raises ERR_FS_CP_EEXIST per colliding file, +// so internal/fs/cp-sync.ts onDir deliberately lacks this branch. function onDir(srcStat, destStat, src, dest, opts) { if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts); if (opts.errorOnExist && !opts.force) { diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts index d4e11c89a286..f20ee97ae484 100644 --- a/test/js/node/fs/cp.test.ts +++ b/test/js/node/fs/cp.test.ts @@ -423,6 +423,76 @@ for (const [name, copy] of impls) { }); } +// node applies `errorOnExist` per copied entry in cpSync, so an existing +// destination directory is merged into and only a colliding file raises +// ERR_FS_CP_EEXIST. node's async cp refuses the directory itself; match both. +describe("'force: false' + 'errorOnExist: true' on an existing destination directory", () => { + test("cpSync merges into it and keeps the entries already there", () => { + const basename = tempDirWithFiles("cp", { + "from/a.txt": "a", + "from/nested/b.txt": "b", + "result/other.txt": "keep this", + "result/nested/c.txt": "keep this too", + }); + const from = join(basename, "from"); + const result = join(basename, "result"); + + fs.cpSync(from, result, { recursive: true, force: false, errorOnExist: true }); + + expect({ + result: fs.readdirSync(result).sort(), + nested: fs.readdirSync(join(result, "nested")).sort(), + "a.txt": fs.readFileSync(join(result, "a.txt"), "utf8"), + "nested/b.txt": fs.readFileSync(join(result, "nested", "b.txt"), "utf8"), + "other.txt": fs.readFileSync(join(result, "other.txt"), "utf8"), + "nested/c.txt": fs.readFileSync(join(result, "nested", "c.txt"), "utf8"), + }).toEqual({ + result: ["a.txt", "nested", "other.txt"], + nested: ["b.txt", "c.txt"], + "a.txt": "a", + "nested/b.txt": "b", + "other.txt": "keep this", + "nested/c.txt": "keep this too", + }); + }); + + test("cpSync still throws ERR_FS_CP_EEXIST on a colliding file", () => { + const basename = tempDirWithFiles("cp", { + "from/nested/a.txt": "lose", + "result/nested/a.txt": "win", + }); + const result = join(basename, "result"); + + let err: any; + try { + fs.cpSync(join(basename, "from"), result, { recursive: true, force: false, errorOnExist: true }); + } catch (e) { + err = e; + } + expect(err?.code).toBe("ERR_FS_CP_EEXIST"); + expect(err?.path).toBe(join(result, "nested", "a.txt")); + expect(fs.readFileSync(join(result, "nested", "a.txt"), "utf8")).toBe("win"); + }); + + test("promises.cp refuses the destination directory and copies nothing", async () => { + const basename = tempDirWithFiles("cp", { + "from/a.txt": "a", + "result/other.txt": "keep this", + }); + const result = join(basename, "result"); + + let err: any; + try { + await fs.promises.cp(join(basename, "from"), result, { recursive: true, force: false, errorOnExist: true }); + } catch (e) { + err = e; + } + expect(err?.code).toBe("ERR_FS_CP_EEXIST"); + expect(err?.path).toBe(result); + expect(fs.readdirSync(result)).toEqual(["other.txt"]); + }); +}); + test("cp with missing callback throws", () => { expect(() => { // @ts-expect-error