





















































In this article by Isaac Strack, author of the book Meteor Cookbook, we will cover the following recipe:
(For more resources related to this topic, see here.)
Adding files via a web application is a pretty standard functionality nowadays. That doesn't mean that it's easy to do, programmatically. New browsers support Web APIs to make our job easier, and a lot of quality libraries/packages exist to help us navigate the file reading/uploading forests, but, being the coding lumberjacks that we are, we like to know how to roll our own! In this recipe, you will learn how to read and upload image files to a Meteor server.
We will be using a default project installation, with client, server, and both folders, and with the addition of a special folder for storing images. In a terminal window, navigate to where you would like your project to reside, and execute the following commands:
$ meteor create imageupload $ cd imageupload $ rm imageupload.* $ mkdir client $ mkdir server $ mkdir both $ mkdir .images
Note the dot in the .images folder. This is really important because we don't want the Meteor application to automatically refresh every time we add an image to the server! By creating the images folder as .images, we are hiding it from the eye-of-Sauron-like monitoring system built into Meteor, because folders starting with a period are "invisible" to Linux or Unix.
Let's also take care of the additional Atmosphere packages we'll need. In the same terminal window, execute the following commands:
$ meteor add twbs:bootstrap $ meteor add voodoohop:masonrify
We're now ready to get started on building our image upload application.
We want to display the images we upload, so we'll be using a layout package (voodoohop:masonrify) for display purposes. We will also initiate uploads via drag and drop, to cut down on UI components. Lastly, we'll be relying on an npm module to make the file upload much easier. Let's break this down into a few steps, starting with the user interface.
<body> <h1>Images!</h1> {{> display}} {{> dropzone}} </body> <template name="display"> {{#masonryContainer columnWidth=50 transitionDuration="0.2s" id="MasonryContainer" }} {{#each imgs}} {{> img}} {{/each}} {{/masonryContainer}} </template> <template name="dropzone"> <div id="dropzone" class="{{dropcloth}}">Drag images here...</div> </template> <template name="img"> {{#masonryElement "MasonryContainer"}} <img src="{{src}}" class="display-image" style="width:{{calcWidth}}"/> {{/masonryElement}} </template>
body { background-color: #f5f0e5; font-size: 2rem; } div#dropzone { position: fixed; bottom:5px; left:2%; width:96%; height:100px; margin: auto auto; line-height: 100px; text-align: center; border: 3px dashed #7f898d; color: #7f8c8d; background-color: rgba(210,200,200,0.5); } div#dropzone.active { border-color: #27ae60; color: #27ae60; background-color: rgba(39, 174, 96,0.3); } img.display-image { max-width: 400px; }
Images = new Mongo.Collection('images');
imgFile = function (d) { d = d || {}; this.name = d.name; this.type = d.type; this.source = d.source; this.size = d.size; };
imgFile.fromJSONValue = function (d) { return new imgFile({ name: d.name, type: d.type, source: EJSON.fromJSONValue(d.source), size: d.size }); }; imgFile.prototype = { constructor: imgFile, typeName: function () { return 'imgFile' }, equals: function (comp) { return (this.name == comp.name && this.size == comp.size); }, clone: function () { return new imgFile({ name: this.name, type: this.type, source: this.source, size: this.size }); }, toJSONValue: function () { return { name: this.name, type: this.type, source: EJSON.toJSONValue(this.source), size: this.size }; } }; EJSON.addType('imgFile', imgFile.fromJSONValue);
The EJSON code used in this recipe is heavily inspired by Chris Mather's Evented Mind file upload tutorials. We recommend checking out his site and learning even more about file uploading at https://www.eventedmind.com.
if (Meteor.isClient){ _.extend(imgFile.prototype, { read: function (f, callback) { var fReader = new FileReader; var self = this; callback = callback || function () {}; fReader.onload = function() { self.source = new Uint8Array(fReader.result); callback(null,self); }; fReader.onerror = function() { callback(fReader.error); }; fReader.readAsArrayBuffer(f); } }); _.extend (imgFile, { read: function (f, callback){ return new imgFile(f).read(f,callback); } }); }; if (Meteor.isServer){ var fs = Npm.require('fs'); var path = Npm.require('path'); _.extend(imgFile.prototype, { save: function(dirPath, options){ var fPath = path.join(process.env.PWD,dirPath,this.name); var imgBuffer = new Buffer(this.source); fs.writeFileSync(fPath, imgBuffer, options); } }); };
Meteor.methods({ addURL : function(uri){ Images.insert({src:uri}); }, uploadIMG : function(iFile){ iFile.save('.images',{}); Images.insert({src:'images/' +iFile.name}); } });
var fs = Npm.require('fs'); WebApp.connectHandlers.use(function(req, res, next) { var re = /^/images/(.*)$/.exec(req.url); if (re !== null) { var filePath = process.env.PWD + '/.images/'+ re[1]; var data = fs.readFileSync(filePath, data); res.writeHead(200, { 'Content-Type': 'image' }); res.write(data); res.end(); } else { next(); } });
Template.display.helpers({ imgs: function () { return Images.find(); } });
Template.display.events({ 'dblclick .display-image': function (e) { Images.remove({ _id: this._id }); } });
Template.dropzone.helpers({ dropcloth: function () { return Session.get('dropcloth'); } }); Template.dropzone.events({ 'dragover #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth', 'active'); }, 'dragleave #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth'); } });
'dragleave #dropzone': function (e) { ... }, 'drop #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth'); var files = e.originalEvent.dataTransfer.files; var images = $(e.originalEvent.dataTransfer.getData('text/html')).find('img'); var fragment = _.findWhere(e.originalEvent.dataTransfer.items, { type: 'text/html' }); if (files.length) { _.each(files, function (e, i, l) { imgFile.read(e, function (error, imgfile) { Meteor.call('uploadIMG', imgfile, function (e) { if (e) { console.log(e.message); } }); }) }); } else if (images.length) { _.each(images, function (e, i, l) { Meteor.call('addURL', $(e).attr('src')); }); } else if (fragment) { fragment.getAsString(function (e) { var frags = $(e); var img = _.find(frags, function (e) { return e.hasAttribute('src'); }); if (img) Meteor.call('addURL', img.src); }); } } });
As you drag and drop the dinosaur images in to the drop zone, they will be uploaded as shown in the following screenshot:
Similarly, dragging and dropping actual files will just as quickly upload and then display images, as shown in the following screenshot:
There are a lot of moving parts to the code we just created, but we can refine it down to four areas.
First, we created a new imgFile object, complete with the internal functions added via the Object.prototype = {…} declaration. The functions added here ( typeName, equals, clone, toJSONValue and fromJSONValue) are primarily used to allow the imgFile object to be serialized and deserialized properly on the client and the server. Normally, this isn't needed, as we can just insert into Mongo Collections directly, but in this case it is needed because we want to use the FileReader and Node fs packages on the client and server respectively to directly load and save image files, rather than write them to a collection.
Second, the underscore _.extend() method is used on the client side to create the read() function, and on the server side to create the save() function. read takes the file(s) that were dropped, reads the file into an ArrayBuffer, and then calls the included callback, which uploads the file to the server. The save function on the server side reads the ArrayBuffer, and writes the subsequent image file to a specified location on the server (in our case, the .images folder).
Third, we created an ondropped event handler, using the 'drop #dropzone' event. This handler determines whether an actual file was dragged and dropped, or if it was simply an HTML <img> element, which contains a URI link in the src property. In the case of a file (determined by files.length), we call the imgFile.read command, and pass a callback with an immediate Meteor.call('uploadIMG'…) method. In the case of an <img> tag, we parse the URI from the src attribute, and use Meteor.call('addURL') to update the Images collection.
Fourth, we have our helper functions for updating the UI. These include Template.helpers functions, Template.events functions, and the WebApp.connectedHandlers.use() function, used to properly serve uploaded images without having to update the UI each time a file is uploaded. Remember, Meteor will update the UI automatically on any file change. This unfortunately includes static files, such as images. To work around this, we store our images in a file invisible to Meteor (using .images). To redirect the traffic to that hidden folder, we implement the .use() method to listen for any traffic meant to hit the '/images/' folder, and redirect it accordingly.
As with any complex recipe, there are other parts to the code, but this should cover the major aspects of file uploading (the four areas mentioned in the preceding section).
The next logical step is to not simply copy the URIs from remote image files, but rather to download, save, and serve local copies of those remote images. This can also be done using the FileReader and Node fs libraries, and can be done either through the existing client code mentioned in the preceding section, or directly on the server, as a type of cron job.
For more information on FileReader, please see the MDN FileReader article, located at https://developer.mozilla.org/en-US/docs/Web/API/FileReader.
In this article, you have learned the basic steps to upload images using the HTML FileReader.
Further resources on this subject: