write-file-atomic
Advanced tools
Comparing version 2.1.0 to 2.3.0
177
index.js
@@ -5,6 +5,9 @@ 'use strict' | ||
module.exports._getTmpname = getTmpname // for testing | ||
module.exports._cleanupOnExit = cleanupOnExit | ||
var fs = require('graceful-fs') | ||
var chain = require('slide').chain | ||
var MurmurHash3 = require('imurmurhash') | ||
var onExit = require('signal-exit') | ||
var path = require('path') | ||
var activeFiles = {} | ||
@@ -20,2 +23,10 @@ var invocations = 0 | ||
function cleanupOnExit (tmpfile) { | ||
return function () { | ||
try { | ||
fs.unlinkSync(typeof tmpfile === 'function' ? tmpfile() : tmpfile) | ||
} catch (_) {} | ||
} | ||
} | ||
function writeFile (filename, data, options, callback) { | ||
@@ -27,65 +38,116 @@ if (options instanceof Function) { | ||
if (!options) options = {} | ||
fs.realpath(filename, function (_, realname) { | ||
_writeFile(realname || filename, data, options, callback) | ||
}) | ||
} | ||
function _writeFile (filename, data, options, callback) { | ||
var tmpfile = getTmpname(filename) | ||
if (options.mode && options.chown) { | ||
return thenWriteFile() | ||
} else { | ||
// Either mode or chown is not explicitly set | ||
// Default behavior is to copy it from original file | ||
return fs.stat(filename, function (err, stats) { | ||
if (err || !stats) return thenWriteFile() | ||
var Promise = options.Promise || global.Promise | ||
var truename | ||
var fd | ||
var tmpfile | ||
var removeOnExit = cleanupOnExit(() => tmpfile) | ||
var absoluteName = path.resolve(filename) | ||
options = Object.assign({}, options) | ||
if (!options.mode) { | ||
options.mode = stats.mode | ||
} | ||
if (!options.chown && process.getuid) { | ||
options.chown = { uid: stats.uid, gid: stats.gid } | ||
} | ||
return thenWriteFile() | ||
new Promise(function serializeSameFile (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) | ||
function thenWriteFile () { | ||
chain([ | ||
[writeFileAsync, tmpfile, data, options.mode, options.encoding || 'utf8'], | ||
options.chown && [fs, fs.chown, tmpfile, options.chown.uid, options.chown.gid], | ||
options.mode && [fs, fs.chmod, tmpfile, options.mode], | ||
[fs, fs.rename, tmpfile, filename] | ||
], function (err) { | ||
err ? fs.unlink(tmpfile, function () { callback(err) }) | ||
: callback() | ||
if (!options.mode) { | ||
options.mode = stats.mode | ||
} | ||
if (!options.chown && process.getuid) { | ||
options.chown = { uid: stats.uid, gid: stats.gid } | ||
} | ||
resolve() | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
// doing this instead of `fs.writeFile` in order to get the ability to | ||
// call `fsync`. | ||
function writeFileAsync (file, data, mode, encoding, cb) { | ||
fs.open(file, 'w', options.mode, function (err, fd) { | ||
if (err) return cb(err) | ||
}).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 (Buffer.isBuffer(data)) { | ||
return fs.write(fd, data, 0, data.length, 0, syncAndClose) | ||
fs.write(fd, data, 0, data.length, 0, function (err) { | ||
if (err) reject(err) | ||
else resolve() | ||
}) | ||
} else if (data != null) { | ||
return fs.write(fd, String(data), 0, String(encoding), syncAndClose) | ||
} else { | ||
return syncAndClose() | ||
} | ||
function syncAndClose (err) { | ||
if (err) return cb(err) | ||
if (options.fsync !== false) { | ||
fs.fsync(fd, function (err) { | ||
if (err) return cb(err) | ||
fs.close(fd, cb) | ||
}) | ||
} else { | ||
fs.close(fd, cb) | ||
} | ||
} | ||
fs.write(fd, String(data), 0, String(options.encoding || 'utf8'), function (err) { | ||
if (err) reject(err) | ||
else resolve() | ||
}) | ||
} else resolve() | ||
}) | ||
} | ||
}).then(function syncAndClose () { | ||
if (options.fsync !== false) { | ||
return new Promise(function (resolve, reject) { | ||
fs.fsync(fd, function (err) { | ||
if (err) reject(err) | ||
else fs.close(fd, resolve) | ||
}) | ||
}) | ||
} | ||
}).then(function chown () { | ||
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() | ||
}) | ||
}) | ||
} | ||
}).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() | ||
}) | ||
}) | ||
} | ||
}).then(function rename () { | ||
return new Promise(function (resolve, reject) { | ||
fs.rename(tmpfile, truename, function (err) { | ||
if (err) reject(err) | ||
else resolve() | ||
}) | ||
}) | ||
}).then(function success () { | ||
removeOnExit() | ||
callback() | ||
}).catch(function fail (err) { | ||
removeOnExit() | ||
fs.unlink(tmpfile, function () { | ||
callback(err) | ||
}) | ||
}).then(function checkQueue () { | ||
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] | ||
}) | ||
} | ||
@@ -120,2 +182,3 @@ | ||
var removeOnExit = onExit(cleanupOnExit(tmpfile)) | ||
var fd = fs.openSync(tmpfile, 'w', options.mode) | ||
@@ -134,3 +197,5 @@ if (Buffer.isBuffer(data)) { | ||
fs.renameSync(tmpfile, filename) | ||
removeOnExit() | ||
} catch (err) { | ||
removeOnExit() | ||
try { fs.unlinkSync(tmpfile) } catch (e) {} | ||
@@ -137,0 +202,0 @@ throw err |
{ | ||
"name": "write-file-atomic", | ||
"version": "2.1.0", | ||
"version": "2.3.0", | ||
"description": "Write files in an atomic fashion w/configurable ownership", | ||
@@ -26,3 +26,3 @@ "main": "index.js", | ||
"imurmurhash": "^0.1.4", | ||
"slide": "^1.1.5" | ||
"signal-exit": "^3.0.2" | ||
}, | ||
@@ -29,0 +29,0 @@ "devDependencies": { |
@@ -18,2 +18,3 @@ write-file-atomic | ||
* mode **Number** default = 438 (aka 0666 in Octal) | ||
* Promise **Object** default = native Promise object | ||
callback **Function** | ||
@@ -29,2 +30,3 @@ | ||
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. | ||
@@ -31,0 +33,0 @@ If provided, the **chown** option requires both **uid** and **gid** properties or else |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
9696
188
50
0
+ Addedsignal-exit@^3.0.2
+ Addedsignal-exit@3.0.7(transitive)
- Removedslide@^1.1.5
- Removedslide@1.1.6(transitive)