@lykmapipo/gridfs-stream
Version:
Writable/Readable Nodejs compatible GridFS streams
279 lines (221 loc) • 6.35 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
//var Writable = require('stream').Writable;
// This is a workaround to implement a _flush method for Writable (like for Transform) to emit the 'finish' event only after all data has been flushed to the underlying system (GridFS). See https://www.npmjs.com/package/flushwritable and https://github.com/joyent/node/issues/7348
var FlushWritable = require('flushwritable');
/**
* expose
* @ignore
*/
module.exports = exports = GridWriteStream;
/**
* GridWriteStream
*
* @param {Grid} grid
* @param {Object} options (optional)
*/
function GridWriteStream (grid, options) {
if (!(this instanceof GridWriteStream))
return new GridWriteStream(grid, options);
FlushWritable.call(this);
this._opened = false;
this._opening = false;
this._writable = true;
this._closing = false;
this._destroyed = false;
this._errorEmitted = false;
this._grid = grid;
// a bit backwards compatible
if (typeof options === 'string') {
options = { filename: options };
}
this.options = options || {};
if(this.options._id) {
this.id = grid.tryParseObjectId(this.options._id);
if(!this.id) {
this.id = this.options._id;
}
}
this.name = this.options.filename; // This may be undefined, that's okay
if (!this.id) {
//_id not passed or unparsable? This is a new file!
this.id = new grid.mongo.ObjectID();
this.name = this.name || ''; // A new file needs a name
}
this.mode = 'w'; //Mongodb v2 driver have disabled w+ because of possible data corruption. So only allow `w` for now.
// The value of this.name may be undefined. GridStore treats that as a missing param
// in the call signature, which is what we want.
this._store = new grid.mongo.GridStore(grid.db, this.id, this.name, this.mode, this.options);
this._delayedWrite = null;
this._delayedFlush = null;
this._delayedClose = null;
var self = this;
self._open();
}
/**
* Inherit from stream.Writable (FlushWritable for workaround to defer finish until all data flushed)
* @ignore
*/
util.inherits(GridWriteStream, FlushWritable);
// private api
/**
* _open
*
* @api private
*/
GridWriteStream.prototype._open = function () {
if (this._opened) return;
if (this._opening) return;
this._opening = true;
var self = this;
this._store.open(function (err, gs) {
self._opening = false;
if (err) return self._error(err);
self._opened = true;
self.emit('open');
// If _close was called during _store opening, then it was delayed until now, so do the close now
if (self._delayedClose) {
var closed = self._delayedClose.cb;
self._delayedClose = null;
return self._closeInternal(closed);
}
// If _flush was called during _store opening, then it was delayed until now, so do the flush now (it's necessarily an empty GridFS file, no _write could have been called and have finished)
if (self._delayedFlush) {
var flushed = self._delayedFlush;
self._delayedFlush = null;
return self._flushInternal(flushed);
}
// If _write was called during _store opening, then it was delayed until now, so do the write now (_flush could not have been called yet as _write has not finished yet)
if (self._delayedWrite) {
var delayedWrite = self._delayedWrite;
self._delayedWrite = null;
return self._writeInternal(delayedWrite.chunk, delayedWrite.encoding, delayedWrite.done);
}
});
}
/**
* _writeInternal
*
* @api private
*/
GridWriteStream.prototype._writeInternal = function (chunk, encoding, done) {
// If destroy or error no more data will be written.
if (!this._writable) return;
var self = this;
// Write the chunk to the GridStore. The write head automatically moves along with each write.
this._store.write(chunk, function (err, store) {
if (err) return self._error(err);
// Emit the write head position
self.emit('progress', store.position);
// We are ready to receive a new chunk from the writestream - call done().
done();
});
}
/**
* _write
*
* @api private
*/
GridWriteStream.prototype._write = function (chunk, encoding, done) {
if (this._opening) {
// if we are still opening the store, then delay the write until it is open.
this._delayedWrite = {chunk: chunk, encoding: encoding, done: done};
return;
}
// otherwise, do the write now
this._writeInternal(chunk, encoding, done);
}
/**
* _flushInternal
*
* @api private
*/
GridWriteStream.prototype._flushInternal = function (flushed) {
this._close(flushed);
}
/**
* _flush
*
* @api private
*/
GridWriteStream.prototype._flush = function (flushed) {
// _flush is called when all _write() have finished (even if no _write() was called (empty GridFS file))
if (this._opening) {
// if we are still opening the store, then delay the flush until it is open.
this._delayedFlush = flushed;
return;
}
// otherwise, do the flush now
this._flushInternal(flushed);
}
/**
* _closeInternal
*
* @api private
*/
GridWriteStream.prototype._closeInternal = function (cb) {
if (!this._opened) return;
if (this._closing) return;
this._closing = true;
var self = this;
this._store.close(function (err, file) {
self._closing = false;
self._opened = false;
if (err) return self._error(err);
self.emit('close', file);
if (cb) cb();
});
}
/**
* _close
*
* @api private
*/
GridWriteStream.prototype._close = function _close (cb) {
if (this._opening) {
// if we are still opening the store, then delay the close until it is open.
this._delayedClose = { cb: cb };
return;
}
// otherwise, do the close now
this._closeInternal(cb);
}
/**
* _error
*
* @api private
*/
GridWriteStream.prototype._error = function _error (err) {
// Stop receiving more data to write, emit `error` and close the store
if (this._errorEmitted) return;
this._errorEmitted = true;
this._writable = false;
this.emit('error', err);
this._close();
}
// public api
/**
* destroy
*
* @api public
*/
GridWriteStream.prototype.destroy = function destroy (err) {
// Abort the write stream, even if write not completed
if (this._destroyed) return;
this._destroyed = true;
var self = this;
process.nextTick(function() {
self._error(err);
});
}
/**
* destroySoon
*
* @api public
* @deprecated just use destroy()
*/
GridWriteStream.prototype.destroySoon = function destroySoon () {
return this.destroy();
};