1'use strict' 2module.exports = writeFile 3module.exports.sync = writeFileSync 4module.exports._getTmpname = getTmpname // for testing 5module.exports._cleanupOnExit = cleanupOnExit 6 7var fs = require('graceful-fs') 8var MurmurHash3 = require('imurmurhash') 9var onExit = require('signal-exit') 10var path = require('path') 11var activeFiles = {} 12 13// if we run inside of a worker_thread, `process.pid` is not unique 14/* istanbul ignore next */ 15var threadId = (function getId () { 16 try { 17 var workerThreads = require('worker_threads') 18 19 /// if we are in main thread, this is set to `0` 20 return workerThreads.threadId 21 } catch (e) { 22 // worker_threads are not available, fallback to 0 23 return 0 24 } 25})() 26 27var invocations = 0 28function getTmpname (filename) { 29 return filename + '.' + 30 MurmurHash3(__filename) 31 .hash(String(process.pid)) 32 .hash(String(threadId)) 33 .hash(String(++invocations)) 34 .result() 35} 36 37function cleanupOnExit (tmpfile) { 38 return function () { 39 try { 40 fs.unlinkSync(typeof tmpfile === 'function' ? tmpfile() : tmpfile) 41 } catch (_) {} 42 } 43} 44 45function writeFile (filename, data, options, callback) { 46 if (options) { 47 if (options instanceof Function) { 48 callback = options 49 options = {} 50 } else if (typeof options === 'string') { 51 options = { encoding: options } 52 } 53 } else { 54 options = {} 55 } 56 57 var Promise = options.Promise || global.Promise 58 var truename 59 var fd 60 var tmpfile 61 /* istanbul ignore next -- The closure only gets called when onExit triggers */ 62 var removeOnExitHandler = onExit(cleanupOnExit(() => tmpfile)) 63 var absoluteName = path.resolve(filename) 64 65 new Promise(function serializeSameFile (resolve) { 66 // make a queue if it doesn't already exist 67 if (!activeFiles[absoluteName]) activeFiles[absoluteName] = [] 68 69 activeFiles[absoluteName].push(resolve) // add this job to the queue 70 if (activeFiles[absoluteName].length === 1) resolve() // kick off the first one 71 }).then(function getRealPath () { 72 return new Promise(function (resolve) { 73 fs.realpath(filename, function (_, realname) { 74 truename = realname || filename 75 tmpfile = getTmpname(truename) 76 resolve() 77 }) 78 }) 79 }).then(function stat () { 80 return new Promise(function stat (resolve) { 81 if (options.mode && options.chown) resolve() 82 else { 83 // Either mode or chown is not explicitly set 84 // Default behavior is to copy it from original file 85 fs.stat(truename, function (err, stats) { 86 if (err || !stats) resolve() 87 else { 88 options = Object.assign({}, options) 89 90 if (options.mode == null) { 91 options.mode = stats.mode 92 } 93 if (options.chown == null && process.getuid) { 94 options.chown = { uid: stats.uid, gid: stats.gid } 95 } 96 resolve() 97 } 98 }) 99 } 100 }) 101 }).then(function thenWriteFile () { 102 return new Promise(function (resolve, reject) { 103 fs.open(tmpfile, 'w', options.mode, function (err, _fd) { 104 fd = _fd 105 if (err) reject(err) 106 else resolve() 107 }) 108 }) 109 }).then(function write () { 110 return new Promise(function (resolve, reject) { 111 if (Buffer.isBuffer(data)) { 112 fs.write(fd, data, 0, data.length, 0, function (err) { 113 if (err) reject(err) 114 else resolve() 115 }) 116 } else if (data != null) { 117 fs.write(fd, String(data), 0, String(options.encoding || 'utf8'), function (err) { 118 if (err) reject(err) 119 else resolve() 120 }) 121 } else resolve() 122 }) 123 }).then(function syncAndClose () { 124 return new Promise(function (resolve, reject) { 125 if (options.fsync !== false) { 126 fs.fsync(fd, function (err) { 127 if (err) fs.close(fd, () => reject(err)) 128 else fs.close(fd, resolve) 129 }) 130 } else { 131 fs.close(fd, resolve) 132 } 133 }) 134 }).then(function chown () { 135 fd = null 136 if (options.chown) { 137 return new Promise(function (resolve, reject) { 138 fs.chown(tmpfile, options.chown.uid, options.chown.gid, function (err) { 139 if (err) reject(err) 140 else resolve() 141 }) 142 }) 143 } 144 }).then(function chmod () { 145 if (options.mode) { 146 return new Promise(function (resolve, reject) { 147 fs.chmod(tmpfile, options.mode, function (err) { 148 if (err) reject(err) 149 else resolve() 150 }) 151 }) 152 } 153 }).then(function rename () { 154 return new Promise(function (resolve, reject) { 155 fs.rename(tmpfile, truename, function (err) { 156 if (err) reject(err) 157 else resolve() 158 }) 159 }) 160 }).then(function success () { 161 removeOnExitHandler() 162 callback() 163 }, function fail (err) { 164 return new Promise(resolve => { 165 return fd ? fs.close(fd, resolve) : resolve() 166 }).then(() => { 167 removeOnExitHandler() 168 fs.unlink(tmpfile, function () { 169 callback(err) 170 }) 171 }) 172 }).then(function checkQueue () { 173 activeFiles[absoluteName].shift() // remove the element added by serializeSameFile 174 if (activeFiles[absoluteName].length > 0) { 175 activeFiles[absoluteName][0]() // start next job if one is pending 176 } else delete activeFiles[absoluteName] 177 }) 178} 179 180function writeFileSync (filename, data, options) { 181 if (typeof options === 'string') options = { encoding: options } 182 else if (!options) options = {} 183 try { 184 filename = fs.realpathSync(filename) 185 } catch (ex) { 186 // it's ok, it'll happen on a not yet existing file 187 } 188 var tmpfile = getTmpname(filename) 189 190 if (!options.mode || !options.chown) { 191 // Either mode or chown is not explicitly set 192 // Default behavior is to copy it from original file 193 try { 194 var stats = fs.statSync(filename) 195 options = Object.assign({}, options) 196 if (!options.mode) { 197 options.mode = stats.mode 198 } 199 if (!options.chown && process.getuid) { 200 options.chown = { uid: stats.uid, gid: stats.gid } 201 } 202 } catch (ex) { 203 // ignore stat errors 204 } 205 } 206 207 var fd 208 var cleanup = cleanupOnExit(tmpfile) 209 var removeOnExitHandler = onExit(cleanup) 210 211 try { 212 fd = fs.openSync(tmpfile, 'w', options.mode) 213 if (Buffer.isBuffer(data)) { 214 fs.writeSync(fd, data, 0, data.length, 0) 215 } else if (data != null) { 216 fs.writeSync(fd, String(data), 0, String(options.encoding || 'utf8')) 217 } 218 if (options.fsync !== false) { 219 fs.fsyncSync(fd) 220 } 221 fs.closeSync(fd) 222 if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid) 223 if (options.mode) fs.chmodSync(tmpfile, options.mode) 224 fs.renameSync(tmpfile, filename) 225 removeOnExitHandler() 226 } catch (err) { 227 if (fd) { 228 try { 229 fs.closeSync(fd) 230 } catch (ex) { 231 // ignore close errors at this stage, error may have closed fd already. 232 } 233 } 234 removeOnExitHandler() 235 cleanup() 236 throw err 237 } 238} 239