Meteor is great but it lacks native supports for traditional file uploading. There are several options to handle file uploading:
From the client, data can be sent using:
In the server, the file can be saved to:
What are the pros and cons for these methods and how best to implement them? I am aware that there are also other options such as saving to a third party site and obtain an url.
When to Use GridFS. In MongoDB, use GridFS for storing files larger than 16 MB. In some situations, storing large files may be more efficient in a MongoDB database than on a system-level filesystem. If your filesystem limits the number of files in a directory, you can use GridFS to store as many files as needed.
GridFS uses two collections to store files. One collection stores the file chunks, and the other stores file metadata.
GridFS is the MongoDB specification for storing and retrieving large files such as images, audio files, video files, etc. It is kind of a file system to store files but its data is stored within MongoDB collections. GridFS has the capability to store files even greater than its document size limit of 16MB.
Large objects, or "files", are easily stored in MongoDB. It is no problem to store 100MB videos in the database. This has a number of advantages over files stored in a file system. Unlike a file system, the database will have no problem dealing with millions of objects.
You can achieve file uploading with Meteor without using any more packages or a third party
/*** client.js ***/ // asign a change event into input tag 'change input' : function(event,template){ var file = event.target.files[0]; //assuming 1 file only if (!file) return; var reader = new FileReader(); //create a reader according to HTML5 File API reader.onload = function(event){ var buffer = new Uint8Array(reader.result) // convert to binary Meteor.call('saveFile', buffer); } reader.readAsArrayBuffer(file); //read the file as arraybuffer } /*** server.js ***/ Files = new Mongo.Collection('files'); Meteor.methods({ 'saveFile': function(buffer){ Files.insert({data:buffer}) } });
Explanation
First, the file is grabbed from the input using HTML5 File API. A reader is created using new FileReader. The file is read as readAsArrayBuffer. This arraybuffer, if you console.log, returns {} and DDP can't send this over the wire, so it has to be converted to Uint8Array.
When you put this in Meteor.call, Meteor automatically runs EJSON.stringify(Uint8Array) and sends it with DDP. You can check the data in chrome console websocket traffic, you will see a string resembling base64
On the server side, Meteor call EJSON.parse() and converts it back to buffer
Pros
Cons
/*** client.js ***/ // asign a change event into input tag 'change input' : function(event,template){ var file = event.target.files[0]; if (!file) return; var xhr = new XMLHttpRequest(); xhr.open('POST', '/uploadSomeWhere', true); xhr.onload = function(event){...} xhr.send(file); } /*** server.js ***/ var fs = Npm.require('fs'); //using interal webapp or iron:router WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){ //var start = Date.now() var file = fs.createWriteStream('/path/to/dir/filename'); file.on('error',function(error){...}); file.on('finish',function(){ res.writeHead(...) res.end(); //end the respone //console.log('Finish uploading, time taken: ' + Date.now() - start); }); req.pipe(file); //pipe the request to the file });
Explanation
The file in the client is grabbed, an XHR object is created and the file is sent via 'POST' to the server.
On the server, the data is piped into an underlying file system. You can additionally determine the filename, perform sanitisation or check if it exists already etc before saving.
Pros
Cons
/*** client.js ***/ //same as option 2 /*** version A: server.js ***/ var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db; var GridStore = MongoInternals.NpmModule.GridStore; WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){ //var start = Date.now() var file = new GridStore(db,'filename','w'); file.open(function(error,gs){ file.stream(true); //true will close the file automatically once piping finishes file.on('error',function(e){...}); file.on('end',function(){ res.end(); //send end respone //console.log('Finish uploading, time taken: ' + Date.now() - start); }); req.pipe(file); }); }); /*** version B: server.js ***/ var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db; var GridStore = Npm.require('mongodb').GridStore; //also need to add Npm.depends({mongodb:'2.0.13'}) in package.js WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){ //var start = Date.now() var file = new GridStore(db,'filename','w').stream(true); //start the stream file.on('error',function(e){...}); file.on('end',function(){ res.end(); //send end respone //console.log('Finish uploading, time taken: ' + Date.now() - start); }); req.pipe(file); });
Explanation
The client script is the same as in option 2.
According to Meteor 1.0.x mongo_driver.js last line, a global object called MongoInternals is exposed, you can call defaultRemoteCollectionDriver() to return the current database db object which is required for the GridStore. In version A, the GridStore is also exposed by the MongoInternals. The mongo used by current meteor is v1.4.x
Then inside a route, you can create a new write object by calling var file = new GridStore(...) (API). You then open the file and create a stream.
I also included a version B. In this version, the GridStore is called using a new mongodb drive via Npm.require('mongodb'), this mongo is the latest v2.0.13 as of this writing. The new API doesn't require you to open the file, you can call stream(true) directly and start piping
Pros
Cons
Benchmark You can see in option 2 and option 3, I included var start = Date.now() and when writing end, I console.log out the time in ms, below is the result. Dual Core, 4 GB ram, HDD, ubuntu 14.04 based.
file size GridFS FS 100 KB 50 2 1 MB 400 30 10 MB 3500 100 200 MB 80000 1240
You can see that FS is much faster than GridFS. For a file of 200 MB, it takes ~80 sec using GridFS but only ~ 1 sec in FS. I haven't tried SSD, the result may be different. However, in real life, the bandwidth may dictate how fast the file is streamed from client to server, achieving 200 MB/sec transfer speed is not typical. On the other hand, a transfer speed ~2 MB/sec (GridFS) is more the norm.
Conclusion
By no mean this is comprehensive, but you can decide which option is best for your need.
Hopefully soon, meteor DDP can support gzip, caching etc and GridFS can be faster...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With