index.js 5.8 KB

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