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