UNPKG

npm-verify-stream

Version:

A duplex stream for receiving a package tarball, verifying arbitrary checks, and emitting that same tarball on success.

183 lines (158 loc) 4.77 kB
'use strict'; var fs = require('fs'), os = require('os'), path = require('path'), zlib = require('zlib'), extend = require('util')._extend, async = require('async'), duplexify = require('duplexify'), fstream = require('fstream'), tar = require('tar'), PackageBuffer = require('npm-package-buffer'); var VerifyStream = module.exports = function VerifyStream(opts) { if (!(this instanceof VerifyStream)) { return new VerifyStream(opts); } if (!opts || !opts.checks || !opts.checks.length) { throw new Error('Checks are required to verify an npm package.'); } var self = this; this.log = opts.log || function () {}; this.read = opts.read || {}; this.read.log = this.read.log || (this.read.log !== false && this.log); this.concurrency = opts.concurrency || 5; this.before = opts.before; this.checks = opts.checks; this.cleanup = opts.cleanup; this.stream = duplexify(); this.stream.__verifier = this; // // When we are piped to then cache that stream // to a temporary location on disk. // // Remark: I believe this could be accomplished // with a through2 stream, but not sure. // this.stream.on('pipe', function (source) { self._cache(source); }); // // Setup our writable stream for parsing gzipped // tarball data as it is written to us. // this._building = true; this.gunzip = zlib.Unzip() .on('error', this._cleanup.bind(this)); this.writable = this.before || this.gunzip; // // Do not listen for errors on our tar parser because // those errors will be emitted by our PackageBuffer. // this.parser = tar.Parse(); this.buffer = new PackageBuffer(this.parser, this.read) .on('error', this._cleanup.bind(this)) .on('end', this.verify.bind(this)); if (this.before) { this.gunzip.pipe(this.parser); this.before.pipe(this.gunzip); } else { this.writable.pipe(this.parser); } this.stream.setWritable(this.writable); return this.stream; }; /* * function verify () * Runs the specified checks against the PackageBuffer */ VerifyStream.prototype.verify = function () { var self = this; var checks = typeof this.checks === 'function' ? this.checks(self.buffer) : this.checks; this._building = false; async.mapLimit( checks, this.concurrency, function runCheck(check, next) { check(self.buffer, next); }, function verifyChecks(err) { if (err) { return self._cleanup(err); } self._flushCache(); } ); }; /* * @private function _flushCache () * Reads the the cached tarball from disk and sets it * as the readable portion of the duplex stream. */ VerifyStream.prototype._flushCache = function () { if (!this.tmp) { // TODO: What do we do here? throw new Error('What is wrong?'); } this.readable = fs.createReadStream(this.tmp) .on('error', this._cleanup.bind(this)) .on('end', this._cleanup.bind(this)); this.stream.setReadable(this.readable); }; /* * @private function _cache (stream) * Caches the stream to a temporary file in a temporary directory */ VerifyStream.prototype._cache = function (source) { if (!this.tmp) { this._configure(); } this.log('cache', this.tmp); source.pipe(fs.createWriteStream(this.tmp)) .on('error', this._cleanup.bind(this)); // // Remark: is set _cacheComplete and attempting to emit output // handling a race we care about? // // .on('end', this._flushCache.bind(this)); }; /* * @private function _configure () * Configures this instance with a temporary cache file * location */ VerifyStream.prototype._configure = function () { var dir = os.tmpdir(); var now = process.hrtime().join('') + '.tgz'; this.tmp = path.join(dir, now); this.log('configure %s', this.tmp); }; /* * @private function _cleanup (err) * Cleans up the temporary file associated with this * instance. */ VerifyStream.prototype._cleanup = function (err) { var self = this; self._building = false; // // Do not clean if we are currently doing so or // we do not have a tmp tarball file. // if (this._cleaning || !this.tmp) { return; } this._cleaning = true; // // Emit an error on our stream instance if we // are passed one here. // TODO: inspection of the error to ignore edge cases // if (err) { this.stream.emit('error', err); } setImmediate(function () { self.log('cleanup %s', self.tmp); if (self.cleanup === false) { return self.log('skip cleanup %s', self.tmp); } fs.unlink(self.tmp, function (err) { var errState; self._cleaning = false; if (err && err.code !== 'ENOENT') { errState = err; } self.tmp = null; self.stream.emit('cleanup', self.tmp, errState); }); }); };