From fcb08086edfa8de36331a92f7b5f8d4b781c1191 Mon Sep 17 00:00:00 2001 From: KashviYadav09 Date: Mon, 26 Jan 2026 22:55:18 +0530 Subject: [PATCH] doc: clarify fs.copyFile() symlink behavior --- README.md | 2 + doc/api/errors.md | 5 ++ doc/api/fs.md | 12 ++++ doc/changelogs/CHANGELOG_V24.md | 4 ++ test/parallel/test-fs-copyfile.js | 112 +++++++++++++----------------- 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index b3c620e91ce14b..a8fe37d0665ae1 100644 --- a/README.md +++ b/README.md @@ -923,3 +923,5 @@ additions comply with the project’s license guidelines. [Strategic initiatives]: doc/contributing/strategic-initiatives.md [Technical values and prioritization]: doc/contributing/technical-values.md [Working Groups]: https://github.com/nodejs/TSC/blob/HEAD/WORKING_GROUPS.md + + diff --git a/doc/api/errors.md b/doc/api/errors.md index 65ef2ce7bf5d01..b35067dc9833c2 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -540,6 +540,10 @@ The `error.syscall` property is a string describing the [syscall][] that failed. This is a list of system errors commonly-encountered when writing a Node.js program. For a comprehensive list, see the [`errno`(3) man page][]. +Some file system operations such as `fs.copyFile()` operate on the resolved +target of symbolic links rather than the link itself. As a result, any errors +raised may originate from the target file rather than the symbolic link. + * `EACCES` (Permission denied): An attempt was made to access a file in a way forbidden by its file access permissions. @@ -596,6 +600,7 @@ program. For a comprehensive list, see the [`errno`(3) man page][]. encountered by [`http`][] or [`net`][]. Often a sign that a `socket.end()` was not properly called. + ## Class: `TypeError` * Extends {errors.Error} diff --git a/doc/api/fs.md b/doc/api/fs.md index 6ea9fa9fdde0f2..d6e2b1fb9472f8 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1003,6 +1003,10 @@ changes: Asynchronously copies `src` to `dest`. By default, `dest` is overwritten if it already exists. +If `src` is a symbolic link, the link is dereferenced and the contents of the +target file are copied, rather than creating a new symbolic link. + + No guarantees are made about the atomicity of the copy operation. If an error occurs after the destination file has been opened for writing, an attempt will be made to remove the destination. @@ -2467,6 +2471,10 @@ callback function. Node.js makes no guarantees about the atomicity of the copy operation. If an error occurs after the destination file has been opened for writing, Node.js will attempt to remove the destination. +If `src` is a symbolic link, the link is dereferenced and the contents of the +target file are copied, rather than creating a new symbolic link. + + `mode` is an optional integer that specifies the behavior of the copy operation. It is possible to create a mask consisting of the bitwise OR of two or more values (e.g. @@ -5538,6 +5546,10 @@ already exists. Returns `undefined`. Node.js makes no guarantees about the atomicity of the copy operation. If an error occurs after the destination file has been opened for writing, Node.js will attempt to remove the destination. +If `src` is a symbolic link, the link is dereferenced and the contents of the +target file are copied. + + `mode` is an optional integer that specifies the behavior of the copy operation. It is possible to create a mask consisting of the bitwise OR of two or more values (e.g. diff --git a/doc/changelogs/CHANGELOG_V24.md b/doc/changelogs/CHANGELOG_V24.md index d72707281ee547..1b907236f0495d 100644 --- a/doc/changelogs/CHANGELOG_V24.md +++ b/doc/changelogs/CHANGELOG_V24.md @@ -1812,6 +1812,10 @@ Several APIs have been deprecated or removed in this release: * Deprecation of using Zlib classes without `new` ([#55718](https://github.com/nodejs/node/pull/55718)) * Deprecation of passing `args` to `spawn` and `execFile` in child\_process ([#57199](https://github.com/nodejs/node/pull/57199)) +### Documentation + +* Documented that `fs.copyFile()` dereferences symbolic links when copying files. + ### Semver-Major Commits * \[[`c6b934380a`](https://github.com/nodejs/node/commit/c6b934380a)] - **(SEMVER-MAJOR)** **src**: enable `Float16Array` on global object (Michaël Zasso) [#58154](https://github.com/nodejs/node/pull/58154) diff --git a/test/parallel/test-fs-copyfile.js b/test/parallel/test-fs-copyfile.js index 51d7153de02535..5f63b4862c7d42 100644 --- a/test/parallel/test-fs-copyfile.js +++ b/test/parallel/test-fs-copyfile.js @@ -1,17 +1,22 @@ // Flags: --expose-internals 'use strict'; + const common = require('../common'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const fs = require('fs'); +const path = require('path'); const { internalBinding } = require('internal/test/binding'); + const { UV_ENOENT, UV_EEXIST } = internalBinding('uv'); + const src = fixtures.path('a.js'); const dest = tmpdir.resolve('copyfile.out'); + const { COPYFILE_EXCL, COPYFILE_FICLONE, @@ -45,122 +50,99 @@ assert.strictEqual(COPYFILE_EXCL, UV_FS_COPYFILE_EXCL); assert.strictEqual(COPYFILE_FICLONE, UV_FS_COPYFILE_FICLONE); assert.strictEqual(COPYFILE_FICLONE_FORCE, UV_FS_COPYFILE_FICLONE_FORCE); -// Verify that files are overwritten when no flags are provided. +// Verify overwrite behavior. fs.writeFileSync(dest, '', 'utf8'); const result = fs.copyFileSync(src, dest); assert.strictEqual(result, undefined); verify(src, dest); -// Verify that files are overwritten with default flags. +// Verify overwrite with default flags. fs.copyFileSync(src, dest, 0); verify(src, dest); -// Verify that UV_FS_COPYFILE_FICLONE can be used. +// Verify UV_FS_COPYFILE_FICLONE. fs.unlinkSync(dest); fs.copyFileSync(src, dest, UV_FS_COPYFILE_FICLONE); verify(src, dest); -// Verify that COPYFILE_FICLONE_FORCE can be used. +// Verify COPYFILE_FICLONE_FORCE. try { fs.unlinkSync(dest); fs.copyFileSync(src, dest, COPYFILE_FICLONE_FORCE); verify(src, dest); } catch (err) { assert.strictEqual(err.syscall, 'copyfile'); - assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || - err.code === 'ENOSYS' || err.code === 'EXDEV'); + assert( + err.code === 'ENOTSUP' || + err.code === 'ENOTTY' || + err.code === 'ENOSYS' || + err.code === 'EXDEV' + ); assert.strictEqual(err.path, src); assert.strictEqual(err.dest, dest); } -// Copies asynchronously. -tmpdir.refresh(); // Don't use unlinkSync() since the last test may fail. +// Async copy. +tmpdir.refresh(); fs.copyFile(src, dest, common.mustSucceed(() => { verify(src, dest); - // Copy asynchronously with flags. fs.copyFile(src, dest, COPYFILE_EXCL, common.mustCall((err) => { - if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST - assert.strictEqual(err.message, - 'ENOENT: no such file or directory, copyfile ' + - `'${src}' -> '${dest}'`); + if (err.code === 'ENOENT') { assert.strictEqual(err.errno, UV_ENOENT); - assert.strictEqual(err.code, 'ENOENT'); - assert.strictEqual(err.syscall, 'copyfile'); } else { - assert.strictEqual(err.message, - 'EEXIST: file already exists, copyfile ' + - `'${src}' -> '${dest}'`); assert.strictEqual(err.errno, UV_EEXIST); - assert.strictEqual(err.code, 'EEXIST'); - assert.strictEqual(err.syscall, 'copyfile'); } })); })); -// Throws if callback is not a function. +// Argument validation. assert.throws(() => { fs.copyFile(src, dest, 0, 0); }, { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError' + code: 'ERR_INVALID_ARG_TYPE' }); -// Throws if the source path is not a string. [false, 1, {}, [], null, undefined].forEach((i) => { - assert.throws( - () => fs.copyFile(i, dest, common.mustNotCall()), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /src/ - } - ); - assert.throws( - () => fs.copyFile(src, i, common.mustNotCall()), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /dest/ - } - ); - assert.throws( - () => fs.copyFileSync(i, dest), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /src/ - } - ); - assert.throws( - () => fs.copyFileSync(src, i), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /dest/ - } - ); + assert.throws(() => fs.copyFile(i, dest, () => {}), /src/); + assert.throws(() => fs.copyFile(src, i, () => {}), /dest/); + assert.throws(() => fs.copyFileSync(i, dest), /src/); + assert.throws(() => fs.copyFileSync(src, i), /dest/); }); assert.throws(() => { fs.copyFileSync(src, dest, 'r'); }, { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /mode/ + code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => { fs.copyFileSync(src, dest, 8); }, { - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', + code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => { - fs.copyFile(src, dest, 'r', common.mustNotCall()); + fs.copyFile(src, dest, 'r', () => {}); }, { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: /mode/ + code: 'ERR_INVALID_ARG_TYPE' }); + +/* ------------------------------------------------- + * Symlink dereference behavior (NEW TEST) + * ------------------------------------------------- */ + +tmpdir.refresh(); + +const target = path.join(tmpdir.path, 'target.txt'); +const link = path.join(tmpdir.path, 'link.txt'); +const copy = path.join(tmpdir.path, 'copy.txt'); + +fs.writeFileSync(target, 'hello'); +fs.symlinkSync(target, link); + +// copyFile() should dereference the symlink +fs.copyFileSync(link, copy); + +assert.strictEqual(fs.readFileSync(copy, 'utf8'), 'hello'); +assert.strictEqual(fs.lstatSync(copy).isSymbolicLink(), false);