• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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