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