bdba315e91dfc7e2d5f0234c30123c790f6fb6d865935957e234b691a84487f251d320e9769d3b21901805521ba5ac8cf42b81cc01cfb2f83ab3b714bdca0c 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. 'use strict'
  2. const util = require('util')
  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 Minipass = require('minipass')
  8. const Pipeline = require('minipass-pipeline')
  9. const Flush = require('minipass-flush')
  10. const path = require('path')
  11. const rimraf = util.promisify(require('rimraf'))
  12. const ssri = require('ssri')
  13. const uniqueFilename = require('unique-filename')
  14. const { disposer } = require('./../util/disposer')
  15. const fsm = require('fs-minipass')
  16. const writeFile = util.promisify(fs.writeFile)
  17. module.exports = write
  18. function write (cache, data, opts) {
  19. opts = opts || {}
  20. if (opts.algorithms && opts.algorithms.length > 1) {
  21. throw new Error('opts.algorithms only supports a single algorithm for now')
  22. }
  23. if (typeof opts.size === 'number' && data.length !== opts.size) {
  24. return Promise.reject(sizeError(opts.size, data.length))
  25. }
  26. const sri = ssri.fromData(data, {
  27. algorithms: opts.algorithms
  28. })
  29. if (opts.integrity && !ssri.checkData(data, opts.integrity, opts)) {
  30. return Promise.reject(checksumError(opts.integrity, sri))
  31. }
  32. return disposer(makeTmp(cache, opts), makeTmpDisposer,
  33. (tmp) => {
  34. return writeFile(tmp.target, data, { flag: 'wx' })
  35. .then(() => moveToDestination(tmp, cache, sri, opts))
  36. })
  37. .then(() => ({ integrity: sri, size: data.length }))
  38. }
  39. module.exports.stream = writeStream
  40. // writes proxied to the 'inputStream' that is passed to the Promise
  41. // 'end' is deferred until content is handled.
  42. class CacacheWriteStream extends Flush {
  43. constructor (cache, opts) {
  44. super()
  45. this.opts = opts
  46. this.cache = cache
  47. this.inputStream = new Minipass()
  48. this.inputStream.on('error', er => this.emit('error', er))
  49. this.inputStream.on('drain', () => this.emit('drain'))
  50. this.handleContentP = null
  51. }
  52. write (chunk, encoding, cb) {
  53. if (!this.handleContentP) {
  54. this.handleContentP = handleContent(
  55. this.inputStream,
  56. this.cache,
  57. this.opts
  58. )
  59. }
  60. return this.inputStream.write(chunk, encoding, cb)
  61. }
  62. flush (cb) {
  63. this.inputStream.end(() => {
  64. if (!this.handleContentP) {
  65. const e = new Error('Cache input stream was empty')
  66. e.code = 'ENODATA'
  67. // empty streams are probably emitting end right away.
  68. // defer this one tick by rejecting a promise on it.
  69. return Promise.reject(e).catch(cb)
  70. }
  71. this.handleContentP.then(
  72. (res) => {
  73. res.integrity && this.emit('integrity', res.integrity)
  74. res.size !== null && this.emit('size', res.size)
  75. cb()
  76. },
  77. (er) => cb(er)
  78. )
  79. })
  80. }
  81. }
  82. function writeStream (cache, opts) {
  83. opts = opts || {}
  84. return new CacacheWriteStream(cache, opts)
  85. }
  86. function handleContent (inputStream, cache, opts) {
  87. return disposer(makeTmp(cache, opts), makeTmpDisposer, (tmp) => {
  88. return pipeToTmp(inputStream, cache, tmp.target, opts)
  89. .then((res) => {
  90. return moveToDestination(
  91. tmp,
  92. cache,
  93. res.integrity,
  94. opts
  95. ).then(() => res)
  96. })
  97. })
  98. }
  99. function pipeToTmp (inputStream, cache, tmpTarget, opts) {
  100. let integrity
  101. let size
  102. const hashStream = ssri.integrityStream({
  103. integrity: opts.integrity,
  104. algorithms: opts.algorithms,
  105. size: opts.size
  106. })
  107. hashStream.on('integrity', i => { integrity = i })
  108. hashStream.on('size', s => { size = s })
  109. const outStream = new fsm.WriteStream(tmpTarget, {
  110. flags: 'wx'
  111. })
  112. // NB: this can throw if the hashStream has a problem with
  113. // it, and the data is fully written. but pipeToTmp is only
  114. // called in promisory contexts where that is handled.
  115. const pipeline = new Pipeline(
  116. inputStream,
  117. hashStream,
  118. outStream
  119. )
  120. return pipeline.promise()
  121. .then(() => ({ integrity, size }))
  122. .catch(er => rimraf(tmpTarget).then(() => { throw er }))
  123. }
  124. function makeTmp (cache, opts) {
  125. const tmpTarget = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
  126. return fixOwner.mkdirfix(cache, path.dirname(tmpTarget)).then(() => ({
  127. target: tmpTarget,
  128. moved: false
  129. }))
  130. }
  131. function makeTmpDisposer (tmp) {
  132. if (tmp.moved) {
  133. return Promise.resolve()
  134. }
  135. return rimraf(tmp.target)
  136. }
  137. function moveToDestination (tmp, cache, sri, opts) {
  138. const destination = contentPath(cache, sri)
  139. const destDir = path.dirname(destination)
  140. return fixOwner
  141. .mkdirfix(cache, destDir)
  142. .then(() => {
  143. return moveFile(tmp.target, destination)
  144. })
  145. .then(() => {
  146. tmp.moved = true
  147. return fixOwner.chownr(cache, destination)
  148. })
  149. }
  150. function sizeError (expected, found) {
  151. const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
  152. err.expected = expected
  153. err.found = found
  154. err.code = 'EBADSIZE'
  155. return err
  156. }
  157. function checksumError (expected, found) {
  158. const err = new Error(`Integrity check failed:
  159. Wanted: ${expected}
  160. Found: ${found}`)
  161. err.code = 'EINTEGRITY'
  162. err.expected = expected
  163. err.found = found
  164. return err
  165. }