GeezerVison is showing github.com — fs: support abortsignal in writeFile · nodejs/node@219cd00
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 +60-1Lines changed: 60 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4375,6 +4375,10 @@ details. | 4375 | 4375 | <!-- YAML | 4376 | 4376 | added: v0.1.29 | 4377 | 4377 | changes: | | 4378 | + - version: REPLACEME | | 4379 | + pr-url: https://github.com/nodejs/node/pull/35993 | | 4380 | + description: The options argument may include an AbortSignal to abort an | | 4381 | + ongoing writeFile request. | 4378 | 4382 | - version: v14.12.0 | 4379 | 4383 | pr-url: https://github.com/nodejs/node/pull/34993 | 4380 | 4384 | description: The `data` parameter will stringify an object with an | @@ -4409,6 +4413,7 @@ changes: | 4409 | 4413 | * `encoding` {string|null} **Default:** `'utf8'` | 4410 | 4414 | * `mode` {integer} **Default:** `0o666` | 4411 | 4415 | * `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`. | | 4416 | + * `signal` {AbortSignal} allows aborting an in-progress writeFile | 4412 | 4417 | * `callback` {Function} | 4413 | 4418 | * `err` {Error} | 4414 | 4419 |
| @@ -4440,6 +4445,28 @@ It is unsafe to use `fs.writeFile()` multiple times on the same file without | 4440 | 4445 | waiting for the callback. For this scenario, [`fs.createWriteStream()`][] is | 4441 | 4446 | recommended. | 4442 | 4447 |
| | 4448 | +Similarly to `fs.readFile` - `fs.writeFile` is a convenience method that | | 4449 | +performs multiple `write` calls internally to write the buffer passed to it. | | 4450 | +For performance sensitive code consider using [`fs.createWriteStream()`][]. | | 4451 | + | | 4452 | +It is possible to use an {AbortSignal} to cancel an `fs.writeFile()`. | | 4453 | +Cancelation is "best effort", and some amount of data is likely still | | 4454 | +to be written. | | 4455 | + | | 4456 | +```js | | 4457 | +const controller = new AbortController(); | | 4458 | +const { signal } = controller; | | 4459 | +const data = new Uint8Array(Buffer.from('Hello Node.js')); | | 4460 | +fs.writeFile('message.txt', data, { signal }, (err) => { | | 4461 | + // When a request is aborted - the callback is called with an AbortError | | 4462 | +}); | | 4463 | +// When the request should be aborted | | 4464 | +controller.abort(); | | 4465 | +``` | | 4466 | + | | 4467 | +Aborting an ongoing request does not abort individual operating | | 4468 | +system requests but rather the internal buffering `fs.writeFile` performs. | | 4469 | + | 4443 | 4470 | ### Using `fs.writeFile()` with file descriptors | 4444 | 4471 |
| 4445 | 4472 | When `file` is a file descriptor, the behavior is almost identical to directly | @@ -5684,6 +5711,10 @@ The `atime` and `mtime` arguments follow these rules: | 5684 | 5711 | <!-- YAML | 5685 | 5712 | added: v10.0.0 | 5686 | 5713 | changes: | | 5714 | + - version: REPLACEME | | 5715 | + pr-url: https://github.com/nodejs/node/pull/35993 | | 5716 | + description: The options argument may include an AbortSignal to abort an | | 5717 | + ongoing writeFile request. | 5687 | 5718 | - version: v14.12.0 | 5688 | 5719 | pr-url: https://github.com/nodejs/node/pull/34993 | 5689 | 5720 | description: The `data` parameter will stringify an object with an | @@ -5700,6 +5731,7 @@ changes: | 5700 | 5731 | * `encoding` {string|null} **Default:** `'utf8'` | 5701 | 5732 | * `mode` {integer} **Default:** `0o666` | 5702 | 5733 | * `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`. | | 5734 | + * `signal` {AbortSignal} allows aborting an in-progress writeFile | 5703 | 5735 | * Returns: {Promise} | 5704 | 5736 |
| 5705 | 5737 | Asynchronously writes data to a file, replacing the file if it already exists. | @@ -5713,7 +5745,34 @@ If `options` is a string, then it specifies the encoding. | 5713 | 5745 | Any specified `FileHandle` has to support writing. | 5714 | 5746 |
| 5715 | 5747 | It is unsafe to use `fsPromises.writeFile()` multiple times on the same file | 5716 | | -without waiting for the `Promise` to be resolved (or rejected). | | 5748 | +without waiting for the `Promise` to be fulfilled (or rejected). | | 5749 | + | | 5750 | +Similarly to `fsPromises.readFile` - `fsPromises.writeFile` is a convenience | | 5751 | +method that performs multiple `write` calls internally to write the buffer | | 5752 | +passed to it. For performance sensitive code consider using | | 5753 | +[`fs.createWriteStream()`][]. | | 5754 | + | | 5755 | +It is possible to use an {AbortSignal} to cancel an `fsPromises.writeFile()`. | | 5756 | +Cancelation is "best effort", and some amount of data is likely still | | 5757 | +to be written. | | 5758 | + | | 5759 | +```js | | 5760 | +const controller = new AbortController(); | | 5761 | +const { signal } = controller; | | 5762 | +const data = new Uint8Array(Buffer.from('Hello Node.js')); | | 5763 | +(async () => { | | 5764 | + try { | | 5765 | + await fs.writeFile('message.txt', data, { signal }); | | 5766 | + } catch (err) { | | 5767 | + // When a request is aborted - err is an AbortError | | 5768 | + } | | 5769 | +})(); | | 5770 | +// When the request should be aborted | | 5771 | +controller.abort(); | | 5772 | +``` | | 5773 | + | | 5774 | +Aborting an ongoing request does not abort individual operating | | 5775 | +system requests but rather the internal buffering `fs.writeFile` performs. | 5717 | 5776 |
| 5718 | 5777 | ## FS constants | 5719 | 5778 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/fs.js +22-4Lines changed: 22 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -71,6 +71,7 @@ const { | 71 | 71 | ERR_INVALID_CALLBACK, | 72 | 72 | ERR_FEATURE_UNAVAILABLE_ON_PLATFORM | 73 | 73 | }, | | 74 | + hideStackFrames, | 74 | 75 | uvErrmapGet, | 75 | 76 | uvException | 76 | 77 | } = require('internal/errors'); | @@ -134,6 +135,13 @@ let ReadStream; | 134 | 135 | let WriteStream; | 135 | 136 | let rimraf; | 136 | 137 | let rimrafSync; | | 138 | +let DOMException; | | 139 | + | | 140 | +const lazyDOMException = hideStackFrames((message, name) => { | | 141 | + if (DOMException === undefined) | | 142 | + DOMException = internalBinding('messaging').DOMException; | | 143 | + return new DOMException(message, name); | | 144 | +}); | 137 | 145 |
| 138 | 146 | // These have to be separate because of how graceful-fs happens to do it's | 139 | 147 | // monkeypatching. | @@ -1425,7 +1433,11 @@ function lutimesSync(path, atime, mtime) { | 1425 | 1433 | handleErrorFromBinding(ctx); | 1426 | 1434 | } | 1427 | 1435 |
| 1428 | | -function writeAll(fd, isUserFd, buffer, offset, length, callback) { | | 1436 | +function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) { | | 1437 | + if (signal?.aborted) { | | 1438 | + callback(lazyDOMException('The operation was aborted', 'AbortError')); | | 1439 | + return; | | 1440 | + } | 1429 | 1441 | // write(fd, buffer, offset, length, position, callback) | 1430 | 1442 | fs.write(fd, buffer, offset, length, null, (writeErr, written) => { | 1431 | 1443 | if (writeErr) { | @@ -1445,7 +1457,7 @@ function writeAll(fd, isUserFd, buffer, offset, length, callback) { | 1445 | 1457 | } else { | 1446 | 1458 | offset += written; | 1447 | 1459 | length -= written; | 1448 | | - writeAll(fd, isUserFd, buffer, offset, length, callback); | | 1460 | + writeAll(fd, isUserFd, buffer, offset, length, signal, callback); | 1449 | 1461 | } | 1450 | 1462 | }); | 1451 | 1463 | } | @@ -1462,16 +1474,22 @@ function writeFile(path, data, options, callback) { | 1462 | 1474 |
| 1463 | 1475 | if (isFd(path)) { | 1464 | 1476 | const isUserFd = true; | 1465 | | - writeAll(path, isUserFd, data, 0, data.byteLength, callback); | | 1477 | + const signal = options.signal; | | 1478 | + writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback); | 1466 | 1479 | return; | 1467 | 1480 | } | 1468 | 1481 |
| | 1482 | + if (options.signal?.aborted) { | | 1483 | + callback(lazyDOMException('The operation was aborted', 'AbortError')); | | 1484 | + return; | | 1485 | + } | 1469 | 1486 | fs.open(path, flag, options.mode, (openErr, fd) => { | 1470 | 1487 | if (openErr) { | 1471 | 1488 | callback(openErr); | 1472 | 1489 | } else { | 1473 | 1490 | const isUserFd = false; | 1474 | | - writeAll(fd, isUserFd, data, 0, data.byteLength, callback); | | 1491 | + const signal = options.signal; | | 1492 | + writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback); | 1475 | 1493 | } | 1476 | 1494 | }); | 1477 | 1495 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/fs/promises.js +8-2Lines changed: 8 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -250,12 +250,15 @@ async function fsCall(fn, handle, ...args) { | 250 | 250 | } | 251 | 251 | } | 252 | 252 |
| 253 | | -async function writeFileHandle(filehandle, data) { | | 253 | +async function writeFileHandle(filehandle, data, signal) { | 254 | 254 | // `data` could be any kind of typed array. | 255 | 255 | data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); | 256 | 256 | let remaining = data.length; | 257 | 257 | if (remaining === 0) return; | 258 | 258 | do { | | 259 | + if (signal?.aborted) { | | 260 | + throw new lazyDOMException('The operation was aborted', 'AbortError'); | | 261 | + } | 259 | 262 | const { bytesWritten } = | 260 | 263 | await write(filehandle, data, 0, | 261 | 264 | MathMin(kWriteFileMaxChunkSize, data.length)); | @@ -644,9 +647,12 @@ async function writeFile(path, data, options) { | 644 | 647 | } | 645 | 648 |
| 646 | 649 | if (path instanceof FileHandle) | 647 | | - return writeFileHandle(path, data); | | 650 | + return writeFileHandle(path, data, options.signal); | 648 | 651 |
| 649 | 652 | const fd = await open(path, flag, options.mode); | | 653 | + if (options.signal?.aborted) { | | 654 | + throw new lazyDOMException('The operation was aborted', 'AbortError'); | | 655 | + } | 650 | 656 | return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close); | 651 | 657 | } | 652 | 658 |
| |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-fs-promises-writefile.js +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Flags: --experimental-abortcontroller | 1 | 2 | 'use strict'; | 2 | 3 |
| 3 | 4 | const common = require('../common'); | @@ -11,6 +12,7 @@ const tmpDir = tmpdir.path; | 11 | 12 | tmpdir.refresh(); | 12 | 13 |
| 13 | 14 | const dest = path.resolve(tmpDir, 'tmp.txt'); | | 15 | +const otherDest = path.resolve(tmpDir, 'tmp-2.txt'); | 14 | 16 | const buffer = Buffer.from('abc'.repeat(1000)); | 15 | 17 | const buffer2 = Buffer.from('xyz'.repeat(1000)); | 16 | 18 |
| @@ -20,6 +22,15 @@ async function doWrite() { | 20 | 22 | assert.deepStrictEqual(data, buffer); | 21 | 23 | } | 22 | 24 |
| | 25 | +async function doWriteWithCancel() { | | 26 | + const controller = new AbortController(); | | 27 | + const { signal } = controller; | | 28 | + process.nextTick(() => controller.abort()); | | 29 | + assert.rejects(fsPromises.writeFile(otherDest, buffer, { signal }), { | | 30 | + name: 'AbortError' | | 31 | + }); | | 32 | +} | | 33 | + | 23 | 34 | async function doAppend() { | 24 | 35 | await fsPromises.appendFile(dest, buffer2); | 25 | 36 | const data = fs.readFileSync(dest); | @@ -41,6 +52,7 @@ async function doReadWithEncoding() { | 41 | 52 | } | 42 | 53 |
| 43 | 54 | doWrite() | | 55 | + .then(doWriteWithCancel) | 44 | 56 | .then(doAppend) | 45 | 57 | .then(doRead) | 46 | 58 | .then(doReadWithEncoding) | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-fs-write-file.js +30Lines changed: 30 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 19 | 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | 20 | 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. | 21 | 21 |
| | 22 | +// Flags: --experimental-abortcontroller | 22 | 23 | 'use strict'; | 23 | 24 | const common = require('../common'); | 24 | 25 | const assert = require('assert'); | @@ -66,3 +67,32 @@ fs.open(filename4, 'w+', common.mustSucceed((fd) => { | 66 | 67 | })); | 67 | 68 | })); | 68 | 69 | })); | | 70 | + | | 71 | + | | 72 | +{ | | 73 | + // Test that writeFile is cancellable with an AbortSignal. | | 74 | + // Before the operation has started | | 75 | + const controller = new AbortController(); | | 76 | + const signal = controller.signal; | | 77 | + const filename3 = join(tmpdir.path, 'test3.txt'); | | 78 | + | | 79 | + fs.writeFile(filename3, s, { signal }, common.mustCall((err) => { | | 80 | + assert.strictEqual(err.name, 'AbortError'); | | 81 | + })); | | 82 | + | | 83 | + controller.abort(); | | 84 | +} | | 85 | + | | 86 | +{ | | 87 | + // Test that writeFile is cancellable with an AbortSignal. | | 88 | + // After the operation has started | | 89 | + const controller = new AbortController(); | | 90 | + const signal = controller.signal; | | 91 | + const filename4 = join(tmpdir.path, 'test4.txt'); | | 92 | + | | 93 | + fs.writeFile(filename4, s, { signal }, common.mustCall((err) => { | | 94 | + assert.strictEqual(err.name, 'AbortError'); | | 95 | + })); | | 96 | + | | 97 | + process.nextTick(() => controller.abort()); | | 98 | +} |
|