NOTE: This tutorial uses Meteor 1.1.3 with cfs:standard-package 0.0.2 as of December 15, 2014. Since this feature is under active development, it may have changed by the time you read this. Feel free to contact me if you have any questions. :)

The Problem

Last week, a friend of mine asked me how to handle file uploads in a Meteor project. I recommended he use Collection FS, and he followed the README.md instructions on their repository. The file upload feature worked fine on localhost but failed once deployed to the free Meteor testing server. In fact, the server would keep refreshing and wouldn’t even load a page.

My Explanation

This issue arises because FS.filesystem uploads the image to a public folder directory. Due to security concerns, this is not allowed by the server unless properly configured. A workaround is to use GridFS as a storage adapter to insert images into MongoDB.

My Solution

Installation

First, instead of using cfs:filesystem, use cfs:gridfs for your package.

    meteor add cfs:standard-packages
    meteor add cfs:gridfs

Syntax

Next, when declaring your collection, switch from using FS.Collection to FS.Store.GridFS.

var imageStore = new FS.Store.GridFS("images")

Images = new FS.Collection("images", {
  stores: [imageStore],
})

Permissions

Then, set up ‘deny’ and ‘allow’ rules based on your needs.

Images.deny({
  insert: function () {
    return false
  },
  update: function () {
    return false
  },
  remove: function () {
    return false
  },
  download: function () {
    return false
  },
})

Images.allow({
  insert: function () {
    return true
  },
  update: function () {
    return true
  },
  remove: function () {
    return true
  },
  download: function () {
    return true
  },
})

User Interface

Add a file input button in your client template for users to click.

<input type="file" name="..." class="myFileInput" />

Handle the event as follows:

Template.Profile.events({
  "change .myFileInput": function (event, template) {
    FS.Utility.eachFile(event, function (file) {
      Images.insert(file, function (err, fileObj) {
        if (err) {
          // handle error
        } else {
          // handle success depending on your needs
          var userId = Meteor.userId()
          var imagesURL = {
            "profile.image": "/cfs/files/images/" + fileObj._id,
          }
          Meteor.users.update(userId, { $set: imagesURL })
        }
      })
    })
  },
})

Publication/Subscription

Lastly, don’t forget to set up publication and subscription in case you’ve removed the autopublish package.

Meteor.publish("images", function () {
  return Images.find()
})

Subscribe to it in your iron:router:

Router.route("/profile", {
  waitOn: function () {
    return Meteor.subscribe("images")
  },
  action: function () {
    if (this.ready()) this.render("Profile")
    else this.render("Loading")
  },
})

I hope this solution works for you. If you’re using an Amazon S3 bucket, consider using cfs:s3 as the adapter. If all else fails, Filepicker could serve as an alternative approach for handling file uploads. For more information, visit Filepicker’s website.