Proxy File Downloads in Meteor

Motivation

I haven’t blogged about Meteor in a long time (since 2014, in fact), so it’s about time. Conveniently, a recent Meteor project which required proxy routes to file downloads provides a great opportunity for a post, as it’s a feature that may well be of interest to others and is extremely easy to set up.

Under the hood

The Meteor core package webapp uses Connect under the hood to serve content to browsers, and it’s relatively easy to use WebApp.connectHandlers to register server-side routes from which to serve content outside your main web app.

However, extra functionality can be provided by adding the simple:json-routes package by core dev Sashko; ostensibly, its purpose is to allow apps to easily respond to requests (optionally including json bodies) with json objects. That’s not required here, but the package also includes connect-route, a really useful router for Connect which allows parameters to be passed in the URL. Adding connect-route could be achieved independently via meteorhacks:npm, but it’s much quicker to just add the simple:json-routes packages, which itself is designed to be lightweight:

meteor add simple:json-routes

A Contrived Example

Let’s say we have three individuals in a collection, all of which have associated files on the local filesystem. We’d like to set up a route from which we can download the relevant file by supplying the individual’s name.

People = new Mongo.Collection('people');

if (Meteor.isServer) {

  Meteor.startup(function () {

    if (!People.findOne()) {
      People.insert({name: 'alice', file: 'file-a.dat'});
      People.insert({name: 'bob', file: 'file-b.dat'});
      People.insert({name: 'charlie', file: 'file-a.dat'});
    }

  });

}

Registering Server-side Routes

The JSON-Routes API now makes it trivially easy to register an appropriate route, which we can use to return the file in question as a download.

// THIS CODE SHOULD BE RUN ONLY ON THE SERVER

// we can Npm.require fs as a core Node package, but we'd need to add
// meteorhacks:npm if we wanted to get the file stream from elsewhere
// (for example aws-sdk)
var fs = Npm.require('fs');

JsonRoutes.add('get', '/file/:name', function(req, res, next) {

  var person = People.findOne({name: req.params.name});

  if (person) {

    // indicate a download and set the filename of the returned file
    res.writeHead(200, {
      'Content-Disposition': 'attachment; filename=' + person.file,
    });
    // read a stream from the local filesystem, and pipe it to the response object
    // note that anything you put in the `private` directory will sit in
    // assets/app/ when the application has been built
    fs.createReadStream('assets/app/' + person.file).pipe(res);

  } else {

    // otherwise indicate that the name is not recognised
    res.writeHead(400);
    res.end('cannot find ' + req.params.name);

  }

});

Two things to point out in the above example:

  1. There’s no error-handling above, so if the app is unable to find the file in question it will simply throw.
  2. A more realistic example probably wouldn’t be reading files from the local filesystem, but more likely from dedicated cloud storage. However, it’s trivial to adapt this code to serve files by creating readable streams from AWS S3 or Google Cloud Storage. This way, the objects in question can remain both private and hidden, and only accessible to the public via your Meteor API.

Comments appreciated.