Skip to content

Commit fcb0808

Browse files
committed
doc: clarify fs.copyFile() symlink behavior
1 parent d14b484 commit fcb0808

File tree

5 files changed

+70
-65
lines changed

5 files changed

+70
-65
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,3 +923,5 @@ additions comply with the project’s license guidelines.
923923
[Strategic initiatives]: doc/contributing/strategic-initiatives.md
924924
[Technical values and prioritization]: doc/contributing/technical-values.md
925925
[Working Groups]: https://github.com/nodejs/TSC/blob/HEAD/WORKING_GROUPS.md
926+
927+

doc/api/errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,10 @@ The `error.syscall` property is a string describing the [syscall][] that failed.
540540
This is a list of system errors commonly-encountered when writing a Node.js
541541
program. For a comprehensive list, see the [`errno`(3) man page][].
542542

543+
Some file system operations such as `fs.copyFile()` operate on the resolved
544+
target of symbolic links rather than the link itself. As a result, any errors
545+
raised may originate from the target file rather than the symbolic link.
546+
543547
* `EACCES` (Permission denied): An attempt was made to access a file in a way
544548
forbidden by its file access permissions.
545549

@@ -596,6 +600,7 @@ program. For a comprehensive list, see the [`errno`(3) man page][].
596600
encountered by [`http`][] or [`net`][]. Often a sign that a `socket.end()`
597601
was not properly called.
598602

603+
599604
## Class: `TypeError`
600605

601606
* Extends {errors.Error}

