write.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. 'use strict'
  2. const BB = require('bluebird')
  3. const contentPath = require('./path')
  4. const fixOwner = require('../util/fix-owner')
  5. const fs = require('graceful-fs')
  6. const moveFile = require('../util/move-file')
  7. const PassThrough = require('stream').PassThrough
  8. const path = require('path')
  9. const pipe = BB.promisify(require('mississippi').pipe)
  10. const rimraf = BB.promisify(require('rimraf'))
  11. const ssri = require('ssri')
  12. const to = require('mississippi').to
  13. const uniqueFilename = require('unique-filename')
  14. const Y = require('../util/y.js')
  15. const writeFileAsync = BB.promisify(fs.writeFile)
  16. module.exports = write
  17. function write (cache, data, opts) {
  18. opts = opts || {}
  19. if (opts.algorithms && opts.algorithms.length > 1) {
  20. throw new Error(
  21. Y`opts.algorithms only supports a single algorithm for now`
  22. )
  23. }
  24. if (typeof opts.size === 'number' && data.length !== opts.size) {
  25. return BB.reject(sizeError(opts.size, data.length))
  26. }
  27. const sri = ssri.fromData(data, opts)
  28. if (opts.integrity && !ssri.checkData(data, opts.integrity, opts)) {
  29. return BB.reject(checksumError(opts.integrity, sri))
  30. }
  31. return BB.using(makeTmp(cache, opts), tmp => (
  32. writeFileAsync(
  33. tmp.target, data, {flag: 'wx'}
  34. ).then(() => (
  35. moveToDestination(tmp, cache, sri, opts)
  36. ))
  37. )).then(() => ({integrity: sri, size: data.length}))
  38. }
  39. module.exports.stream = writeStream
  40. function writeStream (cache, opts) {
  41. opts = opts || {}
  42. const inputStream = new PassThrough()
  43. let inputErr = false
  44. function errCheck () {
  45. if (inputErr) { throw inputErr }
  46. }
  47. let allDone
  48. const ret = to((c, n, cb) => {
  49. if (!allDone) {
  50. allDone = handleContent(inputStream, cache, opts, errCheck)
  51. }
  52. inputStream.write(c, n, cb)
  53. }, cb => {
  54. inputStream.end(() => {
  55. if (!allDone) {
  56. const e = new Error(Y`Cache input stream was empty`)
  57. e.code = 'ENODATA'
  58. return ret.emit('error', e)
  59. }
  60. allDone.then(res => {
  61. res.integrity && ret.emit('integrity', res.integrity)
  62. res.size !== null && ret.emit('size', res.size)
  63. cb()
  64. }, e => {
  65. ret.emit('error', e)
  66. })
  67. })
  68. })
  69. ret.once('error', e => {
  70. inputErr = e
  71. })
  72. return ret
  73. }
  74. function handleContent (inputStream, cache, opts, errCheck) {
  75. return BB.using(makeTmp(cache, opts), tmp => {
  76. errCheck()
  77. return pipeToTmp(
  78. inputStream, cache, tmp.target, opts, errCheck
  79. ).then(res => {
  80. return moveToDestination(
  81. tmp, cache, res.integrity, opts, errCheck
  82. ).then(() => res)
  83. })
  84. })
  85. }
  86. function pipeToTmp (inputStream, cache, tmpTarget, opts, errCheck) {
  87. return BB.resolve().then(() => {
  88. let integrity
  89. let size
  90. const hashStream = ssri.integrityStream({
  91. integrity: opts.integrity,
  92. algorithms: opts.algorithms,
  93. size: opts.size
  94. }).on('integrity', s => {
  95. integrity = s
  96. }).on('size', s => {
  97. size = s
  98. })
  99. const outStream = fs.createWriteStream(tmpTarget, {
  100. flags: 'wx'
  101. })
  102. errCheck()
  103. return pipe(inputStream, hashStream, outStream).then(() => {
  104. return {integrity, size}
  105. }, err => {
  106. return rimraf(tmpTarget).then(() => { throw err })
  107. })
  108. })
  109. }
  110. function makeTmp (cache, opts) {
  111. const tmpTarget = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
  112. return fixOwner.mkdirfix(
  113. path.dirname(tmpTarget), opts.uid, opts.gid
  114. ).then(() => ({
  115. target: tmpTarget,
  116. moved: false
  117. })).disposer(tmp => (!tmp.moved && rimraf(tmp.target)))
  118. }
  119. function moveToDestination (tmp, cache, sri, opts, errCheck) {
  120. errCheck && errCheck()
  121. const destination = contentPath(cache, sri)
  122. const destDir = path.dirname(destination)
  123. return fixOwner.mkdirfix(
  124. destDir, opts.uid, opts.gid
  125. ).then(() => {
  126. errCheck && errCheck()
  127. return moveFile(tmp.target, destination)
  128. }).then(() => {
  129. errCheck && errCheck()
  130. tmp.moved = true
  131. return fixOwner.chownr(destination, opts.uid, opts.gid)
  132. })
  133. }
  134. function sizeError (expected, found) {
  135. var err = new Error(Y`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
  136. err.expected = expected
  137. err.found = found
  138. err.code = 'EBADSIZE'
  139. return err
  140. }
  141. function checksumError (expected, found) {
  142. var err = new Error(Y`Integrity check failed:
  143. Wanted: ${expected}
  144. Found: ${found}`)
  145. err.code = 'EINTEGRITY'
  146. err.expected = expected
  147. err.found = found
  148. return err
  149. }