You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 lines
4.4 KiB

4 years ago
  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, {
  28. algorithms: opts.algorithms
  29. })
  30. if (opts.integrity && !ssri.checkData(data, opts.integrity, opts)) {
  31. return BB.reject(checksumError(opts.integrity, sri))
  32. }
  33. return BB.using(makeTmp(cache, opts), tmp => (
  34. writeFileAsync(
  35. tmp.target, data, { flag: 'wx' }
  36. ).then(() => (
  37. moveToDestination(tmp, cache, sri, opts)
  38. ))
  39. )).then(() => ({ integrity: sri, size: data.length }))
  40. }
  41. module.exports.stream = writeStream
  42. function writeStream (cache, opts) {
  43. opts = opts || {}
  44. const inputStream = new PassThrough()
  45. let inputErr = false
  46. function errCheck () {
  47. if (inputErr) { throw inputErr }
  48. }
  49. let allDone
  50. const ret = to((c, n, cb) => {
  51. if (!allDone) {
  52. allDone = handleContent(inputStream, cache, opts, errCheck)
  53. }
  54. inputStream.write(c, n, cb)
  55. }, cb => {
  56. inputStream.end(() => {
  57. if (!allDone) {
  58. const e = new Error(Y`Cache input stream was empty`)
  59. e.code = 'ENODATA'
  60. return ret.emit('error', e)
  61. }
  62. allDone.then(res => {
  63. res.integrity && ret.emit('integrity', res.integrity)
  64. res.size !== null && ret.emit('size', res.size)
  65. cb()
  66. }, e => {
  67. ret.emit('error', e)
  68. })
  69. })
  70. })
  71. ret.once('error', e => {
  72. inputErr = e
  73. })
  74. return ret
  75. }
  76. function handleContent (inputStream, cache, opts, errCheck) {
  77. return BB.using(makeTmp(cache, opts), tmp => {
  78. errCheck()
  79. return pipeToTmp(
  80. inputStream, cache, tmp.target, opts, errCheck
  81. ).then(res => {
  82. return moveToDestination(
  83. tmp, cache, res.integrity, opts, errCheck
  84. ).then(() => res)
  85. })
  86. })
  87. }
  88. function pipeToTmp (inputStream, cache, tmpTarget, opts, errCheck) {
  89. return BB.resolve().then(() => {
  90. let integrity
  91. let size
  92. const hashStream = ssri.integrityStream({
  93. integrity: opts.integrity,
  94. algorithms: opts.algorithms,
  95. size: opts.size
  96. }).on('integrity', s => {
  97. integrity = s
  98. }).on('size', s => {
  99. size = s
  100. })
  101. const outStream = fs.createWriteStream(tmpTarget, {
  102. flags: 'wx'
  103. })
  104. errCheck()
  105. return pipe(inputStream, hashStream, outStream).then(() => {
  106. return { integrity, size }
  107. }).catch(err => {
  108. return rimraf(tmpTarget).then(() => { throw err })
  109. })
  110. })
  111. }
  112. function makeTmp (cache, opts) {
  113. const tmpTarget = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
  114. return fixOwner.mkdirfix(
  115. cache, path.dirname(tmpTarget)
  116. ).then(() => ({
  117. target: tmpTarget,
  118. moved: false
  119. })).disposer(tmp => (!tmp.moved && rimraf(tmp.target)))
  120. }
  121. function moveToDestination (tmp, cache, sri, opts, errCheck) {
  122. errCheck && errCheck()
  123. const destination = contentPath(cache, sri)
  124. const destDir = path.dirname(destination)
  125. return fixOwner.mkdirfix(
  126. cache, destDir
  127. ).then(() => {
  128. errCheck && errCheck()
  129. return moveFile(tmp.target, destination)
  130. }).then(() => {
  131. errCheck && errCheck()
  132. tmp.moved = true
  133. return fixOwner.chownr(cache, destination)
  134. })
  135. }
  136. function sizeError (expected, found) {
  137. var err = new Error(Y`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
  138. err.expected = expected
  139. err.found = found
  140. err.code = 'EBADSIZE'
  141. return err
  142. }
  143. function checksumError (expected, found) {
  144. var err = new Error(Y`Integrity check failed:
  145. Wanted: ${expected}
  146. Found: ${found}`)
  147. err.code = 'EINTEGRITY'
  148. err.expected = expected
  149. err.found = found
  150. return err
  151. }