UNPKG

@lykmapipo/gridfs-stream

Version:

Writable/Readable Nodejs compatible GridFS streams

798 lines (681 loc) 22.1 kB
// fixture/logo.png var assert = require('assert') , Stream = require('stream') , fs = require('fs') , mongo = require('mongodb') , MongoClient = mongo.MongoClient , Grid = require('../') , crypto = require('crypto') , checksum = require('checksum') , tmpDir = __dirname + '/tmp/' , fixturesDir = __dirname + '/fixtures/' , imgReadPath = fixturesDir + 'mongo.png' , txtReadPath =fixturesDir + 'text.txt' , emptyReadPath = fixturesDir + 'emptydoc.txt' , largeBlobPath = tmpDir + '1mbBlob' , url = 'mongodb://localhost:27017' , dbName = 'gridstream_test' , client , db describe('test', function(){ var id; before(function (done) { if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir); } fs.writeFile(largeBlobPath, crypto.randomBytes(1024*1024), function (err) { if (err) { done(err); } MongoClient.connect(url, { useNewUrlParser: true }, function(err, _client) { client = _client; db = _client.db(dbName); done(err); }); }); }); describe('Grid', function () { it('should be a function', function () { assert('function' == typeof Grid); }); it('should create instances without the new keyword', function () { var x = Grid(2,3); assert(x instanceof Grid); }); it('should store the arguments', function () { var x = new Grid(4, 5); assert.equal(x.db, 4); assert.equal(x.mongo, 5); }); it('should require mongo argument', function(){ assert.throws(function () { new Grid(3) }, /missing mongo argument/); }) it('should require db argument', function(){ assert.throws(function () { new Grid(null, 3) }, /missing db argument/); }) describe('files', function(){ it('returns a collection', function(){ var g = new Grid(db, mongo); assert(g.files instanceof mongo.Collection); }) }) describe('collection()', function(){ it('changes the files collection', function(){ var g = new Grid(db, mongo); assert.equal('function', typeof g.collection); assert(g.collection() instanceof mongo.Collection); assert.equal(g.collection(), g.files); var old = g.collection(); g.collection('changed'); assert(g.collection() instanceof mongo.Collection); assert.ok(g.collection() == g.files); assert.ok(g.collection() != old); assert.equal(g.collection(), g.files); assert.equal(g.collection().collectionName, 'changed.files'); }) }); }); describe('createWriteStream', function(){ it('should be a function', function () { var x = Grid(1, mongo); assert('function' == typeof x.createWriteStream); }); }) describe('GridWriteStream', function(){ var g , ws before(function(){ Grid.mongo = mongo; g = Grid(db); ws = g.createWriteStream({ filename: 'logo.png' }); }); it('should be an instance of Stream', function(){ assert(ws instanceof Stream); }) it('should be an instance of Stream.Writable', function(){ assert(ws instanceof Stream.Writable); }) it('should should be writable', function(){ assert(ws.writable); }); it('should store the grid', function(){ assert(ws._grid == g) }); it('should have an id', function(){ assert(ws.id) }) it('id should be an ObjectId', function(){ assert(ws.id instanceof mongo.ObjectID); }); it('should have a name', function(){ assert(ws.name == 'logo.png') }) describe('options', function(){ it('should have one key', function(){ assert(Object.keys(ws.options).length === 1); }); it('should have filename option', function(){ assert(ws.options.filename === 'logo.png'); }); }) it('mode should default to w', function(){ assert(ws.mode == 'w'); }) describe('store', function(){ it('should be an instance of mongo.GridStore', function(){ assert(ws._store instanceof mongo.GridStore) }) }) describe('#methods', function(){ describe('write', function(){ it('should be a function', function(){ assert('function' == typeof ws.write) }) }) describe('end', function(){ it('should be a function', function(){ assert('function' == typeof ws.end) }) }) describe('destroy', function(){ it('should be a function', function(){ assert('function' == typeof ws.destroy) }) }) }); it('should provide piping from a readableStream into GridFS', function(done){ var readStream = fs.createReadStream(imgReadPath, { bufferSize: 1024 }); var ws = g.createWriteStream({ filename: 'logo.png'}); // used in readable stream test id = ws.id; var progress = 0; var finished = false; var opened = false; var closed = false; var file; ws.on('progress', function (size) { progress = size; }); ws.on('open', function () { opened = true; }); ws.on('close', function (file_) { closed = true; file = file_; }); ws.on('finish', function () { assert(opened); assert(progress > 0); assert(closed); assert(file.filename === 'logo.png'); assert(file._id === id); assert(file.length === fs.readFileSync(imgReadPath).length); done(); }); var pipe = readStream.pipe(ws); }); it('should provide Error and File object on WriteStream close event', function(done){ var readStream = fs.createReadStream(imgReadPath, { bufferSize: 1024 }); var ws = g.createWriteStream({ mode: 'w', filename: 'closeEvent.png', content_type: "image/png" }); // used in readable stream test id = ws.id; var progress = 0; ws.on('progress', function (size) { progress = size; }); ws.on('close', function (file) { assert(file.filename === 'closeEvent.png') assert(file.contentType === 'image/png') assert(progress > 0); done(); }); var pipe = readStream.pipe(ws); }); //W+ not supported in new mongodb v2 gridstore driver it.skip('should pipe more data to an existing GridFS file', function(done){ function pipe (id, cb) { if (!cb) cb = id, id = null; var readStream = fs.createReadStream(txtReadPath); var ws = g.createWriteStream({ _id: id, mode: 'w+' }); ws.on('close', function () { cb(ws.id); }); readStream.pipe(ws); } pipe(function (id) { pipe(id, function (id) { // read the file out. it should consist of two copies of original mongo.GridStore.read(db, id, function (err, txt) { if (err) return done(err); assert.equal(txt.length, fs.readFileSync(txtReadPath).length*2); done(); }); }); }) }); it('should be able to store a 12-letter file name', function() { var ws = g.createWriteStream({ filename: '12345678.png' }); assert.equal(ws.name,'12345678.png'); }); it("shouldn't clobber filename when rewriting to an existing file by id", function(done){ var ws = g.createWriteStream({ mode: 'w', filename: 'filename.txt', content_type: 'text/plain' }); var rewrite_id = ws.id; ws.write("Some text\n"); ws.end(); ws.on('close', function () { // Rewrite the same file by _id var ws2 = g.createWriteStream({ _id: rewrite_id, mode: 'w' }); ws2.write("Some more text\n"); ws2.end(); ws2.on('close', function () { g.exist({ _id: rewrite_id }, function (err, result) { if (err) return done(err); assert.ok(result); g.exist({ filename: "filename.txt" }, function (err, result) { if (err) return done(err); assert.ok(result); done(); }); }); }); }); }); it('should be able to store an empty file', function(done){ var readStream = fs.createReadStream(emptyReadPath); var ws = g.createWriteStream({ mode: 'w', filename: 'closeEvent.txt', content_type: "text/plain" }); ws.on('close', function (file) { assert(file.filename === 'closeEvent.txt') assert(file.contentType === 'text/plain') done(); }); var pipe = readStream.pipe(ws); }); it('should create files with an _id of arbitrary type', function(done){ var readStream = fs.createReadStream(imgReadPath, { bufferSize: 1024 }); var ws = g.createWriteStream({ _id: 'an_arbitrary_id', filename: 'file.img'}); ws.on('close', function (file) { assert(file._id === 'an_arbitrary_id'); done(); }); var pipe = readStream.pipe(ws); }); it('should emit finish after the file exists', function(done){ var readStream = fs.createReadStream(imgReadPath); var ws = g.createWriteStream({ filename: 'logo.png'}); ws.on('finish', function () { var rs = g.createReadStream({_id: id}); var file = fixturesDir + 'byid.png'; var writeStream = fs.createWriteStream(file); rs.on('error', function (err) { // should not happen assert(false); }); writeStream.on('finish', function () { var buf1 = fs.readFileSync(imgReadPath); var buf2 = fs.readFileSync(file); assert(buf1.length === buf2.length); for (var i = 0, len = buf1.length; i < len; ++i) { assert(buf1[i] == buf2[i]); } fs.unlinkSync(file); done(); }); rs.pipe(writeStream); }); var pipe = readStream.pipe(ws); }); it('should emit one error on destroy()', function(done){ var readStream = fs.createReadStream(imgReadPath, { bufferSize: 1024 }); var ws = g.createWriteStream({ filename: 'logo.png'}); var error = new Error('test error from destroy'); var errorCounter = 0; ws.on('error', function (err) { errorCounter += 1; assert(errorCounter === 1); assert(err === error); done(); }); ws.on('progress', function (progress) { ws.destroy(error); ws.destroy(); // test multiple destroy call }); var pipe = readStream.pipe(ws); }); it('should emit error on destroy() on nextTick', function(done){ var ws = g.createWriteStream({ filename: 'logo.png'}); ws.destroy(new Error('early destroy')); ws.on('error', function (err) { done(); }); }); it('should emit close if open is emitted on destroy()', function(done){ var ws = g.createWriteStream({ filename: 'logo.png'}); var opened = false; var error = false; ws.on('open', function () { opened = true; }); ws.on('close', function () { assert(opened); assert(error); done(); }); ws.destroy(); ws.on('error', function (err) { error = true; }); }); it('should create GridWriteStream without options.', function(done){ var ws = g.createWriteStream(); ws.on('close', function () { done(); }); ws.destroy(); ws.on('error', function (err) { assert(!err); }); }); }); describe('createReadStream', function(){ it('should be a function', function () { var x = Grid(1); assert('function' == typeof x.createReadStream); }); }); describe('GridReadStream', function(){ var g , rs before(function(){ g = Grid(db); rs = g.createReadStream({ filename: 'logo.png' }); }); it('should create an instance of Stream', function(){ assert(rs instanceof Stream); }); it('should should be readable', function(){ assert(rs.readable); }); it('should store the grid', function(){ assert(rs._grid == g) }); it('should have a name', function(){ assert(rs.name == 'logo.png') }) it('should not have an id', function(){ assert.equal(rs.id, null) }) describe('options', function(){ it('should have no defaults', function(){ // NOTE: filename is required to avoid a throw here, because you can't create a valid // read stream for a non-existing file. assert(Object.keys(g.createReadStream({filename: 'logo.png'}).options).length === 1); }); }) it('mode should default to r', function(){ assert(rs.mode == 'r'); assert(rs._store.mode == 'r'); }) describe('store', function(){ it('should be an instance of mongo.GridStore', function(){ assert(rs._store instanceof mongo.GridStore) }) }) describe('#methods', function(){ describe('setEncoding', function(){ it('should be a function', function(){ assert('function' == typeof rs.setEncoding) // TODO test actual encodings }) }) describe('pause', function(){ it('should be a function', function(){ assert('function' == typeof rs.pause) }) }) describe('destroy', function(){ it('should be a function', function(){ assert('function' == typeof rs.destroy) }) }) describe('resume', function(){ it('should be a function', function(){ assert('function' == typeof rs.resume) }) }) describe('pipe', function(){ it('should be a function', function(){ assert('function' == typeof rs.pipe) }) }) }); it('should provide piping to a writable stream by name', function(done){ var file = fixturesDir + 'byname.png'; var rs = g.createReadStream({ filename: 'logo.png' }); var writeStream = fs.createWriteStream(file); var opened = false; var ended = false; rs.on('open', function () { opened = true; }); rs.on('error', function (err) { throw err; }); rs.on('end', function () { ended = true; }); writeStream.on('close', function () { // check they are identical var buf1 = fs.readFileSync(imgReadPath); var buf2 = fs.readFileSync(file); assert(buf1.length === buf2.length); for (var i = 0, len = buf1.length; i < len; ++i) { assert(buf1[i] == buf2[i]); } assert(opened); assert(ended); fs.unlinkSync(file); done(); }); rs.pipe(writeStream); }); it('should provide piping to a writable stream by id', function(done){ var file = fixturesDir + 'byid.png'; var rs = g.createReadStream({ _id: id }); var writeStream = fs.createWriteStream(file); assert(rs.id instanceof mongo.ObjectID); assert(rs.id == String(id)) var opened = false; var ended = false; rs.on('open', function () { opened = true; }); rs.on('error', function (err) { throw err; }); rs.on('end', function () { ended = true; }); writeStream.on('close', function () { //check they are identical assert(opened); assert(ended); var buf1 = fs.readFileSync(imgReadPath); var buf2 = fs.readFileSync(file); assert(buf1.length === buf2.length); for (var i = 0, len = buf1.length; i < len; ++i) { assert(buf1[i] == buf2[i]); } fs.unlinkSync(file); done(); }); rs.pipe(writeStream); }); it('should provide piping to a writable stream with a range by id', function(done){ var file = fixturesDir + 'byid.png'; var rs = g.createReadStream({ _id: id, range: { startPos: 1000, endPos: 10000 } }); var writeStream = fs.createWriteStream(file); assert(rs.id instanceof mongo.ObjectID); assert(rs.id == String(id)) var opened = false; var ended = false; rs.on('open', function () { opened = true; }); rs.on('error', function (err) { throw err; }); rs.on('end', function () { ended = true; }); writeStream.on('close', function () { //check they are identical assert(opened); assert(ended); var buf1 = fs.readFileSync(imgReadPath); var buf2 = fs.readFileSync(file); assert(buf2.length === rs.options.range.endPos - rs.options.range.startPos + 1); for (var i = 0, len = buf2.length; i < len; ++i) { assert(buf1[i + rs.options.range.startPos] == buf2[i]); } fs.unlinkSync(file); done(); }); rs.pipe(writeStream); }); it('should read files with an _id of arbitrary type', function(done){ var rs = g.createReadStream({ _id: 'an_arbitrary_id'}); rs.on('open', function () { assert(rs.id === 'an_arbitrary_id'); done(); }); }); it('should allow checking for existence of files', function(done){ g.exist({ _id: id }, function (err, result) { if (err) return done(err); assert.ok(result); done(); }); }); it('should allow checking for non existence of files', function(done){ g.exist({ filename: 'does-not-exists.1234' }, function (err, result) { if (err) return done(err); assert.ok(!result); done(); }); }); // See #51 it('should allow checking for existence of files in an alternate root collection', function(done){ var alternateFileOptions = {filename: 'alternateLogo.png', root: 'alternate' }; var readStream = g.createReadStream({filename: 'logo.png'}); var writeStream = g.createWriteStream(alternateFileOptions); readStream.pipe(writeStream); writeStream.on('close', function () { g.exist(alternateFileOptions, function (err, result) { if (err) return done(err); assert.ok(result); done(); }); }); }); it('should get a specific file', function(done){ g.findOne({ _id: id }, function(err, result) { if (err) return done(err); assert.ok(result); done(); }); }); // See #72 it('should be able to find a file in an alternate root collection', function (done){ g.findOne({filename: 'alternateLogo.png', root: 'alternate' }, function (err, file) { assert.equal(err, null); done(); }); }); it('should allow removing files', function(done){ g.remove({ _id: id }, function (err) { if (err) return done(err); g.files.findOne({ _id: id }, function (err, doc) { if (err) return done(err); assert.ok(!doc); done(); }) }); }) it('should be possible to pause a stream after constructing it', function (done) { var rs = g.createReadStream({ filename: 'logo.png' }); rs.pause(); setTimeout(function () { rs.resume(); }, 1000); rs.on('data', function (data) { rs.destroy(); done(); }); }); //issue #46 it('should be able handle multiple streams with multiple chunks', function (done) { var doneCounter = 0; var totalCounter = 100; var checksums = []; this.timeout(10000); function doTest (i) { var copyFileName = tmpDir + 'logo' + i + '.png'; var readStream = g.createReadStream({filename: '1mbBlob'}); var writeStream = fs.createWriteStream(copyFileName); readStream.pipe(writeStream); writeStream.on('close', function () { checksum.file(copyFileName, function (err, sum) { checksums.push(sum); fs.unlinkSync(copyFileName); if (++doneCounter == totalCounter) { assert(checksums.filter(function (value, index, self) { return self.indexOf(value) === index; }).length === 1); done(); } }); }); } var writeStream = g.createWriteStream({filename: '1mbBlob'}); fs.createReadStream(largeBlobPath).pipe(writeStream); writeStream.on('close', function() { for (var i = totalCounter; i-- > 0;) { doTest(i); } }); }); it('should be able to set the encoding of a readstream', function (done) { var rs = g.createReadStream({ filename: 'logo.png' }); rs.setEncoding('utf8'); rs.on('data', function (data) { assert.equal(typeof data, 'string'); rs.destroy(); done(); }); }); it('should be able to pause/resume after a chunk is sent to be able to throttle the stream', function (done) { var rs = g.createReadStream({ filename: '1mbBlob' }); var numChuksSent = 0 // Pause stream after one chunk has been sent rs.on('data', function (data) { numChuksSent += 1; rs.pause(); }); // Only one chunk should have been sent because it was paused after that. 1mbBlob contains 5 with default gridstream chunk size setTimeout(function () { assert.equal( numChuksSent, 1 ); rs.resume(); }, 500); // Now there should be 2 setTimeout(function () { assert.equal( numChuksSent, 2 ); done() }, 1000); }); }); after(function (done) { fs.unlink(largeBlobPath, function (err) { if (err) { done(err); } fs.rmdir(tmpDir, function () { db.dropDatabase(function () { client.close(true, done); }); }); }); }); });