index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. return new Promise(function (resolve, reject) {
  115. if (options.fsync !== false) {
  116. fs.fsync(fd, function (err) {
  117. if (err) fs.close(fd, () => reject(err))
  118. else fs.close(fd, resolve)
  119. })
  120. } else {
  121. fs.close(fd, resolve)
  122. }
  123. })
  124. }).then(function chown () {
  125. fd = null
  126. if (options.chown) {
  127. return new Promise(function (resolve, reject) {
  128. fs.chown(tmpfile, options.chown.uid, options.chown.gid, function (err) {
  129. if (err) reject(err)
  130. else resolve()
  131. })
  132. })
  133. }
  134. }).then(function chmod () {
  135. if (options.mode) {
  136. return new Promise(function (resolve, reject) {
  137. fs.chmod(tmpfile, options.mode, function (err) {
  138. if (err) reject(err)
  139. else resolve()
  140. })
  141. })
  142. }
  143. }).then(function rename () {
  144. return new Promise(function (resolve, reject) {
  145. fs.rename(tmpfile, truename, function (err) {
  146. if (err) reject(err)
  147. else resolve()
  148. })
  149. })
  150. }).then(function success () {
  151. removeOnExitHandler()
  152. callback()
  153. }, function fail (err) {
  154. return new Promise(resolve => {
  155. return fd ? fs.close(fd, resolve) : resolve()
  156. }).then(() => {
  157. removeOnExitHandler()
  158. fs.unlink(tmpfile, function () {
  159. callback(err)
  160. })
  161. })
  162. }).then(function checkQueue () {
  163. activeFiles[absoluteName].shift() // remove the element added by serializeSameFile
  164. if (activeFiles[absoluteName].length > 0) {
  165. activeFiles[absoluteName][0]() // start next job if one is pending
  166. } else delete activeFiles[absoluteName]
  167. })
  168. }
  169. function writeFileSync (filename, data, options) {
  170. if (typeof options === 'string') options = { encoding: options }
  171. else if (!options) options = {}
  172. try {
  173. filename = fs.realpathSync(filename)
  174. } catch (ex) {
  175. // it's ok, it'll happen on a not yet existing file
  176. }
  177. var tmpfile = getTmpname(filename)
  178. if (!options.mode || !options.chown) {
  179. // Either mode or chown is not explicitly set
  180. // Default behavior is to copy it from original file
  181. try {
  182. var stats = fs.statSync(filename)
  183. options = Object.assign({}, options)
  184. if (!options.mode) {
  185. options.mode = stats.mode
  186. }
  187. if (!options.chown && process.getuid) {
  188. options.chown = { uid: stats.uid, gid: stats.gid }
  189. }
  190. } catch (ex) {
  191. // ignore stat errors
  192. }
  193. }
  194. var fd
  195. var cleanup = cleanupOnExit(tmpfile)
  196. var removeOnExitHandler = onExit(cleanup)
  197. try {
  198. fd = fs.openSync(tmpfile, 'w', options.mode)
  199. if (Buffer.isBuffer(data)) {
  200. fs.writeSync(fd, data, 0, data.length, 0)
  201. } else if (data != null) {
  202. fs.writeSync(fd, String(data), 0, String(options.encoding || 'utf8'))
  203. }
  204. if (options.fsync !== false) {
  205. fs.fsyncSync(fd)
  206. }
  207. fs.closeSync(fd)
  208. if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid)
  209. if (options.mode) fs.chmodSync(tmpfile, options.mode)
  210. fs.renameSync(tmpfile, filename)
  211. removeOnExitHandler()
  212. } catch (err) {
  213. if (fd) {
  214. try {
  215. fs.closeSync(fd)
  216. } catch (ex) {
  217. // ignore close errors at this stage, error may have closed fd already.
  218. }
  219. }
  220. removeOnExitHandler()
  221. cleanup()
  222. throw err
  223. }
  224. }