From 99cfd5d21593ca2f3fa2090cb3ea87025b551fef Mon Sep 17 00:00:00 2001 From: HiroyukiYagihashi Date: Sun, 21 Feb 2021 22:14:21 +0900 Subject: [PATCH] fs: `writeFile` support `AsyncIterable`, `Iterable` & `Stream` as `data` argument Fixes: https://github.com/nodejs/node/issues/37391 --- doc/api/fs.md | 3 +- lib/internal/fs/promises.js | 48 ++++++++++++++---- test/parallel/test-fs-promises-writefile.js | 54 +++++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index 2d179c89396cc9..4799a67388f043 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -3866,7 +3866,8 @@ changes: --> * `file` {string|Buffer|URL|integer} filename or file descriptor -* `data` {string|Buffer|TypedArray|DataView|Object} +* `data` {string|Buffer|TypedArray|DataView|Object|AsyncIterable|Iterable + |Stream} * `options` {Object|string} * `encoding` {string|null} **Default:** `'utf8'` * `mode` {integer} **Default:** `0o666` diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 6c39a13349d27b..9a2a29cf7cafc3 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -22,6 +22,9 @@ const { SafeArrayIterator, Symbol, Uint8Array, + SymbolIterator, + SymbolAsyncIterator, + ArrayIsArray } = primordials; const { @@ -663,19 +666,46 @@ async function writeFile(path, data, options) { options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); const flag = options.flag || 'w'; - if (!isArrayBufferView(data)) { - validateStringAfterArrayBufferView(data, 'data'); - data = Buffer.from(data, options.encoding || 'utf8'); + if (isStream(data) || isIterable(data)) { + const fd = await open(path, flag, options.mode); + if (options.signal?.aborted) { + throw lazyDOMException('The operation was aborted', 'AbortError'); + } + try { + for await (const buf of data) { + await fd.write(buf); + } + } finally { + await fd.close(); + } + } else { + if (!isArrayBufferView(data)) { + validateStringAfterArrayBufferView(data, 'data'); + data = Buffer.from(data, options.encoding || 'utf8'); + } + + if (path instanceof FileHandle) { + return writeFileHandle(path, data, options.signal); + } + + const fd = await open(path, flag, options.mode); + if (options.signal?.aborted) { + throw lazyDOMException('The operation was aborted', 'AbortError'); + } + return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close); } +} - if (path instanceof FileHandle) - return writeFileHandle(path, data, options.signal); +function isStream(obj) { + return !!(obj && typeof obj.pipe === 'function'); +} - const fd = await open(path, flag, options.mode); - if (options.signal?.aborted) { - throw lazyDOMException('The operation was aborted', 'AbortError'); +function isIterable(obj) { + if (!obj || typeof obj === 'string' || obj.buffer || ArrayIsArray(obj)) { + return false; } - return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close); + return typeof obj[SymbolIterator] === 'function' || + typeof obj[SymbolAsyncIterator] === 'function'; } async function appendFile(path, data, options) { diff --git a/test/parallel/test-fs-promises-writefile.js b/test/parallel/test-fs-promises-writefile.js index 7fbe12dda4dc2d..79eb20f84bc9d5 100644 --- a/test/parallel/test-fs-promises-writefile.js +++ b/test/parallel/test-fs-promises-writefile.js @@ -7,6 +7,7 @@ const path = require('path'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const tmpDir = tmpdir.path; +const { Readable } = require('stream'); tmpdir.refresh(); @@ -14,6 +15,22 @@ const dest = path.resolve(tmpDir, 'tmp.txt'); const otherDest = path.resolve(tmpDir, 'tmp-2.txt'); const buffer = Buffer.from('abc'.repeat(1000)); const buffer2 = Buffer.from('xyz'.repeat(1000)); +const stream = Readable.from(['a', 'b', 'c']); +const stream2 = Readable.from(['a', 'b', 'c']); +const iterable = { + [Symbol.iterator]: function*() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; +const asyncIterable = { + async* [Symbol.asyncIterator]() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; async function doWrite() { await fsPromises.writeFile(dest, buffer); @@ -21,6 +38,39 @@ async function doWrite() { assert.deepStrictEqual(data, buffer); } +async function doWriteStream() { + await fsPromises.writeFile(dest, stream); + let expected = ''; + for await (const v of stream2) expected += v; + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, expected); +} + +async function doWriteStreamWithCancel() { + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + assert.rejects(fsPromises.writeFile(otherDest, stream, { signal }), { + name: 'AbortError' + }); +} + +async function doWriteIterable() { + await fsPromises.writeFile(dest, iterable); + let expected = ''; + for await (const v of iterable) expected += v; + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, expected); +} + +async function doWriteAsyncIterable() { + await fsPromises.writeFile(dest, asyncIterable); + let expected = ''; + for await (const v of asyncIterable) expected += v; + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, expected); +} + async function doWriteWithCancel() { const controller = new AbortController(); const { signal } = controller; @@ -55,4 +105,8 @@ doWrite() .then(doAppend) .then(doRead) .then(doReadWithEncoding) + .then(doWriteStream) + .then(doWriteStreamWithCancel) + .then(doWriteIterable) + .then(doWriteAsyncIterable) .then(common.mustCall());