├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── lib └── updater.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | test/ 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sqwiggle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = 'dot' 2 | 3 | test: 4 | @NODE_ENV=test ./node_modules/.bin/mocha \ 5 | --reporter $(REPORTER) \ 6 | $(TESTS) 7 | 8 | test-w: 9 | @NODE_ENV=test ./node_modules/.bin/mocha \ 10 | --require should \ 11 | --reporter $(REPORTER) \ 12 | --watch 13 | $(TESTS) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Webkit Updater 2 | 3 | This package will update a deployed Mac application by downloading a dmg from a specific location, mounting it, copying the contents over the original (keeping code signatures) and cleaning up after itself. 4 | 5 | ## Installation 6 | 7 | You'll want to install the latest stable package from NPM: 8 | 9 | ```js 10 | npm install node-webkit-mac-updater 11 | ``` 12 | 13 | ## Usage 14 | 15 | It's upto your application to know whether an update is needed and where to find it. You can do this by periodically hitting an API endpoint under your control. Once you know an update is needed then simply let the updater know where to find the dmg. 16 | 17 | This gives you the opportunity to ask the user if they wish to update or force an update in the background. 18 | 19 | ```js 20 | var Updater = require('node-webkit-mac-updater'); 21 | 22 | var updater = new Updater({ 23 | dmg_name: 'MyApp Installer', 24 | app_name: 'MyApp', 25 | source: { 26 | host: 's3.amazonaws.com', 27 | port: 80, 28 | path: '/myapp-releases/mac/app-0.2.dmg' 29 | } 30 | }); 31 | 32 | updater.update(function(err){ 33 | if (!err) console.log('App has been updated!'); 34 | }); 35 | 36 | ``` 37 | 38 | 39 | ## DMG Format 40 | 41 | The DMG must be built so that MyApp.app is in the root of the folder structure. You may have other files in the archive but only MyApp.app will be copied. This tool works great for creating DMGs programatically: 42 | 43 | https://github.com/sqwiggle/yoursway-create-dmg 44 | 45 | 46 | ## Future Development 47 | 48 | In the future we will be developing this into a cross-platform updater, covering the quirks and formats required for each individual OS that node-webkit supports. 49 | -------------------------------------------------------------------------------- /lib/updater.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var _ = require('underscore'); 4 | var exec = require('child_process').exec; 5 | var execPath = process.execPath; 6 | var filePath = execPath.substr(0, execPath.lastIndexOf("\\")); 7 | var appPath = path.normalize(execPath + "/../../../../../../.."); 8 | var escapeshell = function(cmd) { 9 | return '"'+cmd.replace(/(["'$`\\])/g,'\\$1')+'"'; 10 | }; 11 | 12 | module.exports = Updater; 13 | 14 | function Updater(config) { 15 | this.config = _.extend(this.config, config || {}); 16 | } 17 | 18 | Updater.prototype = { 19 | 20 | // public 21 | config: { 22 | dmg_name: 'Myapp Installer', 23 | app_name: 'Myapp', 24 | source: { 25 | host: 's3.amazonaws.com', 26 | port: 443, 27 | path: '/myapp-releases/mac/myapp.dmg' 28 | }, 29 | progress: function(percentage) { 30 | console.log(percentage + "%"); 31 | } 32 | }, 33 | 34 | update: function(callback) { 35 | console.log('downloading ' + this.config.source.path); 36 | 37 | var tempName = '.nw-update.dmg'; 38 | var location = appPath+ "/" +tempName; 39 | var self = this; 40 | 41 | try { 42 | this.download(this.config.source, location, function(){ 43 | console.log('downloaded'); 44 | 45 | self.mount(location, self.config.dmg_name, function(mount_point){ 46 | console.log('update mounted at ' + mount_point); 47 | 48 | self.hideOriginal(self.config.app_name, function(err){ 49 | if (err) throw err; 50 | console.log('original application hidden'); 51 | 52 | self.copyUpdate(self.config.app_name, mount_point, appPath, function(err, app){ 53 | if (err) throw err; 54 | console.log('update applied successfully at ', app); 55 | 56 | self.removeQuarantine(app, function(err){ 57 | if (err) throw err; 58 | console.log('quarantine removed, cleaning up'); 59 | }); 60 | 61 | // if either of these fails we're still going to call it a (messy) success 62 | self.cleanup(location); 63 | self.unmount(mount_point, function(){ 64 | 65 | console.log('update complete'); 66 | callback(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | }, this.config.progress); 72 | } catch (err) { 73 | 74 | // in the event of an error, cleanup what we can 75 | this.cleanup(location); 76 | callback(err); 77 | } 78 | }, 79 | 80 | 81 | // private 82 | download: function(options, location, callback, progress) { 83 | 84 | var http = require('http'); 85 | var request = http.get(options, function(res){ 86 | res.setEncoding('binary'); 87 | 88 | var data = ''; 89 | var rln=0,percent=0,ln=res.headers['content-length']; 90 | 91 | res.on('data', function(chunk){ 92 | rln += chunk.length; 93 | data += chunk; 94 | 95 | var p = Math.round((rln/ln)*100); 96 | if (p > percent) { 97 | percent = p; 98 | progress(p); 99 | } 100 | }); 101 | 102 | res.on('end', function(){ 103 | fs.writeFile(location, data, 'binary', callback); 104 | }); 105 | }); 106 | }, 107 | 108 | mount: function(dmg, dmg_name, callback) { 109 | var self = this; 110 | 111 | exec('hdiutil attach ' + escapeshell(dmg) + ' -nobrowse', function(err){ 112 | if (err) throw err; 113 | console.log('mounted volume'); 114 | 115 | self.findMountPoint(dmg_name, callback); 116 | }); 117 | }, 118 | 119 | unmount: function(mount_point, callback) { 120 | exec('hdiutil detach ' + escapeshell(mount_point), callback); 121 | }, 122 | 123 | findMountPoint: function(dmg_name, callback) { 124 | exec('hdiutil info', function(err, stdout){ 125 | if (err) throw err; 126 | 127 | var results = stdout.split("\n"); 128 | 129 | for (var i=0,l=results.length;i= 1.17.1", 39 | "should": ">= 3.1.3" 40 | }, 41 | "main": "./lib/updater.js", 42 | "readmeFilename": "README.md", 43 | "bugs": { 44 | "url": "https://github.com/sqwiggle/node-webkit-mac-updater/issues" 45 | } 46 | } 47 | --------------------------------------------------------------------------------