//
// Copyright (c) Microsoft and contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
var stream = require('stream');
var crypto = require('crypto');
var util = require('util');
var Constants = require('../util/constants');
var bufferSize = Constants.BlobConstants.DEFAULT_WRITE_BLOCK_SIZE_IN_BYTES;
/**
* Chunk stream
* 1. Calculate md5
* 2. Track reading offset
* 3. Work with customize memory allocator
* 4. Buffer data from stream.
* @param {object} options stream.Readable options
*/
function ChunkStream(options) {
stream.Stream.call(this);
this.writable = this.readable = true;
if (!options) {
options = {};
}
this._highWaterMark = options.highWaterMark || bufferSize;
this._paused = undefined; //True/false is the external status from users.
this._isStreamOpened = false;
this._offset = 0;
this._allocator = options.allocator;
this._streamEnded = false;
this._md5hash = null;
this._buffer = null;
this._internalBufferSize = 0;
this._md5sum = undefined;
if (options.calcContentMd5) {
this._md5hash = crypto.createHash('md5');
}
}
util.inherits(ChunkStream, stream.Stream);
/**
* Set the memory allocator.
*/
ChunkStream.prototype.setMemoryAllocator = function(allocator) {
this._allocator = allocator;
};
/**
* Internal stream ended
*/
ChunkStream.prototype.end = function (chunk, encoding, cb) {
Iif (typeof chunk === 'function') {
cb = chunk;
chunk = null;
encoding = null;
} else Iif (typeof encoding === 'function') {
cb = encoding;
encoding = null;
}
Iif (chunk) {
this.write(chunk, encoding);
}
this._streamEnded = true;
this._flushInternalBuffer();
Iif (cb) {
this.once('end', cb);
}
this.emit('end');
};
ChunkStream.prototype.finish = function () {
this.emit('finish');
this.destroy();
};
ChunkStream.prototype.error = function () {
this.emit('error');
this.destroy();
};
ChunkStream.prototype.destroy = function () {
this.writable = this.readable = false;
this.emit('close');
};
/**
* Add event listener
*/
ChunkStream.prototype.write = function (chunk, encoding) {
Eif (!this._isStreamOpened) {
this._isStreamOpened = true;
}
this._buildChunk(chunk, encoding);
return !this._paused;
};
/**
* Buffer the data into a chunk and emit it
*/
ChunkStream.prototype._buildChunk = function (data) {
if(this._md5hash) {
this._md5hash.update(data);
}
var dataSize = data.length;
var dataOffset = 0;
do {
var buffer = null;
var targetSize = this._internalBufferSize + dataSize;
if (targetSize < this._highWaterMark) {
// add the data to the internal buffer and return as it is not yet full
this._copyToInternalBuffer(data, dataOffset, data.length);
return;
} else Eif (targetSize == this._highWaterMark){
Eif(this._internalBufferSize === 0 && data.length === this._highWaterMark) {
// set the buffer to the data passed in to avoid creating a new buffer
buffer = data;
} else {
// add the data to the internal buffer and pop that buffer
this._copyToInternalBuffer(data, dataOffset, data.length);
buffer = this._popInternalBuffer();
}
dataSize = 0;
} else {
// add data to the internal buffer until its full, then return it
// set the dataSize parameter so that additional data is not lost
var copySize = this._highWaterMark - this._internalBufferSize;
this._copyToInternalBuffer(data, dataOffset, dataOffset + copySize);
dataSize -= copySize;
dataOffset += copySize;
buffer = this._popInternalBuffer();
}
this._emitBufferData(buffer);
} while(dataSize > 0);
};
/**
* Emit the buffer
*/
ChunkStream.prototype._emitBufferData = function(buffer) {
var newOffset = this._offset + buffer.length;
var range = {
start : this._offset,
end : newOffset - 1,
size : buffer.length
};
this._offset = newOffset;
this.emit('data', buffer, range);
};
/**
* Copy data into internal buffer
*/
ChunkStream.prototype._copyToInternalBuffer = function(data, start, end) {
Iif(start === undefined) start = 0;
Iif(end === undefined) end = data.length;
Eif (!this._buffer) {
this._buffer = this._allocateNewBuffer();
this._internalBufferSize = 0;
}
var copied = data.copy(this._buffer, this._internalBufferSize, start, end);
this._internalBufferSize += copied;
Iif(copied != (end - start)) {
throw new Error('Can not copy entire data to buffer');
}
};
/**
* Flush internal buffer
*/
ChunkStream.prototype._flushInternalBuffer = function() {
var buffer = this._popInternalBuffer();
if (buffer) {
this._emitBufferData(buffer);
}
};
/**
* Pop internal buffer
*/
ChunkStream.prototype._popInternalBuffer = function () {
var buf = null;
if (!this._buffer || this._internalBufferSize === 0) {
buf = null;
} else Iif(this._internalBufferSize == this._highWaterMark) {
buf = this._buffer;
} else {
buf = this._buffer.slice(0, this._internalBufferSize);
}
this._buffer = null;
this._internalBufferSize = 0;
return buf;
};
/**
* Allocate a buffer
*/
ChunkStream.prototype._allocateNewBuffer = function() {
var size = this._highWaterMark;
Iif(this._allocator && this._allocator.getBuffer) {
return this._allocator.getBuffer(size);
} else {
var buffer = new Buffer(size);
return buffer;
}
};
/**
* Get file content md5 when read completely.
*/
ChunkStream.prototype.getContentMd5 = function(encoding) {
Iif (!encoding) encoding = 'base64';
Iif(!this._md5hash) {
throw new Error('Can\'t get content md5, please set the calcContentMd5 option for FileReadStream.');
} else {
Eif (this._streamEnded) {
Eif (!this._md5sum) {
this._md5sum = this._md5hash.digest(encoding);
}
return this._md5sum;
} else {
throw new Error('Stream has not ended.');
}
}
};
/**
* Pause chunk stream
*/
ChunkStream.prototype.pause = function() {
this._paused = true;
};
/**
* Resume read stream
*/
ChunkStream.prototype.resume = function() {
Eif (this._paused) {
this._paused = false;
this.emit('drain');
}
};
module.exports = ChunkStream; |