Skip to content

Drop Node.js 6, modernize code, return Promise from async function #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ matrix:
allow_failures:
- os: osx
node_js:
- "node"
- "12"
- "10"
- "8"
- "6"
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ write-file-atomic
This is an extension for node's `fs.writeFile` that makes its operation
atomic and allows you set ownership (uid/gid of the file).

### var writeFileAtomic = require('write-file-atomic')<br>writeFileAtomic(filename, data, [options], callback)
### var writeFileAtomic = require('write-file-atomic')<br>writeFileAtomic(filename, data, [options], [callback])

* filename **String**
* data **String** | **Buffer**
Expand All @@ -15,7 +15,6 @@ atomic and allows you set ownership (uid/gid of the file).
* encoding **String** | **Null** default = 'utf8'
* fsync **Boolean** default = true
* mode **Number** default, from existing file, if any
* Promise **Object** default = native Promise object
* callback **Function**

Atomically and asynchronously writes data to a file, replacing the file if it already
Expand All @@ -27,7 +26,7 @@ If writeFile completes successfully then, if passed the **chown** option it will
the ownership of the file. Finally it renames the file back to the filename you specified. If
it encounters errors at any of these steps it will attempt to unlink the temporary file and then
pass the error back to the caller.
If multiple writes are concurrently issued to the same file, the write operations are put into a queue and serialized in the order they were called, using Promises. Native promises are used by default, but you can inject your own promise-like object with the **Promise** option. Writes to different files are still executed in parallel.
If multiple writes are concurrently issued to the same file, the write operations are put into a queue and serialized in the order they were called, using Promises. Writes to different files are still executed in parallel.

