Skip to content

Commit e01c1d7

Browse files
authored
fs: add flush option to writeFile() functions
This commit adds a 'flush' option to the fs.writeFile family of functions. Refs: #49886 PR-URL: #50009 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: LiviaMedeiros <[email protected]> Reviewed-By: Daijiro Wachi <[email protected]>
1 parent 557044a commit e01c1d7

File tree

4 files changed

+210
-14
lines changed

4 files changed

+210
-14
lines changed

doc/api/fs.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,9 @@ All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.
17421742
<!-- YAML
17431743
added: v10.0.0
17441744
changes:
1745+
- version: REPLACEME
1746+
pr-url: https://github.com/nodejs/node/pull/50009
1747+
description: The `flush` option is now supported.
17451748
- version:
17461749
- v15.14.0
17471750
- v14.18.0
@@ -1765,6 +1768,9 @@ changes:
17651768
* `encoding` {string|null} **Default:** `'utf8'`
17661769
* `mode` {integer} **Default:** `0o666`
17671770
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
1771+
* `flush` {boolean} If all data is successfully written to the file, and
1772+
`flush` is `true`, `filehandle.sync()` is used to flush the data.
1773+
**Default:** `false`.
17681774
* `signal` {AbortSignal} allows aborting an in-progress writeFile
17691775
* Returns: {Promise} Fulfills with `undefined` upon success.
17701776
@@ -4879,6 +4885,9 @@ details.
48794885
<!-- YAML
48804886
added: v0.1.29
48814887
changes:
4888+
- version: REPLACEME
4889+
pr-url: https://github.com/nodejs/node/pull/50009
4890+
description: The `flush` option is now supported.
48824891
- version: v19.0.0
48834892
pr-url: https://github.com/nodejs/node/pull/42796
48844893
description: Passing to the `string` parameter an object with an own
@@ -4936,6 +4945,9 @@ changes:
49364945
* `encoding` {string|null} **Default:** `'utf8'`
49374946
* `mode` {integer} **Default:** `0o666`
49384947
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
4948+
* `flush` {boolean} If all data is successfully written to the file, and
4949+
`flush` is `true`, `fs.fsync()` is used to flush the data.
4950+
**Default:** `false`.
49394951
* `signal` {AbortSignal} allows aborting an in-progress writeFile
49404952
* `callback` {Function}
49414953
* `err` {Error|AggregateError}
@@ -6167,6 +6179,9 @@ this API: [`fs.utimes()`][].
61676179
<!-- YAML
61686180
added: v0.1.29
61696181
changes:
6182+
- version: REPLACEME
6183+
pr-url: https://github.com/nodejs/node/pull/50009
6184+
description: The `flush` option is now supported.
61706185
- version: v19.0.0
61716186
pr-url: https://github.com/nodejs/node/pull/42796
61726187
description: Passing to the `data` parameter an object with an own
@@ -6201,8 +6216,9 @@ changes:
62016216
* `encoding` {string|null} **Default:** `'utf8'`
62026217
* `mode` {integer} **Default:** `0o666`
62036218
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
6204-
6205-
Returns `undefined`.
6219+
* `flush` {boolean} If all data is successfully written to the file, and
6220+
`flush` is `true`, `fs.fsyncSync()` is used to flush the data.
6221+
Returns `undefined`.
62066222

62076223
The `mode` option only affects the newly created file. See [`fs.open()`][]
62086224
for more details.

lib/fs.js

+50-9
Original file line numberDiff line numberDiff line change
@@ -2216,7 +2216,7 @@ function lutimesSync(path, atime, mtime) {
22162216
handleErrorFromBinding(ctx);
22172217
}
22182218

2219-
function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
2219+
function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) {
22202220
if (signal?.aborted) {
22212221
const abortError = new AbortError(undefined, { cause: signal?.reason });
22222222
if (isUserFd) {
@@ -2239,15 +2239,33 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
22392239
});
22402240
}
22412241
} else if (written === length) {
2242-
if (isUserFd) {
2243-
callback(null);
2242+
if (!flush) {
2243+
if (isUserFd) {
2244+
callback(null);
2245+
} else {
2246+
fs.close(fd, callback);
2247+
}
22442248
} else {
2245-
fs.close(fd, callback);
2249+
fs.fsync(fd, (syncErr) => {
2250+
if (syncErr) {
2251+
if (isUserFd) {
2252+
callback(syncErr);
2253+
} else {
2254+
fs.close(fd, (err) => {
2255+
callback(aggregateTwoErrors(err, syncErr));
2256+
});
2257+
}
2258+
} else if (isUserFd) {
2259+
callback(null);
2260+
} else {
2261+
fs.close(fd, callback);
2262+
}
2263+
});
22462264
}
22472265
} else {
22482266
offset += written;
22492267
length -= written;
2250-
writeAll(fd, isUserFd, buffer, offset, length, signal, callback);
2268+
writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback);
22512269
}
22522270
});
22532271
}
@@ -2261,14 +2279,23 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
22612279
* mode?: number;
22622280
* flag?: string;
22632281
* signal?: AbortSignal;
2282+
* flush?: boolean;
22642283
* } | string} [options]
22652284
* @param {(err?: Error) => any} callback
22662285
* @returns {void}
22672286
*/
22682287
function writeFile(path, data, options, callback) {
22692288
callback = maybeCallback(callback || options);
2270-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
2289+
options = getOptions(options, {
2290+
encoding: 'utf8',
2291+
mode: 0o666,
2292+
flag: 'w',
2293+
flush: false,
2294+
});
22712295
const flag = options.flag || 'w';
2296+
const flush = options.flush ?? false;
2297+
2298+
validateBoolean(flush, 'options.flush');
22722299

22732300
if (!isArrayBufferView(data)) {
22742301
validateStringAfterArrayBufferView(data, 'data');
@@ -2278,7 +2305,7 @@ function writeFile(path, data, options, callback) {
22782305
if (isFd(path)) {
22792306
const isUserFd = true;
22802307
const signal = options.signal;
2281-
writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback);
2308+
writeAll(path, isUserFd, data, 0, data.byteLength, signal, flush, callback);
22822309
return;
22832310
}
22842311

@@ -2291,7 +2318,7 @@ function writeFile(path, data, options, callback) {
22912318
} else {
22922319
const isUserFd = false;
22932320
const signal = options.signal;
2294-
writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback);
2321+
writeAll(fd, isUserFd, data, 0, data.byteLength, signal, flush, callback);
22952322
}
22962323
});
22972324
}
@@ -2304,11 +2331,21 @@ function writeFile(path, data, options, callback) {
23042331
* encoding?: string | null;
23052332
* mode?: number;
23062333
* flag?: string;
2334+
* flush?: boolean;
23072335
* } | string} [options]
23082336
* @returns {void}
23092337
*/
23102338
function writeFileSync(path, data, options) {
2311-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
2339+
options = getOptions(options, {
2340+
encoding: 'utf8',
2341+
mode: 0o666,
2342+
flag: 'w',
2343+
flush: false,
2344+
});
2345+
2346+
const flush = options.flush ?? false;
2347+
2348+
validateBoolean(flush, 'options.flush');
23122349

23132350
if (!isArrayBufferView(data)) {
23142351
validateStringAfterArrayBufferView(data, 'data');
@@ -2328,6 +2365,10 @@ function writeFileSync(path, data, options) {
23282365
offset += written;
23292366
length -= written;
23302367
}
2368+
2369+
if (flush) {
2370+
fs.fsyncSync(fd);
2371+
}
23312372
} finally {
23322373
if (!isUserFd) fs.closeSync(fd);
23332374
}

lib/internal/fs/promises.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,18 @@ async function handleFdClose(fileOpPromise, closeFunc) {
414414
);
415415
}
416416