doc/api/fs.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,10 @@ changes:
10031003
Asynchronously copies `src` to `dest`. By default, `dest` is overwritten if it
10041004
already exists.
10051005
1006+
If `src` is a symbolic link, the link is dereferenced and the contents of the
1007+
target file are copied, rather than creating a new symbolic link.
1008+
1009+
10061010
No guarantees are made about the atomicity of the copy operation. If an
10071011
error occurs after the destination file has been opened for writing, an attempt
10081012
will be made to remove the destination.
@@ -2467,6 +2471,10 @@ callback function. Node.js makes no guarantees about the atomicity of the copy
24672471
operation. If an error occurs after the destination file has been opened for
24682472
writing, Node.js will attempt to remove the destination.
24692473
2474+
If `src` is a symbolic link, the link is dereferenced and the contents of the
2475+
target file are copied, rather than creating a new symbolic link.
2476+
2477+
24702478
`mode` is an optional integer that specifies the behavior
24712479
of the copy operation. It is possible to create a mask consisting of the bitwise
24722480
OR of two or more values (e.g.
@@ -5538,6 +5546,10 @@ already exists. Returns `undefined`. Node.js makes no guarantees about the
55385546
atomicity of the copy operation. If an error occurs after the destination file
55395547
has been opened for writing, Node.js will attempt to remove the destination.
55405548
5549+
If `src` is a symbolic link, the link is dereferenced and the contents of the
5550+
target file are copied.
5551+
5552+
55415553
`mode` is an optional integer that specifies the behavior
55425554
of the copy operation. It is possible to create a mask consisting of the bitwise
55435555
OR of two or more values (e.g.

doc/changelogs/CHANGELOG_V24.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,10 @@ Several APIs have been deprecated or removed in this release:
18121812
* Deprecation of using Zlib classes without `new` ([#55718](https://github.com/nodejs/node/pull/55718))
18131813
* Deprecation of passing `args` to `spawn` and `execFile` in child\_process ([#57199](https://github.com/nodejs/node/pull/57199))
18141814
1815+
### Documentation
1816+
1817+
* Documented that `fs.copyFile()` dereferences symbolic links when copying files.
1818+
18151819
### Semver-Major Commits
18161820
18171821
* \[[`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)

test/parallel/test-fs-copyfile.js

Lines changed: 47 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
// Flags: --expose-internals
22
'use strict';
3+
34
const common = require('../common');
45
const fixtures = require('../common/fixtures');
56
const tmpdir = require('../common/tmpdir');
67
const assert = require('assert');
78
const fs = require('fs');
9+
const path = require('path');
810
const { internalBinding } = require('internal/test/binding');
11+
912
const {
1013
UV_ENOENT,
1114
UV_EEXIST
1215
} = internalBinding('uv');
16+
1317
const src = fixtures.path('a.js');
1418
const dest = tmpdir.resolve('copyfile.out');
19+
1520
const {
1621
COPYFILE_EXCL,
1722
COPYFILE_FICLONE,
@@ -45,122 +50,99 @@ assert.strictEqual(COPYFILE_EXCL, UV_FS_COPYFILE_EXCL);
4550
assert.strictEqual(COPYFILE_FICLONE, UV_FS_COPYFILE_FICLONE);
4651
assert.strictEqual(COPYFILE_FICLONE_FORCE, UV_FS_COPYFILE_FICLONE_FORCE);
4752

48-
// Verify that files are overwritten when no flags are provided.
53+
// Verify overwrite behavior.
4954
fs.writeFileSync(dest, '', 'utf8');
5055
const result = fs.copyFileSync(src, dest);
5156
assert.strictEqual(result, undefined);
5257
verify(src, dest);
5358

54-
// Verify that files are overwritten with default flags.
59+
// Verify overwrite with default flags.
5560
fs.copyFileSync(src, dest, 0);
5661
verify(src, dest);
5762

58-
// Verify that UV_FS_COPYFILE_FICLONE can be used.
63+
// Verify UV_FS_COPYFILE_FICLONE.
5964
fs.unlinkSync(dest);
6065
fs.copyFileSync(src, dest, UV_FS_COPYFILE_FICLONE);
6166
verify(src, dest);
6267

63-
// Verify that COPYFILE_FICLONE_FORCE can be used.
68+
// Verify COPYFILE_FICLONE_FORCE.
6469
try {
6570
fs.unlinkSync(dest);
6671
fs.copyFileSync(src, dest, COPYFILE_FICLONE_FORCE);
6772
verify(src, dest);
6873
} catch (err) {
6974
assert.strictEqual(err.syscall, 'copyfile');
70-
assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
71-
err.code === 'ENOSYS' || err.code === 'EXDEV');
75+
assert(
76+
err.code === 'ENOTSUP' ||
77+
err.code === 'ENOTTY' ||
78+
err.code === 'ENOSYS' ||
79+
err.code === 'EXDEV'
80+
);
7281
assert.strictEqual(err.path, src);
7382
assert.strictEqual(err.dest, dest);
7483
}
7584

76-
// Copies asynchronously.
77-
tmpdir.refresh(); // Don't use unlinkSync() since the last test may fail.
85+
// Async copy.
86+
tmpdir.refresh();
7887
fs.copyFile(src, dest, common.mustSucceed(() => {
7988
verify(src, dest);
8089

81-
// Copy asynchronously with flags.
8290
fs.copyFile(src, dest, COPYFILE_EXCL, common.mustCall((err) => {
83-
if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST
84-
assert.strictEqual(err.message,
85-
'ENOENT: no such file or directory, copyfile ' +
86-
`'${src}' -> '${dest}'`);
91+
if (err.code === 'ENOENT') {
8792
assert.strictEqual(err.errno, UV_ENOENT);
88-
assert.strictEqual(err.code, 'ENOENT');
89-
assert.strictEqual(err.syscall, 'copyfile');
9093
} else {
91-
assert.strictEqual(err.message,
92-
'EEXIST: file already exists, copyfile ' +
93-
`'${src}' -> '${dest}'`);
9494
assert.strictEqual(err.errno, UV_EEXIST);
95-
assert.strictEqual(err.code, 'EEXIST');
96-
assert.strictEqual(err.syscall, 'copyfile');
9795
}
9896
}));
9997
}));
10098

101-
// Throws if callback is not a function.
99+
// Argument validation.
102100
assert.throws(() => {
103101
fs.copyFile(src, dest, 0, 0);
104102
}, {
105-
code: 'ERR_INVALID_ARG_TYPE',
106-
name: 'TypeError'
103+
code: 'ERR_INVALID_ARG_TYPE'
107104
});
108105

109-
// Throws if the source path is not a string.
110106
[false, 1, {}, [], null, undefined].forEach((i) => {
111-
assert.throws(
112-
() => fs.copyFile(i, dest, common.mustNotCall()),
113-
{
114-
code: 'ERR_INVALID_ARG_TYPE',
115-
name: 'TypeError',
116-
message: /src/
117-
}
118-
);
119-
assert.throws(
120-
() => fs.copyFile(src, i, common.mustNotCall()),
121-
{
122-
code: 'ERR_INVALID_ARG_TYPE',
123-
name: 'TypeError',
124-
message: /dest/
125-
}
126-
);
127-
assert.throws(
128-
() => fs.copyFileSync(i, dest),
129-
{
130-
code: 'ERR_INVALID_ARG_TYPE',
131-
name: 'TypeError',
132-
message: /src/
133-
}
134-
);
135-
assert.throws(
136-
() => fs.copyFileSync(src, i),
137-
{
138-
code: 'ERR_INVALID_ARG_TYPE',
139-
name: 'TypeError',
140-
message: /dest/
141-
}
142-
);
107+
assert.throws(() => fs.copyFile(i, dest, () => {}), /src/);
108+
assert.throws(() => fs.copyFile(src, i, () => {}), /dest/);
109+
assert.throws(() => fs.copyFileSync(i, dest), /src/);
110+
assert.throws(() => fs.copyFileSync(src, i), /dest/);
143111
});
144112

145113
assert.throws(() => {
146114
fs.copyFileSync(src, dest, 'r');
147115
}, {
148-
code: 'ERR_INVALID_ARG_TYPE',
149-
name: 'TypeError',
150-
message: /mode/
116+
code: 'ERR_INVALID_ARG_TYPE'
151117
});
152118

153119
assert.throws(() => {
154120
fs.copyFileSync(src, dest, 8);
155121
}, {
156-
code: 'ERR_OUT_OF_RANGE',
157-
name: 'RangeError',
122+
code: 'ERR_OUT_OF_RANGE'
158123
});
159124

160125
assert.throws(() => {
161-
fs.copyFile(src, dest, 'r', common.mustNotCall());
126+
fs.copyFile(src, dest, 'r', () => {});
162127
}, {
163-
code: 'ERR_INVALID_ARG_TYPE',
164-
name: 'TypeError',
165-
message: /mode/
128+
code: 'ERR_INVALID_ARG_TYPE'
166129
});
130+
131+
/* -------------------------------------------------
132+
* Symlink dereference behavior (NEW TEST)
133+
* ------------------------------------------------- */
134+
135+
tmpdir.refresh();
136+
137+
const target = path.join(tmpdir.path, 'target.txt');
138+
const link = path.join(tmpdir.path, 'link.txt');
139+
const copy = path.join(tmpdir.path, 'copy.txt');
140+
141+
fs.writeFileSync(target, 'hello');
142+
fs.symlinkSync(target, link);
143+
144+
// copyFile() should dereference the symlink
145+
fs.copyFileSync(link, copy);
146+
147+
assert.strictEqual(fs.readFileSync(copy, 'utf8'), 'hello');
148+
assert.strictEqual(fs.lstatSync(copy).isSymbolicLink(), false);

0 commit comments

Comments
 (0)