You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2014/05/21 16:29:42 UTC

git commit: Add pushing via zip file logic.

Repository: cordova-app-harness
Updated Branches:
  refs/heads/master d58c684e9 -> 4a366039f


Add pushing via zip file logic.


Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/4a366039
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/4a366039
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/4a366039

Branch: refs/heads/master
Commit: 4a366039f8a1ac343ace5eb5bdaabf5880d08ab2
Parents: d58c684
Author: Andrew Grieve <ag...@chromium.org>
Authored: Wed May 21 10:29:05 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Wed May 21 10:29:05 2014 -0400

----------------------------------------------------------------------
 harness-push/harness-push.js  | 521 +++++++++++++++++++++----------------
 harness-push/package.json     |   7 +-
 www/cdvah/js/HarnessServer.js | 102 +++++---
 3 files changed, 373 insertions(+), 257 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/4a366039/harness-push/harness-push.js
----------------------------------------------------------------------
diff --git a/harness-push/harness-push.js b/harness-push/harness-push.js
index d505ea0..07e6ff1 100755
--- a/harness-push/harness-push.js
+++ b/harness-push/harness-push.js
@@ -25,34 +25,90 @@ var fs = require('fs'),
     Q = require('q'),
     request = require('request'),
     nopt = require('nopt'),
