4

I'm trying to write a node module that accepts an incoming piped binary (or base-64-encoded) stream, but frankly I don't even know where to start. I can't see any examples in the Node docs about handling incoming streams; I only see examples on consuming them?

Say for example I want to be able to do this:

var asset = new ProjectAsset('myFile', __dirname + '/image.jpg')
var stream = fs.createReadStream(__dirname + '/image.jpg', { encoding: 'base64' }).pipe(asset)
stream.on('finish', function() {
    done()
})

I've gotten ProjectAsset looking like this, but I'm at a loss of where to go next:

'use strict'

var stream = require('stream'),
    util = require('util')

var ProjectAsset = function() {
    var self = this

    Object.defineProperty(self, 'binaryData', {
        configurable: true,
        writable: true
    })

    stream.Stream.call(self)

    self.on('pipe', function(src) {
        // does it happen here? how do I set self.binaryData?
    })

    return self
}

util.inherits(ProjectAsset, stream.Stream)

module.exports = ProjectAsset
module.exports.DEFAULT_FILE_NAME = 'file'
5
  • this might help to mention that the buffer module can handle binary data so if youre reading and translating the binary in the same module you might want to look at the buffer module Commented Feb 2, 2016 at 17:03
  • I'm actually using Buffer to store the data when setting manually anyway - I need to translate the piped stream to a buffer so I can determine content type and accurate length. Commented Feb 2, 2016 at 17:06
  • so if you know how to use the stream api one of the options is the even listener on "data" if you just append that data to your buffer it should get what youre looking for Commented Feb 2, 2016 at 17:10
  • Care to add in an answer? Commented Feb 2, 2016 at 17:18
  • yeah i added it to an answer Commented Feb 2, 2016 at 17:34

3 Answers 3

4
+50

It is possible to inherit from stream.Stream and make it work, however based on what's available in the documentation I would suggest inheriting from stream.Writable. Piping into a stream.Writable you'll need to have _write(chunk, encoding, done) defined to handle the piping. Here is an example:

var asset = new ProjectAsset('myFile', __dirname + '/image.jpg')
var stream = fs.createReadStream(__dirname + '/image.jpg', { encoding: 'base64' }).pipe(asset)
stream.on('finish', function() {
    console.log(asset.binaryData);
})

Project Asset

'use strict'

var stream = require('stream'),
    util = require('util')

var ProjectAsset = function() {
    var self = this

    self.data
    self.binaryData = [];

    stream.Writable.call(self)

    self._write = function(chunk, encoding, done) {
        // Can handle this data however you want
        self.binaryData.push(chunk.toString())
        // Call after processing data
        done()
    }
    self.on('finish', function() {
        self.data = Buffer.concat(self.binaryData)
    })

    return self
}

util.inherits(ProjectAsset, stream.Writable)

module.exports = ProjectAsset
module.exports.DEFAULT_FILE_NAME = 'file'

If you're looking to also read from the stream, take a look at inheriting from stream.Duplex and also including the _read(size) method.

There's also the simplified constructors api if you're doing something simpler.

Sign up to request clarification or add additional context in comments.

5 Comments

Sorry it took so long, but so far this looks like the way to go. Only last bit is that the binaryData is still an array of bytes; somehow I need to convert that to a Buffer as well so that I can infer content type from it etc.
Hrm, and when I try fs.writeFile(__dirname + '/image2.jpg', asset.binaryData), the output file is not the same as the input. In fact, it's not even renderable as a jpg.
For this you'll want to look at the Buffer class in node. I haven't messed with it much, but you can try starting by initializing the buffer as self.binaryData = new Buffer(''); From here you'll need to add data into the buffer doing something like this in your _write function: self.binaryData = Buffer.concat([self.binaryData, new Buffer(chunk, encoding)]). You may need to play with the encodings and try explicitly setting an encoding on the writeFile call.
So the missing piece here was that I needed to call Buffer.concat() on the binary data array. I'm going to have to figure out what to do about the bounty now, since both you and @Binvention helped equally!
Just added in the last piece that worked for me into your answer.
2

Im not sure if this is exaclty what you were looking for but i think you could handle it using the buffer api with Buffer.concat on an array of buffers that can be retrieved form chunk on the stream data listener

'use strict'

var stream = require('stream'),
    util = require('util');

var ProjectAsset = function() {
    var self = this

    Object.defineProperty(self, 'binaryData', {
        configurable: true,
        writable: true
    })

    stream.Stream.call(self)
    var data;
    var dataBuffer=[];
    self.on('data', function(chunk) {
        dataBuffer.push(chunk);
    }).on('end',function(){
        data=Buffer.concat(dataBuffer);
    });
    self.binaryData=data.toString('binary');
    return self
}

util.inherits(ProjectAsset, stream.Stream)

module.exports = ProjectAsset
module.exports.DEFAULT_FILE_NAME = 'file'

2 Comments

Nice! Not quite a working example, but in the end, I took @pohlman's code, and simply used Buffer.concat on the incoming buffer array in the end.
Yeah sorry I didn't have the time to test the code but glad I could help
2

Since your using var asset = new ProjectAsset('myFile', __dirname + '/image.jpg') I suppose your ProjectAsset responsibility is to take some input stream do some transformations and write that to a file. You could implement a transform stream because you receive some input from a stream and generate some output of it that can be saved to a file or to some other write stream.

You could of course implement a transform stream by inheriting from node.js Transform Stream but inheriting is quite cumbersome so my implementation uses through2 to implement the transform stream:

module.exports = through2(function (chunk, enc, callback) {
  // This function is called whenever a piece of data from the incoming stream is read
  // Transform the chunk or buffer the chunk in case you need more data to transform

  // Emit a data package to the next stream in the pipe or omit this call if you need more data from the input stream to be read
  this.push(chunk);

  // Signal through2 that you processed the incoming data package
  callback();
 }))

Usage

var stream = fs.createReadStream(__dirname + '/image.jpg', { encoding: 'base64' })
               .pipe(projectAsset)
               .pipe(fs.createWriteStream(__dirname + '/image.jpg'));

As you can see in this example implementing a stream pipeline fully decouples data transformation and saving of the data.

Factory Function

If you prefer to use a constructor like approach in the project asset module because you need to pass some values or things you could easily export a constructor function as shown below

var through2 = require('through2');

module.exports = function(someData) {

  // New stream is returned that can use someData argument for doing things
  return through2(function (chunk, enc, callback) {
    // This function is called whenever a piece of data from the incoming stream is read
    // Transform the chunk or buffer the chunk in case you need more data to transform

    // Emit a data package to the next stream in the pipe or omit this call if you need more data from the input stream to be read
    this.push(chunk);

    // Signal through2 that you processed the incoming data package
    callback();
  });
}

Usage

var stream = fs.createReadStream(__dirname + '/image.jpg', { encoding: 'base64' })
               .pipe(projectAsset({ foo: 'bar' }))
               .pipe(fs.createWriteStream(__dirname + '/image.jpg'));

6 Comments

I'm thinking of this more as a proxy - less likely to load from fs and more likely to load from request. I'm using fs though just to construct the function and write tests.
I like the idea of this, but in the interests of keeping 3rd party dependencies to a min., I'm going to implement it natively. Thanks for the tip on through2 though!
@remus through2 is better, less likely to mess things up. And easier. YMMW though.
Aright, well first, I can't wrap my class in through2 - I return the class not an instance of it in module.exports do that I can do var asset = new ProjectAsset
You could return a factory function from module.exports instead of a constructor which encapsulates how an instance is created. My tip: Don't OOP to much in JavaScript :)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.