'use strict' module.exports = writeFile module.exports.sync = writeFileSync module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit var fs = require('graceful-fs') var MurmurHash3 = require('imurmurhash') var onExit = require('signal-exit') var path = require('path') var activeFiles = {} var invocations = 0 function getTmpname (filename) { return filename + '.' + MurmurHash3(__filename) .hash(String(process.pid)) .hash(String(++invocations)) .result() } function cleanupOnExit (tmpfile) { return function () { try { fs.unlinkSync(typeof tmpfile === 'function' ? tmpfile() : tmpfile) } catch (_) {} } } function writeFile (filename, data, options, callback) { if (options instanceof Function) { callback = options options = null } if (!options) options = {} var Promise = options.Promise || global.Promise var truename var fd var tmpfile var removeOnExit = cleanupOnExit(() => tmpfile) var absoluteName = path.resolve(filename) 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) if (!options.mode) { options.mode = stats.mode } if (!options.chown && 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 (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 () { 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] }) } function writeFileSync (filename, data, options) { if (!options) options = {} try { filename = fs.realpathSync(filename) } catch (ex) { // it's ok, it'll happen on a not yet existing file } var tmpfile = getTmpname(filename) try { 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) options = Object.assign({}, options) if (!options.mode) { options.mode = stats.mode } if (!options.chown && process.getuid) { options.chown = { uid: stats.uid, gid: stats.gid } } } catch (ex) { // ignore stat errors } } var removeOnExit = onExit(cleanupOnExit(tmpfile)) var fd = fs.openSync(tmpfile, 'w', options.mode) if (Buffer.isBuffer(data)) { fs.writeSync(fd, data, 0, data.length, 0) } else if (data != null) { fs.writeSync(fd, String(data), 0, String(options.encoding || 'utf8')) } if (options.fsync !== false) { fs.fsyncSync(fd) } fs.closeSync(fd) if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid) if (options.mode) fs.chmodSync(tmpfile, options.mode) fs.renameSync(tmpfile, filename) removeOnExit() } catch (err) { removeOnExit() try { fs.unlinkSync(tmpfile) } catch (e) {} throw err } }