163 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			163 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict'
 | |
| 
 | |
| const BB = require('bluebird')
 | |
| 
 | |
| const contentPath = require('./path')
 | |
| const fixOwner = require('../util/fix-owner')
 | |
| const fs = require('graceful-fs')
 | |
| const moveFile = require('../util/move-file')
 | |
| const PassThrough = require('stream').PassThrough
 | |
| const path = require('path')
 | |
| const pipe = BB.promisify(require('mississippi').pipe)
 | |
| const rimraf = BB.promisify(require('rimraf'))
 | |
| const ssri = require('ssri')
 | |
| const to = require('mississippi').to
 | |
| const uniqueFilename = require('unique-filename')
 | |
| const Y = require('../util/y.js')
 | |
| 
 | |
| const writeFileAsync = BB.promisify(fs.writeFile)
 | |
| 
 | |
| module.exports = write
 | |
| function write (cache, data, opts) {
 | |
|   opts = opts || {}
 | |
|   if (opts.algorithms && opts.algorithms.length > 1) {
 | |
|     throw new Error(
 | |
|       Y`opts.algorithms only supports a single algorithm for now`
 | |
|     )
 | |
|   }
 | |
|   if (typeof opts.size === 'number' && data.length !== opts.size) {
 | |
|     return BB.reject(sizeError(opts.size, data.length))
 | |
|   }
 | |
|   const sri = ssri.fromData(data, opts)
 | |
|   if (opts.integrity && !ssri.checkData(data, opts.integrity, opts)) {
 | |
|     return BB.reject(checksumError(opts.integrity, sri))
 | |
|   }
 | |
|   return BB.using(makeTmp(cache, opts), tmp => (
 | |
|     writeFileAsync(
 | |
|       tmp.target, data, {flag: 'wx'}
 | |
|     ).then(() => (
 | |
|       moveToDestination(tmp, cache, sri, opts)
 | |
|     ))
 | |
|   )).then(() => ({integrity: sri, size: data.length}))
 | |
| }
 | |
| 
 | |
| module.exports.stream = writeStream
 | |
| function writeStream (cache, opts) {
 | |
|   opts = opts || {}
 | |
|   const inputStream = new PassThrough()
 | |
|   let inputErr = false
 | |
|   function errCheck () {
 | |
|     if (inputErr) { throw inputErr }
 | |
|   }
 | |
| 
 | |
|   let allDone
 | |
|   const ret = to((c, n, cb) => {
 | |
|     if (!allDone) {
 | |
|       allDone = handleContent(inputStream, cache, opts, errCheck)
 | |
|     }
 | |
|     inputStream.write(c, n, cb)
 | |
|   }, cb => {
 | |
|     inputStream.end(() => {
 | |
|       if (!allDone) {
 | |
|         const e = new Error(Y`Cache input stream was empty`)
 | |
|         e.code = 'ENODATA'
 | |
|         return ret.emit('error', e)
 | |
|       }
 | |
|       allDone.then(res => {
 | |
|         res.integrity && ret.emit('integrity', res.integrity)
 | |
|         res.size !== null && ret.emit('size', res.size)
 | |
|         cb()
 | |
|       }, e => {
 | |
|         ret.emit('error', e)
 | |
|       })
 | |
|     })
 | |
|   })
 | |
|   ret.once('error', e => {
 | |
|     inputErr = e
 | |
|   })
 | |
|   return ret
 | |
| }
 | |
| 
 | |
| function handleContent (inputStream, cache, opts, errCheck) {
 | |
|   return BB.using(makeTmp(cache, opts), tmp => {
 | |
|     errCheck()
 | |
|     return pipeToTmp(
 | |
|       inputStream, cache, tmp.target, opts, errCheck
 | |
|     ).then(res => {
 | |
|       return moveToDestination(
 | |
|         tmp, cache, res.integrity, opts, errCheck
 | |
|       ).then(() => res)
 | |
|     })
 | |
|   })
 | |
| }
 | |
| 
 | |
| function pipeToTmp (inputStream, cache, tmpTarget, opts, errCheck) {
 | |
|   return BB.resolve().then(() => {
 | |
|     let integrity
 | |
|     let size
 | |
|     const hashStream = ssri.integrityStream({
 | |
|       integrity: opts.integrity,
 | |
|       algorithms: opts.algorithms,
 | |
|       size: opts.size
 | |
|     }).on('integrity', s => {
 | |
|       integrity = s
 | |
|     }).on('size', s => {
 | |
|       size = s
 | |
|     })
 | |
|     const outStream = fs.createWriteStream(tmpTarget, {
 | |
|       flags: 'wx'
 | |
|     })
 | |
|     errCheck()
 | |
|     return pipe(inputStream, hashStream, outStream).then(() => {
 | |
|       return {integrity, size}
 | |
|     }, err => {
 | |
|       return rimraf(tmpTarget).then(() => { throw err })
 | |
|     })
 | |
|   })
 | |
| }
 | |
| 
 | |
| function makeTmp (cache, opts) {
 | |
|   const tmpTarget = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
 | |
|   return fixOwner.mkdirfix(
 | |
|     path.dirname(tmpTarget), opts.uid, opts.gid
 | |
|   ).then(() => ({
 | |
|     target: tmpTarget,
 | |
|     moved: false
 | |
|   })).disposer(tmp => (!tmp.moved && rimraf(tmp.target)))
 | |
| }
 | |
| 
 | |
| function moveToDestination (tmp, cache, sri, opts, errCheck) {
 | |
|   errCheck && errCheck()
 | |
|   const destination = contentPath(cache, sri)
 | |
|   const destDir = path.dirname(destination)
 | |
| 
 | |
|   return fixOwner.mkdirfix(
 | |
|     destDir, opts.uid, opts.gid
 | |
|   ).then(() => {
 | |
|     errCheck && errCheck()
 | |
|     return moveFile(tmp.target, destination)
 | |
|   }).then(() => {
 | |
|     errCheck && errCheck()
 | |
|     tmp.moved = true
 | |
|     return fixOwner.chownr(destination, opts.uid, opts.gid)
 | |
|   })
 | |
| }
 | |
| 
 | |
| function sizeError (expected, found) {
 | |
|   var err = new Error(Y`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
 | |
|   err.expected = expected
 | |
|   err.found = found
 | |
|   err.code = 'EBADSIZE'
 | |
|   return err
 | |
| }
 | |
| 
 | |
| function checksumError (expected, found) {
 | |
|   var err = new Error(Y`Integrity check failed:
 | |
|   Wanted: ${expected}
 | |
|    Found: ${found}`)
 | |
|   err.code = 'EINTEGRITY'
 | |
|   err.expected = expected
 | |
|   err.found = found
 | |
|   return err
 | |
| }
 |