// This is a fork of https://github.com/npm/write-file-atomic v2.3.0 // with graceful-fs replaced with fs to avoid memory leak during testing // See: https://github.com/stylelint/stylelint/pull/2992 "use strict"; module.exports = writeFile; 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 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; } }