417+
async function handleFdSync(fileOpPromise, handle) {
418+
return PromisePrototypeThen(
419+
fileOpPromise,
420+
(result) => PromisePrototypeThen(
421+
handle.sync(),
422+
() => result,
423+
(syncError) => PromiseReject(syncError),
424+
),
425+
(opError) => PromiseReject(opError),
426+
);
427+
}
428+
417429
async function fsCall(fn, handle, ...args) {
418430
assert(handle[kRefs] !== undefined,
419431
'handle must be an instance of FileHandle');
@@ -1007,8 +1019,16 @@ async function mkdtemp(prefix, options) {
10071019
}
10081020

10091021
async function writeFile(path, data, options) {
1010-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
1022+
options = getOptions(options, {
1023+
encoding: 'utf8',
1024+
mode: 0o666,
1025+
flag: 'w',
1026+
flush: false,
1027+
});
10111028
const flag = options.flag || 'w';
1029+
const flush = options.flush ?? false;
1030+
1031+
validateBoolean(flush, 'options.flush');
10121032

10131033
if (!isArrayBufferView(data) && !isCustomIterable(data)) {
10141034
validateStringAfterArrayBufferView(data, 'data');
@@ -1022,8 +1042,13 @@ async function writeFile(path, data, options) {
10221042
checkAborted(options.signal);
10231043

10241044
const fd = await open(path, flag, options.mode);
1025-
return handleFdClose(
1026-
writeFileHandle(fd, data, options.signal, options.encoding), fd.close);
1045+
let writeOp = writeFileHandle(fd, data, options.signal, options.encoding);
1046+
1047+
if (flush) {
1048+
writeOp = handleFdSync(writeOp, fd);
1049+
}
1050+
1051+
return handleFdClose(writeOp, fd.close);
10271052
}
10281053

10291054
function isCustomIterable(obj) {
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
const common = require('../common');
3+
const tmpdir = require('../common/tmpdir');
4+
const assert = require('node:assert');
5+
const fs = require('node:fs');
6+
const fsp = require('node:fs/promises');
7+
const test = require('node:test');
8+
const data = 'foo';
9+
let cnt = 0;
10+
11+
function nextFile() {
12+
return tmpdir.resolve(`${cnt++}.out`);
13+
}
14+
15+
tmpdir.refresh();
16+
17+
test('synchronous version', async (t) => {
18+
await t.test('validation', (t) => {
19+
for (const v of ['true', '', 0, 1, [], {}, Symbol()]) {
20+
assert.throws(() => {
21+
fs.writeFileSync(nextFile(), data, { flush: v });
22+
}, { code: 'ERR_INVALID_ARG_TYPE' });
23+
}
24+
});
25+
26+
await t.test('performs flush', (t) => {
27+
const spy = t.mock.method(fs, 'fsyncSync');
28+
const file = nextFile();
29+
fs.writeFileSync(file, data, { flush: true });
30+
const calls = spy.mock.calls;
31+
assert.strictEqual(calls.length, 1);
32+
assert.strictEqual(calls[0].result, undefined);
33+
assert.strictEqual(calls[0].error, undefined);
34+
assert.strictEqual(calls[0].arguments.length, 1);
35+
assert.strictEqual(typeof calls[0].arguments[0], 'number');
36+
assert.strictEqual(fs.readFileSync(file, 'utf8'), data);
37+
});
38+
39+
await t.test('does not perform flush', (t) => {
40+
const spy = t.mock.method(fs, 'fsyncSync');
41+
42+
for (const v of [undefined, null, false]) {
43+
const file = nextFile();
44+
fs.writeFileSync(file, data, { flush: v });
45+
assert.strictEqual(fs.readFileSync(file, 'utf8'), data);
46+
}
47+
48+
assert.strictEqual(spy.mock.calls.length, 0);
49+
});
50+
});
51+
52+
test('callback version', async (t) => {
53+
await t.test('validation', (t) => {
54+
for (const v of ['true', '', 0, 1, [], {}, Symbol()]) {
55+
assert.throws(() => {
56+
fs.writeFileSync(nextFile(), data, { flush: v });
57+
}, { code: 'ERR_INVALID_ARG_TYPE' });
58+
}
59+
});
60+
61+
await t.test('performs flush', (t, done) => {
62+
const spy = t.mock.method(fs, 'fsync');
63+
const file = nextFile();
64+
fs.writeFile(file, data, { flush: true }, common.mustSucceed(() => {
65+
const calls = spy.mock.calls;
66+
assert.strictEqual(calls.length, 1);
67+
assert.strictEqual(calls[0].result, undefined);
68+
assert.strictEqual(calls[0].error, undefined);
69+
assert.strictEqual(calls[0].arguments.length, 2);
70+
assert.strictEqual(typeof calls[0].arguments[0], 'number');
71+
assert.strictEqual(typeof calls[0].arguments[1], 'function');
72+
assert.strictEqual(fs.readFileSync(file, 'utf8'), data);
73+
done();
74+
}));
75+
});
76+
77+
await t.test('does not perform flush', (t, done) => {
78+
const values = [undefined, null, false];
79+
const spy = t.mock.method(fs, 'fsync');
80+
let cnt = 0;
81+
82+
for (const v of values) {
83+
const file = nextFile();
84+
85+
fs.writeFile(file, data, { flush: v }, common.mustSucceed(() => {
86+
assert.strictEqual(fs.readFileSync(file, 'utf8'), data);
87+
cnt++;
88+
89+
if (cnt === values.length) {
90+
assert.strictEqual(spy.mock.calls.length, 0);
91+
done();
92+
}
93+
}));
94+
}
95+
});
96+
});
97+
98+
test('promise based version', async (t) => {
99+
await t.test('validation', async (t) => {
100+
for (const v of ['true', '', 0, 1, [], {}, Symbol()]) {
101+
await assert.rejects(() => {
102+
return fsp.writeFile(nextFile(), data, { flush: v });
103+
}, { code: 'ERR_INVALID_ARG_TYPE' });
104+
}
105+
});
106+
107+
await t.test('success path', async (t) => {
108+
for (const v of [undefined, null, false, true]) {
109+
const file = nextFile();
110+
await fsp.writeFile(file, data, { flush: v });
111+
assert.strictEqual(await fsp.readFile(file, 'utf8'), data);
112+
}
113+
});
114+
});

0 commit comments

Comments
 (0)