@lykmapipo/gridfs-stream
Version:
Writable/Readable Nodejs compatible GridFS streams
248 lines (189 loc) • 6.41 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
var Readable = require('stream').Readable;
/**
* expose
* @ignore
*/
module.exports = exports = GridReadStream;
/**
* GridReadStream
*
* @param {Grid} grid
* @param {Object} options
*/
function GridReadStream (grid, options) {
if (!(this instanceof GridReadStream))
return new GridReadStream(grid, options);
Readable.call(this);
this._opened = false;
this._opening = false;
this._closing = false;
this._end = false;
this._needToPush = false;
this._grid = grid;
// a bit backwards compatible
if (typeof options === 'string') {
options = { filename: options };
}
this.options = options || {};
if(options._id) {
this.id = grid.tryParseObjectId(options._id);
if(!this.id) {
this.id = options._id;
}
}
this.name = this.options.filename || '';
this.mode = 'r';
// If chunk size specified use it for read chunk size otherwise default to 255k (GridStore default). chunkSize and chunk_size in mongodb api so check both.
this._chunkSize = this.options.chunkSize || this.options.chunk_size || 1024 * 255;
this.range = this.options.range || { startPos: 0, endPos: undefined };
if (typeof(this.range.startPos) === 'undefined') {
this.range.startPos = 0;
}
this._currentPos = this.range.startPos;
var options = {};
for (var i in this.options) { options[i] = this.options[i]; }
options.root || (options.root = this._grid.curCol);
this._store = new grid.mongo.GridStore(grid.db, this.id || new grid.mongo.ObjectID(), this.name, this.mode, options);
// Workaround for Gridstore issue https://github.com/mongodb/node-mongodb-native/pull/930
if (!this.id) {
// var REFERENCE_BY_FILENAME = 0,
this._store.referenceBy = 0;
}
var self = this;
//Close the store once `end` received
this.on('end', function() {
self._end = true;
self._close()
});
process.nextTick(function() {
self._open();
});
}
/**
* Inherit from stream.Readable
* @ignore
*/
util.inherits(GridReadStream, Readable);
/**
* _open
*
* @api private
*/
GridReadStream.prototype._open = function _open () {
if (this._opening) return;
this._opening = true;
var self = this;
// Open the store
this._store.open(function (err, gs) {
if (err) return self._error(err);
// Find the length of the file by setting the head to the end of the file and requesting the position
self._store.seek(0, self._grid.mongo.GridStore.IO_SEEK_END, function(err) {
if (err) return self._error(err);
// Request the position of the end of the file
self._store.tell(function(err, position) {
if (err) return self._error(err);
// Calculate the correct end position either from EOF or end of range. Also handle incorrect range request.
if (!self.range.endPos || self.range.endPos > position-1) {self.range.endPos = position - 1};
// Set the read head to the beginning of the file or start position if specified
self._store.seek(self.range.startPos, self._grid.mongo.GridStore.IO_SEEK_SET, function(err) {
if (err) return self._error(err);
// The store is now open
self.emit('open');
self._opened = true;
// If `_read()` was already called then we need to start pushing data to the stream. Otherwise `_read()` will handle this once called from stream.
if (self._needToPush) self._push();
});
});
});
});
}
/**
* _read
*
* @api private
*/
// `_read()` will be called when the stream wants to pull more data in
// The advisory `size` argument is ignored in this case and user specified use or default to 255kk.
GridReadStream.prototype._read = function _read (size) {
var self = this;
// Set `_needToPush` to true because the store may still be closed if data is immediately piped. Once the store is open `_needToPush` is checked and _push() called if necessary.
self._needToPush = true;
// The store must be open
if (!this._opened) return;
// Read data from GridStore and push to stream
self._push();
}
/**
* _push
*
* @api private
*/
GridReadStream.prototype._push = function _push () {
var self = this;
// Do not continue if the store is closed
if (!this._opened) return self._error('Unable to push data. Expected gridstore to be open');
// Check if EOF, if the full requested range has been pushed or if the stream must be destroyed. If so than push EOF-signalling `null` chunk
if ( !this._store.eof() && (self._currentPos <= self.range.endPos) && !this._end) {
// Determine the chunk size for the read from GridStore
// Use default chunk size or user specified
var readChunkSize = self._chunkSize
// Override the chunk size if the chunk size is more than the size that is left until EOF/range
if (self.range.endPos-self._currentPos < self._chunkSize) {readChunkSize = self.range.endPos - self._currentPos + 1};
// Read the chunk from GridStore. Head moves automatically after each read.
self._store.read(readChunkSize,function(err, data) {
// If error stop and close the store
if (err) return self._error(err);
// Advance the current position of the read head
self._currentPos += data.length;
// Push data
if (!self._end) self.push(data)
})
} else {
// Push EOF-signalling `null` chunk
this._end = true;
self.push(null);
}
}
/**
* _close
*
* @api private
*/
GridReadStream.prototype._close = function _close () {
var self = this;
if (!self._opened) return;
if (self._closing) return;
this._closing = true;
// Close the store and emit `close` event
self._store.close(function (err) {
if (err) return self._error(err);
self.emit('close');
});
}
/**
* _error
*
* @api private
*/
GridReadStream.prototype._error = function _error (err) {
// Set end true so that no further reads from GridStore are possible and close the store
this._end = true;
// Emit the error event
this.emit('error', err);
// Close the gridstore if an error is received.
this._close()
}
/**
* destroy
*
* @api public
*/
GridReadStream.prototype.destroy = function destroy () {
// Set end true so that no further reads from GridSotre are possible and close the store
this._end = true;
this._close();
}