-    shelljs = require('shelljs');
+    shelljs = require('shelljs'),
+    JSZip = require('jszip');
+
+var IGNORE_PATH_FOR_PUSH_REGEXP = /www\/(?:plugins\/|cordova\.js)/;
+
+function getDerivedWwwDir(dir, platformId) {
+    if (platformId == 'android') {
+        return path.join(dir, 'platforms', platformId, 'assets', 'www');
+    } else if (platformId == 'ios') {
+        return path.join(dir, 'platforms', platformId, 'www');
+    }
+    throw new Error('Platform not supported: ' + platformId);
+}
+
+function getDerivedConfigXmlPath(dir, platformId) {
+    if (platformId == 'android') {
+        return path.join(dir, 'platforms', platformId, 'res', 'xml', 'config.xml');
+    } else if (platformId == 'ios') {
+        var base = path.join(dir, 'platforms', platformId);
+        var ret = null;
+        fs.readdirSync(base).forEach(function(a) {
+            if (a != 'www' && a != 'cordova' && a != 'CordovaLib') {
+                var fullPath = path.join(base, a, 'config.xml');
+                if (fs.existsSync(fullPath)) {
+                    ret = fullPath;
+                }
+            }
+        });
+        return ret;
+    }
+    throw new Error('Platform not supported: ' + platformId);
+}
+
+function discoverAppId(dir) {
+    var configXmlPath = path.join(dir, 'config.xml');
+    if (!fs.existsSync(configXmlPath)) {
+        configXmlPath = path.join(dir, 'www', 'config.xml');
+    }
+    if (!fs.existsSync(configXmlPath)) {
+        throw new Error('Not a Cordova project: ' + dir);
+    }
+
+    var configData = fs.readFileSync(configXmlPath, 'utf8');
+    var m = /\bid="(.*?)"/.exec(configData);
+    var appId = m && m[1];
+    if (!appId) {
+        throw new Error('Could not find app ID within: ' + configXmlPath);
+    }
+    return appId;
+}
 
 // Takes a Node-style callback: function(err).
 exports.push = function(target, dir, pretend) {
-  var appId;
-  var appType = 'cordova';
-  var configXmlPath = path.join(dir, 'config.xml');
-  dir = path.join(dir, 'www');
-  if (!fs.existsSync(configXmlPath)) {
-    configXmlPath = path.join(dir, 'config.xml');
-  }
-  if (!fs.existsSync(configXmlPath)) {
-    throw new Error('Not a Cordova project.');
-  }
-  var configData = fs.readFileSync(configXmlPath, { encoding: 'utf-8' });
-  var m = /\bid="(.*?)"/.exec(configData);
-  appId = m && m[1];
-
-  // ToDo - Add ability to bootstrap with a zip file.
-  return doFileSync(target, appId, appType, configXmlPath, dir, pretend);
+    var appId = discoverAppId(dir);
+    var appType = 'cordova';
+
+    return exports.assetmanifest(target, appId)
+    .then(function(result) {
+        var derivedWwwDir = getDerivedWwwDir(dir, result.body['platform']);
+        var derivedConfigXmlPath = getDerivedConfigXmlPath(dir, result.body['platform']);
+        console.log(derivedConfigXmlPath);
+        var cordovaPluginsPath = path.join(derivedWwwDir, 'cordova_plugins.js');
+        if (!fs.existsSync(cordovaPluginsPath)) {
+            throw new Error('Could not find: ' + cordovaPluginsPath);
+        }
+        if (!fs.existsSync(derivedConfigXmlPath)) {
+            throw new Error('Could not find: ' + derivedConfigXmlPath);
+        }
+
+        var existingAssetManifest = result.body['assetManifest'];
+        if (existingAssetManifest) {
+            return doFileSync(target, appId, appType, derivedWwwDir, derivedConfigXmlPath, existingAssetManifest, pretend);
+        }
+        // TODO: It might be faster to use Zip even when some files exist.
+        return doZipPush(target, appId, appType, derivedWwwDir, derivedConfigXmlPath);
+    });
 }
 
 function calculateMd5(fileName) {
-    var BUF_LENGTH = 64*1024,
-        buf = new Buffer(BUF_LENGTH),
-        bytesRead = BUF_LENGTH,
-        pos = 0,
-        fdr = fs.openSync(fileName, 'r');
+    var BUF_LENGTH = 64*1024;
+    var buf = new Buffer(BUF_LENGTH);
+    var bytesRead = BUF_LENGTH;
+    var pos = 0;
+    var fdr = fs.openSync(fileName, 'r');
 
     try {
         var md5sum = crypto.createHash('md5');
@@ -67,280 +123,299 @@ function calculateMd5(fileName) {
     return md5sum.digest('hex');
 }
 
-function buildAssetManifest(configXmlPath, dir) {
-  var fileList = shelljs.find(dir).filter(function(a) {
-    return !fs.statSync(a).isDirectory();
-  });
-
-  var ret = Object.create(null);
-  for (var i = 0; i < fileList.length; ++i) {
-    // TODO: convert windows slash to unix slash here.
-    var appPath = 'www' + fileList[i].slice(dir.length);
-    ret[appPath] = {
-        path: appPath,
-        realPath: fileList[i],
-        etag: calculateMd5(fileList[i]),
-    };
-  }
-  if (configXmlPath && path.dirname(configXmlPath) != dir) {
+function buildAssetManifest(dir, configXmlPath) {
+    var fileList = shelljs.find(dir).filter(function(a) {
+        return !IGNORE_PATH_FOR_PUSH_REGEXP.exec(a) && !fs.statSync(a).isDirectory();
+    });
+
+    var ret = Object.create(null);
+    for (var i = 0; i < fileList.length; ++i) {
+        // TODO: convert windows slash to unix slash here.
+        var appPath = 'www' + fileList[i].slice(dir.length);
+        ret[appPath] = {
+            path: appPath,
+            realPath: fileList[i],
+            etag: calculateMd5(fileList[i]),
+        };
+    }
     ret['config.xml'] = {
         path: 'config.xml',
         realPath: configXmlPath,
         etag: calculateMd5(configXmlPath)
     };
-  }
-  return ret;
+    return ret;
 }
 
 function buildDeleteList(existingAssetManifest, newAssetManifest) {
-  var toDelete = [];
-  for (var k in existingAssetManifest) {
-    // Don't delete top-level files ever.
-    if (k.slice(0, 4) != 'www/') {
-      continue;
-    }
-    if (!newAssetManifest[k]) {
-      toDelete.push(k);
+    var toDelete = [];
+    for (var k in existingAssetManifest) {
+        // Don't delete top-level files ever.
+        if (k.slice(0, 4) != 'www/') {
+            continue;
+        }
+        if (!newAssetManifest[k]) {
+            toDelete.push(k);
+        }
     }
-  }
-  return toDelete;
+    return toDelete;
 }
 
 function buildPushList(existingAssetManifest, newAssetManifest) {
-  var ret = [];
-  for (var k in newAssetManifest) {
-    var entry = newAssetManifest[k];
-    if (entry.etag != existingAssetManifest[k]) {
-      if (entry.path == 'config.xml' || entry.path == 'www/config.xml') {
-        ret.unshift(entry);
-      } else {
-        ret.push(entry);
-      }
+    var ret = [];
+    for (var k in newAssetManifest) {
+        var entry = newAssetManifest[k];
+        if (entry.etag != existingAssetManifest[k]) {
+            if (entry.path == 'config.xml') {
+                ret.unshift(entry);
+            } else {
+                ret.push(entry);
+            }
+        }
     }
-  }
-  return ret;
+    return ret;
 }
 
 function calculatePushBytes(pushList) {
-  var ret = 0;
-  for (var i = 0; i < pushList.length; ++i) {
-    ret += fs.statSync(pushList[i].realPath).size;
-  }
-  return ret;
+    var ret = 0;
+    for (var i = 0; i < pushList.length; ++i) {
+        ret += fs.statSync(pushList[i].realPath).size;
+    }
+    return ret;
 }
 
-function doFileSync(target, appId, appType, configXmlPath, dir, pretend) {
-  return exports.assetmanifest(target, appId)
-  .then(function(result) {
-    var existingAssetManifest = result.body['assetManifest'] || {};
-    var newAssetManifest = buildAssetManifest(configXmlPath, dir);
+function doFileSync(target, appId, appType, dir, configXmlPath, existingAssetManifest, pretend) {
+    var newAssetManifest = buildAssetManifest(dir, configXmlPath);
     var deleteList = buildDeleteList(existingAssetManifest, newAssetManifest);
     var pushList = buildPushList(existingAssetManifest, newAssetManifest);
     var totalPushBytes = calculatePushBytes(pushList);
     if (pretend) {
-      console.log('AppId=' + appId);
-      console.log('Would delete: ' + JSON.stringify(deleteList));
-      console.log('Would upload: ' + JSON.stringify(pushList));
-      console.log('Upload bytes: ' + totalPushBytes);
+        console.log('AppId=' + appId);
+        console.log('Would delete: ' + JSON.stringify(deleteList));
+        console.log('Would upload: ' + JSON.stringify(pushList));
+        console.log('Upload bytes: ' + totalPushBytes);
     } else if (deleteList.length === 0 && pushList.length === 0) {
-      console.log('Application already up-to-date.');
+        console.log('Application already up-to-date.');
     } else {
-      return doRequest('POST', target, 'prepupdate', { appId: appId, appType: appType, json: {'transferSize': totalPushBytes}})
-      .then(function() {
-        if (deleteList.length > 0) {
-          return doRequest('POST', target, 'deletefiles', { appId: appId, appType: appType, json: {'paths': deleteList}})
-        }
-      })
-      .then(function pushNextFile() {
-        if (pushList.length === 0) {
-          console.log('Push complete.');
-          return;
-        }
-        var curPushEntry = pushList.shift();
-        return doRequest('PUT', target, '/putfile', {
-          appId: appId,
-          appType: appType,
-          body: fs.readFileSync(curPushEntry.realPath),
-          query: {
-            path: curPushEntry.path,
-            etag: curPushEntry.etag
-          }
-        }).then(pushNextFile);
-      });
+        return doRequest('POST', target, 'prepupdate', { appId: appId, appType: appType, json: {'transferSize': totalPushBytes}})
+        .then(function() {
+            if (deleteList.length > 0) {
+                return doRequest('POST', target, 'deletefiles', { appId: appId, appType: appType, json: {'paths': deleteList}})
+            }
+        })
+        .then(function pushNextFile() {
+            if (pushList.length === 0) {
+                console.log('Push complete.');
+                return;
+            }
+            var curPushEntry = pushList.shift();
+            return doRequest('PUT', target, '/putfile', {
+                appId: appId,
+                appType: appType,
+                body: fs.readFileSync(curPushEntry.realPath),
+                query: {
+                    path: curPushEntry.path,
+                    etag: curPushEntry.etag
+                }
+            }).then(pushNextFile);
+        });
     }
-  });
+}
+
+function zipDir(dir, zip) {
+    var contents = fs.readdirSync(dir);
+    contents.forEach(function(f) {
+        var fullPath = path.join(dir, f);
+        if (!IGNORE_PATH_FOR_PUSH_REGEXP.exec(fullPath)) {
+            if (fs.statSync(fullPath).isDirectory()) {
+                var inner = zip.folder(f);
+                zipDir(path.join(dir, f), inner);
+            } else {
+                zip.file(f, fs.readFileSync(fullPath, 'binary'), { binary: true });
+            }
+        }
+    });
+}
+
+function doZipPush(target, appId, appType, dir, configXmlPath) {
+    var zip = new JSZip();
+    zipDir(dir, zip.folder('www'));
+    zip.file('config.xml', fs.readFileSync(configXmlPath, 'binary'), { binary: true });
+    var newAssetManifest = buildAssetManifest(dir, configXmlPath);
+    zip.file('zipassetmanifest.json', JSON.stringify(newAssetManifest));
+    var zipData = new Buffer(zip.generate({ type: 'base64' }), 'base64');
+    return doRequest('POST', target, '/zippush', {appId:appId, appType: appType, body: zipData});
 }
 
 function doRequest(method, target, action, options) {
-  var ret = Q.defer();
-  var targetParts = target.split(':');
-  var host = targetParts[0];
-  var port = +(targetParts[1] || 2424);
-  options = options || {};
-
-  var queryParams = {};
-  if (options.query) {
-    Object.keys(options.query).forEach(function(k) {
-      queryParams[k] = options.query[k];
+    var ret = Q.defer();
+    var targetParts = target.split(':');
+    var host = targetParts[0];
+    var port = +(targetParts[1] || 2424);
+    options = options || {};
+
+    var queryParams = {};
+    if (options.query) {
+        Object.keys(options.query).forEach(function(k) {
+            queryParams[k] = options.query[k];
+        });
+    }
+    if (options.appId) {
+        queryParams['appId'] = options.appId;
+    }
+    if (options.appType) {
+        queryParams['appType'] = options.appType;
+    }
+
+    // Send the HTTP request. crxContents is a Node Buffer, which is the payload.
+    // Prepare the form data for upload.
+    var uri = url.format({
+        protocol: 'http',
+        hostname: host,
+        port: port,
+        pathname: action,
+        query: queryParams
     });
-  }
-  if (options.appId) {
-    queryParams['appId'] = options.appId;
-  }
-  if (options.appType) {
-    queryParams['appType'] = options.appType;
-  }
-
-  // Send the HTTP request. crxContents is a Node Buffer, which is the payload.
-  // Prepare the form data for upload.
-  var uri = url.format({
-    protocol: 'http',
-    hostname: host,
-    port: port,
-    pathname: action,
-    query: queryParams
-  });
-
-  process.stdout.write(method + ' ' + uri);
-
-  var headers = {};
-  if (options.json) {
-    options.body = JSON.stringify(options.json);
-    headers['Content-Type'] = 'application/json';
-  }
-
-  function statusCheck(err, res, body) {
-    if (err) {
-      err.statusCode = res && res.statusCode;
-    } else if (res) {
-      process.stdout.write(' ==> ' + (res && res.statusCode) + '\n');
-      if (res.statusCode != 200) {
-        err = new Error('Server returned status code: ' + res.statusCode);
-      } else if (options.expectJson) {
-        try {
-          body = JSON.parse(body);
-        } catch (e) {
-          err = new Error('Invalid JSON: ' + body.slice(500));
+
+    process.stdout.write(method + ' ' + uri);
+
+    var headers = {};
+    if (options.json) {
+        options.body = JSON.stringify(options.json);
+        headers['Content-Type'] = 'application/json';
+    }
+
+    function statusCheck(err, res, body) {
+        if (err) {
+            err.statusCode = res && res.statusCode;
+        } else if (res) {
+            process.stdout.write(' ==> ' + (res && res.statusCode) + '\n');
+            if (res.statusCode != 200) {
+                err = new Error('Server returned status code: ' + res.statusCode);
+            } else if (options.expectJson) {
+                try {
+                    body = JSON.parse(body);
+                } catch (e) {
+                    err = new Error('Invalid JSON: ' + body.slice(500));
+                }
+            }
+        }
+        if (err) {
+            ret.reject(err);
+        } else {
+            ret.resolve({res:res, body:body});
         }
-      }
     }
-    if (err) {
-      ret.reject(err);
-    } else {
-      ret.resolve({res:res, body:body});
+    var req = request({
+        uri: uri,
+        headers: headers,
+        method: method,
+        body: options.body
+    }, statusCheck);
+
+    if (options.form) {
+        var f = options.form;
+        req.form().append(f.key, f.formBody, { filename: f.filename, contentType: f.contentType });
     }
-  }
-  var req = request({
-    uri: uri,
-    headers: headers,
-    method: method,
-    body: options.body
-  }, statusCheck);
-
-  if (options.form) {
-    var f = options.form;
-    req.form().append(f.key, f.formBody, { filename: f.filename, contentType: f.contentType });
-  }
-  return ret.promise;
+    return ret.promise;
 };
 
 exports.info = function(target) {
-  return doRequest('GET', target, '/info', { expectJson: true });
+    return doRequest('GET', target, '/info', { expectJson: true });
 };
 
 exports.assetmanifest = function(target, appId) {
-  return doRequest('GET', target, '/assetmanifest', {expectJson: true, appId: appId});
+    return doRequest('GET', target, '/assetmanifest', {expectJson: true, appId: appId});
 };
 
 exports.menu = function(target) {
-  return doRequest('POST', target, '/menu');
+    return doRequest('POST', target, '/menu');
 };
 
 exports.eval = function(target, someJs) {
-  return doRequest('POST', target, '/exec', { query: {code: someJs} });
+    return doRequest('POST', target, '/exec', { query: {code: someJs} });
 };
 
 exports.launch = function(target, appId) {
-  return doRequest('POST', target, '/launch', { appId: appId});
+    return doRequest('POST', target, '/launch', { appId: appId});
 };
 
 exports.deleteAllApps = function(target) {
-  return doRequest('POST', target, '/deleteapp', { query: {'all': 1} });
+    return doRequest('POST', target, '/deleteapp', { query: {'all': 1} });
 };
 
 exports.deleteApp = function(target, appId) {
-  return doRequest('POST', target, '/deleteApp', { appId: appId});
+    return doRequest('POST', target, '/deleteApp', { appId: appId});
 };
 
 function parseArgs(argv) {
     var opts = {
-      'help': Boolean,
-      'target': String
+        'help': Boolean,
+        'target': String
     };
     var ret = nopt(opts, null, argv);
     if (!ret.target) {
-      ret.target = 'localhost:2424';
+        ret.target = 'localhost:2424';
     }
     return ret;
 }
 
 function usage() {
-  console.log('Usage: harness-push push path/to/chrome_app --target=IP_ADDRESS:PORT');
-  console.log('Usage: harness-push menu');
-  console.log('Usage: harness-push eval "alert(1)"');
-  console.log('Usage: harness-push info');
-  console.log('Usage: harness-push launch [appId]');
-  console.log();
-  console.log('--target defaults to localhost:2424');
-  console.log('To deploy to Android over USB, use: adb forward tcp:2424 tcp:2424');
-  process.exit(1);
+    console.log('Usage: harness-push push path/to/chrome_app --target=IP_ADDRESS:PORT');
+    console.log('Usage: harness-push menu');
+    console.log('Usage: harness-push eval "alert(1)"');
+    console.log('Usage: harness-push info');
+    console.log('Usage: harness-push launch [appId]');
+    console.log();
+    console.log('--target defaults to localhost:2424');
+    console.log('To deploy to Android over USB, use: adb forward tcp:2424 tcp:2424');
+    process.exit(1);
 }
 
 function main() {
-  var args = parseArgs(process.argv);
-
-  function onFailure(err) {
-    console.error(err);
-  }
-  function onSuccess(result) {
-    if (typeof result.body == 'object') {
-      console.log(JSON.stringify(result.body, null, 4));
-    } else if (result.body) {
-      console.log(result.body);
-    }
-  }
+    var args = parseArgs(process.argv);
 
-  var cmd = args.argv.remain[0];
-  if (cmd == 'push') {
-    if (!args.argv.remain[1]) {
-      usage();
+    function onFailure(err) {
+        console.error(err);
     }
-    exports.push(args.target, args.argv.remain[1], args.pretend).then(onSuccess, onFailure);
-  } else if (cmd == 'deleteall') {
-    exports.deleteAllApps(args.target);
-  } else if (cmd == 'delete') {
-    if (!args.argv.remain[1]) {
-      usage();
+    function onSuccess(result) {
+        if (typeof result.body == 'object') {
+            console.log(JSON.stringify(result.body, null, 4));
+        } else if (result.body) {
+            console.log(result.body);
+        }
     }
-    exports.deleteApp(args.target).then(onSuccess, onFailure);
-  } else if (cmd == 'menu') {
-    exports.menu(args.target).then(onSuccess, onFailure);
-  } else if (cmd == 'eval') {
-    if (!args.argv.remain[1]) {
-      usage();
+
+    var cmd = args.argv.remain[0];
+    if (cmd == 'push') {
+        if (!args.argv.remain[1]) {
+            usage();
+        }
+        exports.push(args.target, args.argv.remain[1], args.pretend).then(onSuccess, onFailure);
+    } else if (cmd == 'deleteall') {
+        exports.deleteAllApps(args.target);
+    } else if (cmd == 'delete') {
+        if (!args.argv.remain[1]) {
+            usage();
+        }
+        exports.deleteApp(args.target).then(onSuccess, onFailure);
+    } else if (cmd == 'menu') {
+        exports.menu(args.target).then(onSuccess, onFailure);
+    } else if (cmd == 'eval') {
+        if (!args.argv.remain[1]) {
+            usage();
+        }
+        exports.eval(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+    } else if (cmd == 'assetmanifest') {
+        exports.assetmanifest(args.target, args.appid).then(onSuccess, onFailure);
+    } else if (cmd == 'info') {
+        exports.info(args.target).then(onSuccess, onFailure);
+    } else if (cmd == 'launch') {
+        exports.launch(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+    } else {
+        usage();
     }
-    exports.eval(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
-  } else if (cmd == 'assetmanifest') {
-    exports.assetmanifest(args.target, args.appid).then(onSuccess, onFailure);
-  } else if (cmd == 'info') {
-    exports.info(args.target).then(onSuccess, onFailure);
-  } else if (cmd == 'launch') {
-    exports.launch(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
-  } else {
-    usage();
-  }
 }
 
 if (require.main === module) {
-  main();
+    main();
 }

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/4a366039/harness-push/package.json
----------------------------------------------------------------------
diff --git a/harness-push/package.json b/harness-push/package.json
index 4357bd4..e58b848 100644
--- a/harness-push/package.json
+++ b/harness-push/package.json
@@ -24,9 +24,10 @@
     "harness-push": "./harness-push.js"
   },
   "dependencies": {
-    "request": "~2.34",
+    "jszip": "~2.1",
     "nopt": "~2.2",
-    "shelljs": "0.1.x",
-    "q": "~0.9"
+    "q": "~0.9",
+    "request": "~2.34",
+    "shelljs": "0.1.x"
   }
 }

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/4a366039/www/cdvah/js/HarnessServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HarnessServer.js b/www/cdvah/js/HarnessServer.js
index a596ab4..51a5c59 100644
--- a/www/cdvah/js/HarnessServer.js
+++ b/www/cdvah/js/HarnessServer.js
@@ -53,6 +53,7 @@
 
     myApp.factory('HarnessServer', ['$q', 'HttpServer', 'ResourcesLoader', 'AppHarnessUI', 'AppsService', 'notifier', function($q, HttpServer, ResourcesLoader, AppHarnessUI, AppsService, notifier) {
 
+        var PROTOCOL_VER = 2;
         var server = null;
         var listenAddress = null;
 
@@ -130,7 +131,10 @@
                 return null;
             }).then(function(assetManifest) {
                 resp.sendJsonResponse({
-                    'assetManifest': assetManifest
+                    'assetManifest': assetManifest,
+                    'platform': cordova.platformId,
+                    'cordovaVer': cordova.version,
+                    'protocolVer': PROTOCOL_VER,
                 });
             });
         }
@@ -198,35 +202,10 @@
                 var tmpUrl = ResourcesLoader.createTmpFileUrl();
                 return pipeRequestToFile(req, tmpUrl)
                 .then(function() {
-                    var ret = $q.when();
-                    if (path == 'www/cordova_plugins.js') {
-                        path = 'orig-cordova_plugins.js';
-                    }
-                    if (path == 'www/config.xml') {
-                        ret = ret.then(function() {
-                          return ResourcesLoader.downloadFromUrl(tmpUrl, tmpUrl + '-2');
-                        });
-                    }
-                    ret = ret.then(function() {
-                        return app.directoryManager.addFile(tmpUrl, path, etag);
-                    });
-                    if (path == 'www/config.xml') {
-                        ret = ret.then(function() {
-                            return app.directoryManager.addFile(tmpUrl + '-2', 'config.xml', etag);
-                        });
-                    }
-                    if (path == 'config.xml' || path == 'www/config.xml') {
-                        ret = ret.then(function() {
-                            return app.readConfigXml();
-                        });
-                    } else if (path == 'orig-cordova_plugins.js') {
-                        ret = ret.then(function() {
-                            return app.readCordovaPluginsFile();
-                        });
-                    }
-                    return ret;
+                    return importFile(tmpUrl, path, app, etag);
                 })
                 .then(function() {
+                    // TODO: Add a timeout that resets updatingStatus if no more requests come in.
                     app.updateBytesSoFar += +req.headers['content-length'];
                     app.updatingStatus = app.updateBytesTotal / app.updateBytesSoFar;
                     if (app.updatingStatus === 1) {
@@ -239,11 +218,71 @@
             });
         }
 
+        function importFile(fileUrl, destPath, app, etag) {
+            console.log('Adding file: ' + destPath);
+            var ret = $q.when();
+            if (destPath == 'www/cordova_plugins.js') {
+                destPath = 'orig-cordova_plugins.js';
+            }
+            ret = ret.then(function() {
+                return app.directoryManager.addFile(fileUrl, destPath, etag);
+            });
+            if (destPath == 'config.xml') {
+                ret = ret.then(function() {
+                    return app.readConfigXml();
+                });
+            } else if (destPath == 'orig-cordova_plugins.js') {
+                ret = ret.then(function() {
+                    return app.readCordovaPluginsFile();
+                });
+            }
+            return ret;
+        }
+
+        function handleZipPush(req, resp) {
+            var appId = req.getQueryParam('appId');
+            var appType = req.getQueryParam('appType') || 'cordova';
+            return AppsService.getAppById(appId, appType)
+            .then(function(app) {
+                var tmpZipUrl = ResourcesLoader.createTmpFileUrl();
+                var tmpDirUrl = ResourcesLoader.createTmpFileUrl() + '/';
+                return pipeRequestToFile(req, tmpZipUrl)
+                .then(function() {
+                    console.log('Extracting update zip');
+                    return ResourcesLoader.extractZipFile(tmpZipUrl, tmpDirUrl);
+                })
+                .then(function() {
+                    return ResourcesLoader.readJSONFileContents(tmpDirUrl + 'zipassetmanifest.json');
+                })
+                .then(function(zipAssetManifest) {
+                    var keys = Object.keys(zipAssetManifest);
+                    return $q.when()
+                    .then(function next() {
+                        var k = keys.shift();
+                        if (k) {
+                            return importFile(tmpDirUrl + k, k, app, zipAssetManifest[k]['etag'])
+                            .then(next);
+                        }
+                    });
+                })
+                .then(function() {
+                    app.lastUpdated = new Date();
+                    notifier.success('Update complete.');
+                    return resp.sendTextResponse(200, '');
+                })
+                .finally(function() {
+                    app.updatingStatus = null;
+                    ResourcesLoader.delete(tmpZipUrl);
+                    ResourcesLoader.delete(tmpDirUrl);
+                });
+            });
+        }
+
         function handleInfo(req, resp) {
             var json = {
                 'platform': cordova.platformId,
                 'cordovaVer': cordova.version,
-                'protocolVer': 2,
+                'protocolVer': PROTOCOL_VER,
                 'userAgent': navigator.userAgent,
                 'appList': AppsService.getAppListAsJson()
             };
@@ -262,8 +301,9 @@
                 .addRoute('/assetmanifest', ensureMethodDecorator('GET', handleAssetManifest))
                 .addRoute('/prepupdate', ensureMethodDecorator('POST', handlePrepUpdate))
                 .addRoute('/deletefiles', ensureMethodDecorator('POST', handleDeleteFiles))
-                .addRoute('/deleteapp', ensureMethodDecorator('POST', handleDeleteApp))
-                .addRoute('/putfile', ensureMethodDecorator('PUT', handlePutFile));
+                .addRoute('/putfile', ensureMethodDecorator('PUT', handlePutFile))
+                .addRoute('/zippush', ensureMethodDecorator('POST', handleZipPush))
+                .addRoute('/deleteapp', ensureMethodDecorator('POST', handleDeleteApp));
             return server.start();
         }