text: var crypto = Npm.require('crypto');
  var fs = Npm.require('fs');
  var path = Npm.require('path');
  
  var _disableSizeCheck = false;
  
  Meteor.AppCache = {
    config: function (options) {
      _.each(options, function (value, option) {
        if (option === 'browsers') {
          disabledBrowsers = {};
          _.each(value, function (browser) {
            disabledBrowsers[browser] = false;
          });
        }
        else if (option === 'onlineOnly') {
          _.each(value, function (urlPrefix) {
            RoutePolicy.declare(urlPrefix, 'static-online');
          });
        }
        // option to suppress warnings for tests.
        else if (option === '_disableSizeCheck') {
          _disableSizeCheck = value;
        }
        else if (value === false) {
          disabledBrowsers[option] = true;
        }
        else if (value === true) {
          disabledBrowsers[option] = false;
        } else {
          throw new Error('Invalid AppCache config option: ' + option);
        }
      });
    }
  };
  
  var disabledBrowsers = {};
  var browserDisabled = function (request) {
    return disabledBrowsers[request.browser.name];
  };
  
  WebApp.addHtmlAttributeHook(function (request) {
    if (browserDisabled(request))
      return null;
    else
      return { manifest: "/app.manifest" };
  });
  
  WebApp.connectHandlers.use(function (req, res, next) {
    if (req.url !== '/app.manifest') {
      return next();
    }
  
    // Browsers will get confused if we unconditionally serve the
    // manifest and then disable the app cache for that browser.  If
    // the app cache had previously been enabled for a browser, it
    // will continue to fetch the manifest as long as it's available,
    // even if we now are not including the manifest attribute in the
    // app HTML.  (Firefox for example will continue to display "this
    // website is asking to store data on your computer for offline
    // use").  Returning a 404 gets the browser to really turn off the
    // app cache.
  
    if (browserDisabled(WebApp.categorizeRequest(req))) {
      res.writeHead(404);
      res.end();
      return;
    }
  
    var manifest = "CACHE MANIFEST\n\n";
  
    // After the browser has downloaded the app files from the server and
    // has populated the browser's application cache, the browser will
    // *only* connect to the server and reload the application if the
    // *contents* of the app manifest file has changed.
    //
    // So to ensure that the client updates if client resources change,
    // include a hash of client resources in the manifest.
  
    manifest += "# " + WebApp.clientHash() + "\n";
  
    // When using the autoupdate package, also include
    // AUTOUPDATE_VERSION.  Otherwise the client will get into an
    // infinite loop of reloads when the browser doesn't fetch the new
    // app HTML which contains the new version, and autoupdate will
    // reload again trying to get the new code.
  
    if (Package.autoupdate) {
      var version = Package.autoupdate.Autoupdate.autoupdateVersion;
      if (version !== WebApp.clientHash())
        manifest += "# " + version + "\n";
    }
  
    manifest += "\n";
  
    manifest += "CACHE:" + "\n";
    manifest += "/" + "\n";
    _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
      if (resource.where === 'client' &&
          ! RoutePolicy.classify(resource.url)) {
        manifest += resource.url;
        // If the resource is not already cacheable (has a query
        // parameter, presumably with a hash or version of some sort),
        // put a version with a hash in the cache.
        //
        // Avoid putting a non-cacheable asset into the cache, otherwise
        // the user can't modify the asset until the cache headers
        // expire.
        if (!resource.cacheable)
          manifest += "?" + resource.hash;
  
        manifest += "\n";
      }
    });
    manifest += "\n";
  
    manifest += "FALLBACK:\n";
    manifest += "/ /" + "\n";
    // Add a fallback entry for each uncacheable asset we added above.
    //
    // This means requests for the bare url (/image.png instead of
    // /image.png?hash) will work offline. Online, however, the browser
    // will send a request to the server. Users can remove this extra
    // request to the server and have the asset served from cache by
    // specifying the full URL with hash in their code (manually, with
    // some sort of URL rewriting helper)
    _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
      if (resource.where === 'client' &&
          ! RoutePolicy.classify(resource.url) &&
          !resource.cacheable) {
        manifest += resource.url + " " + resource.url +
          "?" + resource.hash + "\n";
      }
    });
  
    manifest += "\n";
  
    manifest += "NETWORK:\n";
    // TODO adding the manifest file to NETWORK should be unnecessary?
    // Want more testing to be sure.
    manifest += "/app.manifest" + "\n";
    _.each(
      [].concat(
        RoutePolicy.urlPrefixesFor('network'),
        RoutePolicy.urlPrefixesFor('static-online')
      ),
      function (urlPrefix) {
        manifest += urlPrefix + "\n";
      }
    );
    manifest += "*" + "\n";
  
    // content length needs to be based on bytes
    var body = new Buffer(manifest);
  
    res.setHeader('Content-Type', 'text/cache-manifest');
    res.setHeader('Content-Length', body.length);
    return res.end(body);
  });
  
  var sizeCheck = function () {
    var totalSize = 0;
    _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
      if (resource.where === 'client' &&
          ! RoutePolicy.classify(resource.url)) {
        totalSize += resource.size;
      }
    });
    if (totalSize > 5 * 1024 * 1024) {
      Meteor._debug(
        "** You are using the appcache package but the total size of the\n" +
        "** cached resources is " +
        (totalSize / 1024 / 1024).toFixed(1) + "MB.\n" +
        "**\n" +
        "** This is over the recommended maximum of 5 MB and may break your\n" +
        "** app in some browsers! See http://docs.meteor.com/#appcache\n" +
        "** for more information and fixes.\n"
      );
    }
  };
  
  // Run the size check after user code has had a chance to run. That way,
  // the size check can take into account files that the user does not
  // want cached. Otherwise, the size check warning will still print even
  // if the user excludes their large files with
  // `Meteor.AppCache.config({onlineOnly: files})`.
  Meteor.startup(function () {
    if (! _disableSizeCheck)
      sizeCheck();
  });
  