If provided, the **chown** option requires both **uid** and **gid** properties or else
you'll get an error. If **chown** is not specified it will default to using
Expand Down Expand Up @@ -55,6 +54,20 @@ writeFileAtomic('message.txt', 'Hello Node', {chown:{uid:100,gid:50}}, function
});
```

This function also supports async/await:

```javascript
(async () => {
try {
await writeFileAtomic('message.txt', 'Hello Node', {chown:{uid:100,gid:50}});
console.log('It\'s saved!');
} catch (err) {
console.error(err);
process.exit(1);
}
})();
```

### var writeFileAtomicSync = require('write-file-atomic').sync<br>writeFileAtomicSync(filename, data, [options])

The synchronous version of **writeFileAtomic**. Returns the initial
Expand Down
241 changes: 105 additions & 136 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ module.exports.sync = writeFileSync
module.exports._getTmpname = getTmpname // for testing
module.exports._cleanupOnExit = cleanupOnExit

var fs = require('fs')
var MurmurHash3 = require('imurmurhash')
var onExit = require('signal-exit')
var path = require('path')
var isTypedArray = require('is-typedarray')
var typedArrayToBuffer = require('typedarray-to-buffer')
var activeFiles = {}
const fs = require('fs')
const MurmurHash3 = require('imurmurhash')
const onExit = require('signal-exit')
const path = require('path')
const isTypedArray = require('is-typedarray')
const typedArrayToBuffer = require('typedarray-to-buffer')
const { promisify } = require('util')
const activeFiles = {}

// if we run inside of a worker_thread, `process.pid` is not unique
/* istanbul ignore next */
var threadId = (function getId () {
const threadId = (function getId () {
try {
var workerThreads = require('worker_threads')
const workerThreads = require('worker_threads')

/// if we are in main thread, this is set to `0`
return workerThreads.threadId
Expand All @@ -26,7 +27,7 @@ var threadId = (function getId () {
}
})()

var invocations = 0
let invocations = 0
function getTmpname (filename) {
return filename + '.' +
MurmurHash3(__filename)
Expand All @@ -37,149 +38,117 @@ function getTmpname (filename) {
}

function cleanupOnExit (tmpfile) {
return function () {
return () => {
try {
fs.unlinkSync(typeof tmpfile === 'function' ? tmpfile() : tmpfile)
} catch (_) {}
}
}

function writeFile (filename, data, options, callback) {
if (options) {
if (options instanceof Function) {
callback = options
options = {}
} else if (typeof options === 'string') {
options = { encoding: options }
}
} else {
options = {}
}

var Promise = options.Promise || global.Promise
var truename
var fd
var tmpfile
/* istanbul ignore next -- The closure only gets called when onExit triggers */
var removeOnExitHandler = onExit(cleanupOnExit(() => tmpfile))
var absoluteName = path.resolve(filename)

new Promise(function serializeSameFile (resolve) {
function serializeActiveFile (absoluteName) {
return new Promise(resolve => {
// make a queue if it doesn't already exist
if (!activeFiles[absoluteName]) activeFiles[absoluteName] = []

activeFiles[absoluteName].push(resolve) // add this job to the queue
if (activeFiles[absoluteName].length === 1) resolve() // kick off the first one
}).then(function getRealPath () {
return new Promise(function (resolve) {
fs.realpath(filename, function (_, realname) {
truename = realname || filename
tmpfile = getTmpname(truename)
resolve()
})
})
}).then(function stat () {
return new Promise(function stat (resolve) {
if (options.mode && options.chown) resolve()
else {
// Either mode or chown is not explicitly set
// Default behavior is to copy it from original file
fs.stat(truename, function (err, stats) {
if (err || !stats) resolve()
else {
options = Object.assign({}, options)

if (options.mode == null) {
options.mode = stats.mode
}
if (options.chown == null && process.getuid) {
options.chown = { uid: stats.uid, gid: stats.gid }
}
resolve()
}
})
}
})
}).then(function thenWriteFile () {
return new Promise(function (resolve, reject) {
fs.open(tmpfile, 'w', options.mode, function (err, _fd) {
fd = _fd
if (err) reject(err)
else resolve()
})
})
}).then(function write () {
return new Promise(function (resolve, reject) {
if (isTypedArray(data)) {
data = typedArrayToBuffer(data)
}
if (Buffer.isBuffer(data)) {
fs.write(fd, data, 0, data.length, 0, function (err) {
if (err) reject(err)
else resolve()
})
} else if (data != null) {
fs.write(fd, String(data), 0, String(options.encoding || 'utf8'), function (err) {
if (err) reject(err)
else resolve()
})
} else resolve()
})
}).then(function syncAndClose () {
return new Promise(function (resolve, reject) {
if (options.fsync !== false) {
fs.fsync(fd, function (err) {
if (err) fs.close(fd, () => reject(err))
else fs.close(fd, resolve)
})
} else {
fs.close(fd, resolve)
})
}

async function writeFileAsync (filename, data, tmpfileStorage, options = {}) {
if (typeof options === 'string') {
options = { encoding: options }
}

let fd
let tmpfile
/* istanbul ignore next -- The closure only gets called when onExit triggers */
const removeOnExitHandler = onExit(cleanupOnExit(() => tmpfile))
const absoluteName = path.resolve(filename)

try {
await serializeActiveFile(absoluteName)
const truename = await promisify(fs.realpath)(filename).catch(() => filename)
tmpfile = getTmpname(truename)

if (!options.mode || !options.chown) {
// Either mode or chown is not explicitly set
// Default behavior is to copy it from original file
const stats = await promisify(fs.stat)(truename).catch(() => {})
if (stats) {
if (options.mode == null) {
options.mode = stats.mode
}

if (options.chown == null && process.getuid) {
options.chown = { uid: stats.uid, gid: stats.gid }
}
}
})
}).then(function chown () {
}

fd = await promisify(fs.open)(tmpfile, 'w', options.mode)
if (isTypedArray(data)) {
data = typedArrayToBuffer(data)
}
if (Buffer.isBuffer(data)) {
await promisify(fs.write)(fd, data, 0, data.length, 0)
} else if (data != null) {
await promisify(fs.write)(fd, String(data), 0, String(options.encoding || 'utf8'))
}

if (options.fsync !== false) {
await promisify(fs.fsync)(fd)
}

fd = null
if (options.chown) {
return new Promise(function (resolve, reject) {
fs.chown(tmpfile, options.chown.uid, options.chown.gid, function (err) {
if (err) reject(err)
else resolve()
})
})
await promisify(fs.chown)(tmpfile, options.chown.uid, options.chown.gid)
}
}).then(function chmod () {

if (options.mode) {
return new Promise(function (resolve, reject) {
fs.chmod(tmpfile, options.mode, function (err) {
if (err) reject(err)
else resolve()
})
})
await promisify(fs.chmod)(tmpfile, options.mode)
}

await promisify(fs.rename)(tmpfile, truename)

removeOnExitHandler()
} finally {
if (fd) {
await promisify(fs.close)(fd).catch(
/* istanbul ignore next */
() => {}
)
}
}).then(function rename () {
return new Promise(function (resolve, reject) {
fs.rename(tmpfile, truename, function (err) {
if (err) reject(err)
else resolve()
})
})
}).then(function success () {
tmpfileStorage.value = tmpfile
removeOnExitHandler()
callback(null, tmpfile)
}, function fail (err) {
return new Promise(resolve => {
return fd ? fs.close(fd, resolve) : resolve()
}).then(() => {
removeOnExitHandler()
fs.unlink(tmpfile, function () {
callback(err, tmpfile)
})
})
}).then(function checkQueue () {
await promisify(fs.unlink)(tmpfile).catch(() => {})
activeFiles[absoluteName].shift() // remove the element added by serializeSameFile
if (activeFiles[absoluteName].length > 0) {
activeFiles[absoluteName][0]() // start next job if one is pending
} else delete activeFiles[absoluteName]
})
}

return tmpfile
}

function writeFile (filename, data, options, callback) {
if (options instanceof Function) {
callback = options
options = {}
}

/* This is so we can provide tmpfile to the callback even in the failure condition. */
const tmpfileStorage = {}
const promise = writeFileAsync(filename, data, tmpfileStorage, options)
/* istanbul ignore else */
if (callback) {
promise.then(
tmpfile => callback(null, tmpfile),
err => callback(err, tmpfileStorage.value)
)
}

return promise
}

function writeFileSync (filename, data, options) {
Expand All @@ -190,13 +159,13 @@ function writeFileSync (filename, data, options) {
} catch (ex) {
// it's ok, it'll happen on a not yet existing file
}
var tmpfile = getTmpname(filename)
const tmpfile = getTmpname(filename)

if (!options.mode || !options.chown) {
// Either mode or chown is not explicitly set
// Default behavior is to copy it from original file
try {
var stats = fs.statSync(filename)
const stats = fs.statSync(filename)
options = Object.assign({}, options)
if (!options.mode) {
options.mode = stats.mode
Expand All @@ -209,9 +178,9 @@ function writeFileSync (filename, data, options) {
}
}

var fd
var cleanup = cleanupOnExit(tmpfile)
var removeOnExitHandler = onExit(cleanup)
let fd
const cleanup = cleanupOnExit(tmpfile)
const removeOnExitHandler = onExit(cleanup)

try {
fd = fs.openSync(tmpfile, 'w', options.mode)
Expand Down
Loading