Repository: jprichardson/node-fs-extra
Pull request #449 (2017-06-26 18:36:56)

BREAKING: fs.read() & fs.write() should return objects
For forward-compat with `util.promisify()`

Resolves https://github.com/jprichardson/node-fs-extra/issues/436

Needs docs yet

coveralls: 
[![Coverage Status](https://:/builds/12138042/badge)](https://:/builds/12138042)

Coverage increased (+0.2%) to 87.174% when pulling **e76eaec8a754533553c94c9cd0e7ead068375568 on read-write-obj** into **15db64fbba505ee9395a4be9c8147d956f950df2 on master**.

coveralls: 
[![Coverage Status](https://:/builds/12138489/badge)](https://:/builds/12138489)

Coverage increased (+0.2%) to 87.174% when pulling **28ac447ec37191368a3bae3d52c8739e6b307a9c on read-write-obj** into **15db64fbba505ee9395a4be9c8147d956f950df2 on master**.

coveralls: 
[![Coverage Status](https://:/builds/12138592/badge)](https://:/builds/12138592)

Coverage increased (+0.2%) to 87.174% when pulling **d3545d42941f51b00cd9dcd525f8c78b36db8b51 on read-write-obj** into **15db64fbba505ee9395a4be9c8147d956f950df2 on master**.

RyanZim: Windows CI fails due to funky windows timestamps; unrelated to this change.

Patch:
diff --git a/README.md b/README.md
index 61c79c8..7dbb9f7 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ Methods
 - [writeJsonSync](docs/writeJson-sync.md)
 
 
-**NOTE:** You can still use the native Node.js methods. They are promisified and copied over to `fs-extra`.
+**NOTE:** You can still use the native Node.js methods. They are promisified and copied over to `fs-extra`. See [notes on `fs.read()` & `fs.write()`](docs/fs-read-write.md)
 
 ### What happened to `walk()` and `walkSync()`?
 
diff --git a/docs/fs-read-write.md b/docs/fs-read-write.md
new file mode 100644
index 0000000..ea9cf17
--- /dev/null
+++ b/docs/fs-read-write.md
@@ -0,0 +1,39 @@
+# About `fs.read()` & `fs.write()`
+
+[`fs.read()`](https://nodejs.org/api/fs.html#fs_fs_read_fd_buffer_offset_length_position_callback) & [`fs.write()`](https://nodejs.org/api/fs.html#fs_fs_write_fd_buffer_offset_length_position_callback) are different from other `fs` methods in that their callbacks are called with 3 arguments instead of the usual 2 arguments.
+
+If you're using them with callbacks, they will behave as usual. However, their promise usage is a little different. `fs-extra` promisifies these methods like [`util.promisify()`](https://nodejs.org/api/util.html#util_util_promisify_original) (only availible in Node 8+) does.
+
+Here's the example promise usage:
+
+## `fs.read()`
+
+```js
+// Basic promises
+fs.read(fd, buffer, offset, length, position)
+  .then(results => {
+    console.log(results)
+    // { bytesRead: 20, buffer: <Buffer 0f 34 5d ...> }
+  })
+
+// Async/await usage:
+async function example () {
+  const { bytesRead, buffer } = await fs.read(fd, buffer, offset, length, position)
+}
+```
+
+## `fs.write()`
+
+```js
+// Basic promises
+fs.write(fd, buffer, offset, length, position)
+  .then(results => {
+    console.log(results)
+    // { bytesWritten: 20, buffer: <Buffer 0f 34 5d ...> }
+  })
+
+// Async/await usage:
+async function example () {
+  const { bytesWritten, buffer } = await fs.write(fd, buffer, offset, length, position)
+}
+```
diff --git a/lib/fs/index.js b/lib/fs/index.js
index e20ae76..551ca6a 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -21,7 +21,6 @@ const api = [
   'lstat',
   'mkdir',
   'open',
-  'read',
   'readFile',
   'readdir',
   'readlink',
@@ -33,7 +32,6 @@ const api = [
   'truncate',
   'unlink',
   'utimes',
-  'write',
   'writeFile'
 ]
 // fs.mkdtemp() was added in Node.js v5.10.0, so check if it exists
@@ -59,3 +57,45 @@ exports.exists = function (filename, callback) {
     return fs.exists(filename, resolve)
   })
 }
+
+// fs.read() & fs.write need special treatment due to multiple callback args
+
+exports.read = function (fd, buffer, offset, length, position, callback) {
+  if (typeof callback === 'function') {
+    return fs.read(fd, buffer, offset, length, position, callback)
+  }
+  return new Promise((resolve, reject) => {
+    fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
+      if (err) return reject(err)
+      resolve({ bytesRead, buffer })
+    })
+  })
+}
+
+// Function signature can be
+// fs.write(fd, buffer[, offset[, length[, position]]], callback)
+// OR
+// fs.write(fd, string[, position[, encoding]], callback)
+// so we need to handle both cases
+exports.write = function (fd, buffer, a, b, c, callback) {
+  if (typeof arguments[arguments.length - 1] === 'function') {
+    return fs.write(fd, buffer, a, b, c, callback)
+  }
+
+  // Check for old, depricated fs.write(fd, string[, position[, encoding]], callback)
+  if (typeof buffer === 'string') {
+    return new Promise((resolve, reject) => {
+      fs.write(fd, buffer, a, b, (err, bytesWritten, buffer) => {
+        if (err) return reject(err)
+        resolve({ bytesWritten, buffer })
+      })
+    })
+  }
+
+  return new Promise((resolve, reject) => {
+    fs.write(fd, buffer, a, b, c, (err, bytesWritten, buffer) => {
+      if (err) return reject(err)
+      resolve({ bytesWritten, buffer })
+    })
+  })
+}
diff --git a/package.json b/package.json
index e539c95..6ba12c8 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
     "read-dir-files": "^0.1.1",
     "rimraf": "^2.2.8",
     "secure-random": "^1.1.1",
+    "semver": "^5.3.0",
     "standard": "^10.0.2",
     "standard-markdown": "^2.3.0"
   },


Test patch:
diff --git a/lib/fs/__tests__/multi-param.test.js b/lib/fs/__tests__/multi-param.test.js
new file mode 100644
index 0000000..92c619b
--- /dev/null
+++ b/lib/fs/__tests__/multi-param.test.js
@@ -0,0 +1,154 @@
+'use strict'
+/* eslint-env mocha */
+const assert = require('assert')
+const path = require('path')
+const crypto = require('crypto')
+const os = require('os')
+const semver = require('semver')
+const fs = require('../..')
+
+const SIZE = 1000
+
+// Used for tests on Node 7.2.0+ only
+const onNode7it = semver.gte(process.version, '7.2.0') ? it : it.skip
+
+describe('fs.read()', () => {
+  let TEST_FILE
+  let TEST_DATA
+  let TEST_FD
+
+  beforeEach(() => {
+    TEST_FILE = path.join(os.tmpdir(), 'fs-extra', 'read-test-file')
+    TEST_DATA = crypto.randomBytes(SIZE)
+    fs.writeFileSync(TEST_FILE, TEST_DATA)
+    TEST_FD = fs.openSync(TEST_FILE, 'r')
+  })
+
+  afterEach(() => {
+    return fs.close(TEST_FD)
+      .then(() => fs.remove(TEST_FILE))
+  })
+
+  describe('with promises', () => {
+    it('returns an object', () => {
+      return fs.read(TEST_FD, Buffer.alloc(SIZE), 0, SIZE, 0)
+        .then(results => {
+          const bytesRead = results.bytesRead
+          const buffer = results.buffer
+          assert.equal(bytesRead, SIZE, 'bytesRead is correct')
+          assert(buffer.equals(TEST_DATA), 'data is correct')
+        })
+    })
+
+    it('returns an object when position is not set', () => {
+      return fs.read(TEST_FD, Buffer.alloc(SIZE), 0, SIZE)
+        .then(results => {
+          const bytesRead = results.bytesRead
+          const buffer = results.buffer
+          assert.equal(bytesRead, SIZE, 'bytesRead is correct')
+          assert(buffer.equals(TEST_DATA), 'data is correct')
+        })
+    })
+  })
+
+  describe('with callbacks', () => {
+    it('works', done => {
+      fs.read(TEST_FD, Buffer.alloc(SIZE), 0, SIZE, 0, (err, bytesRead, buffer) => {
+        assert.ifError(err)
+        assert.equal(bytesRead, SIZE, 'bytesRead is correct')
+        assert(buffer.equals(TEST_DATA), 'data is correct')
+        done()
+      })
+    })
+
+    it('works when position is null', done => {
+      fs.read(TEST_FD, Buffer.alloc(SIZE), 0, SIZE, null, (err, bytesRead, buffer) => {
+        assert.ifError(err)
+        assert.equal(bytesRead, SIZE, 'bytesRead is correct')
+        assert(buffer.equals(TEST_DATA), 'data is correct')
+        done()
+      })
+    })
+  })
+})
+
+describe('fs.write()', () => {
+  let TEST_FILE
+  let TEST_DATA
+  let TEST_FD
+
+  beforeEach(() => {
+    TEST_FILE = path.join(os.tmpdir(), 'fs-extra', 'write-test-file')
+    TEST_DATA = crypto.randomBytes(SIZE)
+    fs.ensureDirSync(path.dirname(TEST_FILE))
+    TEST_FD = fs.openSync(TEST_FILE, 'w')
+  })
+
+  afterEach(() => {
+    return fs.close(TEST_FD)
+      .then(() => fs.remove(TEST_FILE))
+  })
+
+  describe('with promises', () => {
+    it('returns an object', () => {
+      return fs.write(TEST_FD, TEST_DATA, 0, SIZE, 0)
+        .then(results => {
+          const bytesWritten = results.bytesWritten
+          const buffer = results.buffer
+          assert.equal(bytesWritten, SIZE, 'bytesWritten is correct')
+          assert(buffer.equals(TEST_DATA), 'data is correct')
+        })
+    })
+
+    onNode7it('returns an object when minimal arguments are passed', () => {
+      return fs.write(TEST_FD, TEST_DATA)
+        .then(results => {
+          const bytesWritten = results.bytesWritten
+          const buffer = results.buffer
+          assert.equal(bytesWritten, SIZE, 'bytesWritten is correct')
+          assert(buffer.equals(TEST_DATA), 'data is correct')
+        })
+    })
+
+    it('returns an object when writing a string', () => {
+      const message = 'Hello World!'
+      return fs.write(TEST_FD, message)
+        .then(results => {
+          const bytesWritten = results.bytesWritten
+          const buffer = results.buffer
+          assert.equal(bytesWritten, message.length, 'bytesWritten is correct')
+          assert.equal(buffer, message, 'data is correct')
+        })
+    })
+  })
+
+  describe('with callbacks', () => {
+    it('works', done => {
+      fs.write(TEST_FD, TEST_DATA, 0, SIZE, 0, (err, bytesWritten, buffer) => {
+        assert.ifError(err)
+        assert.equal(bytesWritten, SIZE, 'bytesWritten is correct')
+        assert(buffer.equals(TEST_DATA), 'data is correct')
+        done()
+      })
+    })
+
+    onNode7it('works when minimal arguments are passed', done => {
+      fs.write(TEST_FD, TEST_DATA, (err, bytesWritten, buffer) => {
+        assert.ifError(err)
+        assert.equal(bytesWritten, SIZE, 'bytesWritten is correct')
+        assert(buffer.equals(TEST_DATA), 'data is correct')
+        done()
+      })
+    })
+
+    it('works when writing a string', done => {
+      const message = 'Hello World!'
+      return fs.write(TEST_FD, message, (err, bytesWritten, buffer) => {
+        assert.ifError(err)
+        assert.equal(bytesWritten, message.length, 'bytesWritten is correct')
+        assert.equal(buffer, message, 'data is correct')
+        done()
+      })
+    })
+  })
+})