GeezerVison is showing github.com — fs: add flush option to writeFile() functions · nodejs/node@22e3eb6
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/fs.md +18-2Lines changed: 18 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1718,6 +1718,9 @@ All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`. | 1718 | 1718 | <!-- YAML | 1719 | 1719 | added: v10.0.0 | 1720 | 1720 | changes: | | 1721 | + - version: REPLACEME | | 1722 | + pr-url: https://github.com/nodejs/node/pull/50009 | | 1723 | + description: The `flush` option is now supported. | 1721 | 1724 | - version: | 1722 | 1725 | - v15.14.0 | 1723 | 1726 | - v14.18.0 | @@ -1741,6 +1744,9 @@ changes: | 1741 | 1744 | * `encoding` {string|null} **Default:** `'utf8'` | 1742 | 1745 | * `mode` {integer} **Default:** `0o666` | 1743 | 1746 | * `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`. | | 1747 | + * `flush` {boolean} If all data is successfully written to the file, and | | 1748 | + `flush` is `true`, `filehandle.sync()` is used to flush the data. | | 1749 | + **Default:** `false`. | 1744 | 1750 | * `signal` {AbortSignal} allows aborting an in-progress writeFile | 1745 | 1751 | * Returns: {Promise} Fulfills with `undefined` upon success. | 1746 | 1752 |
| @@ -4849,6 +4855,9 @@ details. | 4849 | 4855 | <!-- YAML | 4850 | 4856 | added: v0.1.29 | 4851 | 4857 | changes: | | 4858 | + - version: REPLACEME | | 4859 | + pr-url: https://github.com/nodejs/node/pull/50009 | | 4860 | + description: The `flush` option is now supported. | 4852 | 4861 | - version: v19.0.0 | 4853 | 4862 | pr-url: https://github.com/nodejs/node/pull/42796 | 4854 | 4863 | description: Passing to the `string` parameter an object with an own | @@ -4906,6 +4915,9 @@ changes: | 4906 | 4915 | * `encoding` {string|null} **Default:** `'utf8'` | 4907 | 4916 | * `mode` {integer} **Default:** `0o666` | 4908 | 4917 | * `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`. | | 4918 | + * `flush` {boolean} If all data is successfully written to the file, and | | 4919 | + `flush` is `true`, `fs.fsync()` is used to flush the data. | | 4920 | + **Default:** `false`. | 4909 | 4921 | * `signal` {AbortSignal} allows aborting an in-progress writeFile | 4910 | 4922 | * `callback` {Function} | 4911 | 4923 | * `err` {Error|AggregateError} | @@ -6131,6 +6143,9 @@ this API: [`fs.utimes()`][]. | 6131 | 6143 | <!-- YAML | 6132 | 6144 | added: v0.1.29 | 6133 | 6145 | changes: | | 6146 | + - version: REPLACEME | | 6147 | + pr-url: https://github.com/nodejs/node/pull/50009 | | 6148 | + description: The `flush` option is now supported. | 6134 | 6149 | - version: v19.0.0 | 6135 | 6150 | pr-url: https://github.com/nodejs/node/pull/42796 | 6136 | 6151 | description: Passing to the `data` parameter an object with an own | @@ -6165,8 +6180,9 @@ changes: | 6165 | 6180 | * `encoding` {string|null} **Default:** `'utf8'` | 6166 | 6181 | * `mode` {integer} **Default:** `0o666` | 6167 | 6182 | * `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`. | 6168 | | - | 6169 | | -Returns `undefined`. | | 6183 | + * `flush` {boolean} If all data is successfully written to the file, and | | 6184 | + `flush` is `true`, `fs.fsyncSync()` is used to flush the data. | | 6185 | + Returns `undefined`. | 6170 | 6186 |
| 6171 | 6187 | The `mode` option only affects the newly created file. See [`fs.open()`][] | 6172 | 6188 | for more details. | |
Collapse file Copy file name to clipboardExpand all lines: lib/fs.js +50-9Lines changed: 50 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -2214,7 +2214,7 @@ function lutimesSync(path, atime, mtime) { | 2214 | 2214 | handleErrorFromBinding(ctx); | 2215 | 2215 | } | 2216 | 2216 |
| 2217 | | -function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) { | | 2217 | +function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) { | 2218 | 2218 | if (signal?.aborted) { | 2219 | 2219 | const abortError = new AbortError(undefined, { cause: signal?.reason }); | 2220 | 2220 | if (isUserFd) { | @@ -2237,15 +2237,33 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) { | 2237 | 2237 | }); | 2238 | 2238 | } | 2239 | 2239 | } else if (written === length) { | 2240 | | - if (isUserFd) { | 2241 | | - callback(null); | | 2240 | + if (!flush) { | | 2241 | + if (isUserFd) { | | 2242 | + callback(null); | | 2243 | + } else { | | 2244 | + fs.close(fd, callback); | | 2245 | + } | 2242 | 2246 | } else { | 2243 | | - fs.close(fd, callback); | | 2247 | + fs.fsync(fd, (syncErr) => { | | 2248 | + if (syncErr) { | | 2249 | + if (isUserFd) { | | 2250 | + callback(syncErr); | | 2251 | + } else { | | 2252 | + fs.close(fd, (err) => { | | 2253 | + callback(aggregateTwoErrors(err, syncErr)); | | 2254 | + }); | | 2255 | + } | | 2256 | + } else if (isUserFd) { | | 2257 | + callback(null); | | 2258 | + } else { | | 2259 | + fs.close(fd, callback); | | 2260 | + } | | 2261 | + }); | 2244 | 2262 | } | 2245 | 2263 | } else { | 2246 | 2264 | offset += written; | 2247 | 2265 | length -= written; | 2248 | | - writeAll(fd, isUserFd, buffer, offset, length, signal, callback); | | 2266 | + writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback); | 2249 | 2267 | } | 2250 | 2268 | }); | 2251 | 2269 | } | @@ -2259,14 +2277,23 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) { | 2259 | 2277 | * mode?: number; | 2260 | 2278 | * flag?: string; | 2261 | 2279 | * signal?: AbortSignal; | | 2280 | + * flush?: boolean; | 2262 | 2281 | * } | string} [options] | 2263 | 2282 | * @param {(err?: Error) => any} callback | 2264 | 2283 | * @returns {void} | 2265 | 2284 | */ | 2266 | 2285 | function writeFile(path, data, options, callback) { | 2267 | 2286 | callback = maybeCallback(callback || options); | 2268 | | - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); | | 2287 | + options = getOptions(options, { | | 2288 | + encoding: 'utf8', | | 2289 | + mode: 0o666, | | 2290 | + flag: 'w', | | 2291 | + flush: false, | | 2292 | + }); | 2269 | 2293 | const flag = options.flag || 'w'; | | 2294 | + const flush = options.flush ?? false; | | 2295 | + | | 2296 | + validateBoolean(flush, 'options.flush'); | 2270 | 2297 |
| 2271 | 2298 | if (!isArrayBufferView(data)) { | 2272 | 2299 | validateStringAfterArrayBufferView(data, 'data'); | @@ -2276,7 +2303,7 @@ function writeFile(path, data, options, callback) { | 2276 | 2303 | if (isFd(path)) { | 2277 | 2304 | const isUserFd = true; | 2278 | 2305 | const signal = options.signal; | 2279 | | - writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback); | | 2306 | + writeAll(path, isUserFd, data, 0, data.byteLength, signal, flush, callback); | 2280 | 2307 | return; | 2281 | 2308 | } | 2282 | 2309 |
| @@ -2289,7 +2316,7 @@ function writeFile(path, data, options, callback) { | 2289 | 2316 | } else { | 2290 | 2317 | const isUserFd = false; | 2291 | 2318 | const signal = options.signal; | 2292 | | - writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback); | | 2319 | + writeAll(fd, isUserFd, data, 0, data.byteLength, signal, flush, callback); | 2293 | 2320 | } | 2294 | 2321 | }); | 2295 | 2322 | } | @@ -2302,11 +2329,21 @@ function writeFile(path, data, options, callback) { | 2302 | 2329 | * encoding?: string | null; | 2303 | 2330 | * mode?: number; | 2304 | 2331 | * flag?: string; | | 2332 | + * flush?: boolean; | 2305 | 2333 | * } | string} [options] | 2306 | 2334 | * @returns {void} | 2307 | 2335 | */ | 2308 | 2336 | function writeFileSync(path, data, options) { | 2309 | | - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); | | 2337 | + options = getOptions(options, { | | 2338 | + encoding: 'utf8', | | 2339 | + mode: 0o666, | | 2340 | + flag: 'w', | | 2341 | + flush: false, | | 2342 | + }); | | 2343 | + | | 2344 | + const flush = options.flush ?? false; | | 2345 | + | | 2346 | + validateBoolean(flush, 'options.flush'); | 2310 | 2347 |
| 2311 | 2348 | if (!isArrayBufferView(data)) { | 2312 | 2349 | validateStringAfterArrayBufferView(data, 'data'); | @@ -2326,6 +2363,10 @@ function writeFileSync(path, data, options) { | 2326 | 2363 | offset += written; | 2327 | 2364 | length -= written; | 2328 | 2365 | } | | 2366 | + | | 2367 | + if (flush) { | | 2368 | + fs.fsyncSync(fd); | | 2369 | + } | 2329 | 2370 | } finally { | 2330 | 2371 | if (!isUserFd) fs.closeSync(fd); | 2331 | 2372 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/fs/promises.js +28-3Lines changed: 28 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -413,6 +413,18 @@ async function handleFdClose(fileOpPromise, closeFunc) { | 413 | 413 | ); | 414 | 414 | } | 415 | 415 |
| | 416 | +async function handleFdSync(fileOpPromise, handle) { | | 417 | + return PromisePrototypeThen( | | 418 | + fileOpPromise, | | 419 | + (result) => PromisePrototypeThen( | | 420 | + handle.sync(), | | 421 | + () => result, | | 422 | + (syncError) => PromiseReject(syncError), | | 423 | + ), | | 424 | + (opError) => PromiseReject(opError), | | 425 | + ); | | 426 | +} | | 427 | + | 416 | 428 | async function fsCall(fn, handle, ...args) { | 417 | 429 | assert(handle[kRefs] !== undefined, | 418 | 430 | 'handle must be an instance of FileHandle'); | @@ -1003,8 +1015,16 @@ async function mkdtemp(prefix, options) { | 1003 | 1015 | } | 1004 | 1016 |
| 1005 | 1017 | async function writeFile(path, data, options) { | 1006 | | - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); | | 1018 | + options = getOptions(options, { | | 1019 | + encoding: 'utf8', | | 1020 | + mode: 0o666, | | 1021 | + flag: 'w', | | 1022 | + flush: false, | | 1023 | + }); | 1007 | 1024 | const flag = options.flag || 'w'; | | 1025 | + const flush = options.flush ?? false; | | 1026 | + | | 1027 | + validateBoolean(flush, 'options.flush'); | 1008 | 1028 |
| 1009 | 1029 | if (!isArrayBufferView(data) && !isCustomIterable(data)) { | 1010 | 1030 | validateStringAfterArrayBufferView(data, 'data'); | @@ -1018,8 +1038,13 @@ async function writeFile(path, data, options) { | 1018 | 1038 | checkAborted(options.signal); | 1019 | 1039 |
| 1020 | 1040 | const fd = await open(path, flag, options.mode); | 1021 | | - return handleFdClose( | 1022 | | - writeFileHandle(fd, data, options.signal, options.encoding), fd.close); | | 1041 | + let writeOp = writeFileHandle(fd, data, options.signal, options.encoding); | | 1042 | + | | 1043 | + if (flush) { | | 1044 | + writeOp = handleFdSync(writeOp, fd); | | 1045 | + } | | 1046 | + | | 1047 | + return handleFdClose(writeOp, fd.close); | 1023 | 1048 | } | 1024 | 1049 |
| 1025 | 1050 | function isCustomIterable(obj) { | |
Collapse file Copy file name to clipboard +114Lines changed: 114 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +}); |
|