index.js 6.5 KB

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