├── .gitignore ├── server-load-balancer ├── .gitignore ├── package.json └── app.js ├── package.json ├── LICENSE ├── README.md └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | build-projects 2 | .env 3 | node_modules -------------------------------------------------------------------------------- /server-load-balancer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /server-load-balancer/package.json: -------------------------------------------------------------------------------- 1 | B{ 2 | "name": "ringo-load-balancer", 3 | "version": "1.0.1", 4 | "description": "Decides which Mac server to route the user to.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "Ringo" 11 | ], 12 | "author": "Gautam Mittal", 13 | "license": "MIT", 14 | "dependencies": { 15 | "body-parser": "^1.13.2", 16 | "colors": "^1.1.2", 17 | "dotenv": "^1.2.0", 18 | "express": "^4.13.1", 19 | "jetty": "^0.2.1", 20 | "prettyjson": "^1.1.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ringo-server", 3 | "version": "1.0.0", 4 | "description": "A REST API on top of Xcode.", 5 | "main": "app.js", 6 | "dependencies": { 7 | "body-parser": "^1.13.2", 8 | "colors": "^1.1.2", 9 | "dotenv": "^1.2.0", 10 | "express": "^4.13.1", 11 | "external-ip": "^0.2.3", 12 | "keen.io": "^0.1.3", 13 | "request": "^2.60.0", 14 | "satelize": "^0.1.2", 15 | "sendgrid": "^1.9.2", 16 | "serial-number": "^1.0.0", 17 | "shelljs": "^0.5.1" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://www.github.com/gmittal/ringo.git" 26 | }, 27 | "keywords": [ 28 | "Xcode", 29 | "Apple", 30 | "Ringo", 31 | "Japanese", 32 | "Browser", 33 | "iOS" 34 | ], 35 | "author": "Gautam Mittal", 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Gautam Mittal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` _ 2 | _____(_)___ ____ _____ 3 | / ___/ / __ \/ __ `/ __ \ 4 | / / / / / / / /_/ / /_/ / 5 | /_/ /_/_/ /_/\__, /\____/ 6 | /____/ 7 | ``` 8 | # Ringo 9 | 10 | Harness the power of Xcode using a simple, extensible, hackable API. 11 | 12 | ### What is it? 13 | Ringo is a simple web server written in [Node.js](https://nodejs.org) that can allow you to harness the power of Xcode by using a set of simple API endpoints. You can run the core build server on your own Mac hardware, and from there start writing applications that take advantage of Xcode. I built Ringo for the purpose of having the ability to build and run iOS applications from any web browser (or any platform that has an internet connection for that matter). 14 | 15 | Currently, the Ringo core build server requires Mac hardware in order to run. However, Swift is said to go open-source later this year, and this hopefully will open some opportunities to get rid of the necessity of a Mac. 16 | 17 | ### Installation 18 | Firstly, you're going to need to have Xcode installed on your Mac. Once you have that installed you're going to need the Xcode command line tools, which allow Ringo to interface with Xcode easily. Type the following into your command line to install them: 19 | 20 | ```$ xcode-select --install``` 21 | 22 | Clone the repository and navigate to your local copy of the build server: 23 | 24 | ``` $ git clone https://www.github.com/gmittal/ringo.git && cd ringo ``` 25 | 26 | Install the various dependencies: 27 | 28 | ``` $ npm install && brew install wget && gem install nomad-cli``` 29 | 30 | Install [ngrok](http://ngrok.com) and run the following: 31 | ``` $ ngrok http 3000 ``` 32 | 33 | You're also going to need to populate your environment variables (stored in a .env file) with some important information. For now, Ringo uses [Appetize](http://www.appetize.io) to run your iOS apps in an in-browser simulator. That does require an API key, but they are free and easy to get ahold of. 34 | ``` 35 | APPETIZE_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 36 | HOSTNAME=http://YOUR_NGROK.ngrok.io 37 | SECURE_HOSTNAME=https://YOUR_NGROK.ngrok.io 38 | ``` 39 | 40 | Once all of that is complete, it should be fairly easy to get the server running: 41 | 42 | ``` $ node app.js ``` 43 | 44 | The server should spit out an ngrok localhost tunnel which should allow you to access the server externally, allowing you to get up and running with hacking it. 45 | 46 | 47 | #### Ready to start hacking with Ringo? Read the [docs](https://www.github.com/gmittal/ringo/wiki/Documentation). 48 | 49 | 50 | 51 | 52 | 53 | ### License 54 | 55 | ##### [TL;DR](https://tldrlegal.com/license/mit-license) 56 | 57 | The MIT License (MIT) 58 | 59 | Copyright (c) 2015 Gautam Mittal 60 | 61 | Permission is hereby granted, free of charge, to any person obtaining a copy 62 | of this software and associated documentation files (the "Software"), to deal 63 | in the Software without restriction, including without limitation the rights 64 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 65 | copies of the Software, and to permit persons to whom the Software is 66 | furnished to do so, subject to the following conditions: 67 | 68 | The above copyright notice and this permission notice shall be included in all 69 | copies or substantial portions of the Software. 70 | 71 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 72 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 73 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 74 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 75 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 76 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 77 | SOFTWARE. 78 | -------------------------------------------------------------------------------- /server-load-balancer/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Gautam Mittal under MIT License 2 | 3 | var dotenv = require('dotenv'); 4 | dotenv.load(); 5 | 6 | var express = require('express'); 7 | var app = express(); 8 | var bodyParser = require('body-parser'); 9 | var colors = require('colors'); 10 | var Jetty = require('jetty'); 11 | var jetty = new Jetty(process.stdout); 12 | var prettyjson = require('prettyjson'); 13 | 14 | jetty.clear(); // clear the console 15 | 16 | var port = 3001; 17 | 18 | app.use(bodyParser()); 19 | app.use(function(req, res, next) { 20 | res.header("Access-Control-Allow-Origin", "*"); 21 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 22 | next(); 23 | }); 24 | 25 | var servers = {}; 26 | var ids = []; 27 | var loadStats = []; 28 | 29 | // Endpoint to serve the user so that it 30 | app.get('/get-server-url', function (req, res) { 31 | if (ids.length > 0) { // at least one server has to have been registered 32 | var relaxedServer = loadStats[0]; 33 | for (var id in servers) { 34 | if (servers[id].load == relaxedServer) { 35 | console.log('Server #['+id+'] with url['+servers[id].accessURL+']'); 36 | res.send(servers[id].accessURL); 37 | } 38 | } 39 | 40 | } else { 41 | res.send("No servers have been registered."); 42 | } 43 | 44 | }); 45 | 46 | 47 | // Register the server with the load balancer 48 | app.post('/register-server', function (req, res) { 49 | if (req.body.key == process.env.BALANCER_AUTH_KEY) { // for security reasons, we have a secret key which prevents random people from registering their own servers with the production load balancer 50 | if (req.body.tunnel) { 51 | if (req.body.server_id) { 52 | if (req.body.load) { 53 | servers[req.body.server_id] = { 54 | 'accessURL': req.body.tunnel, 55 | 'load': req.body.load 56 | }; 57 | 58 | if (ids.indexOf(req.body.server_id) == -1) { // if it doesn't already exist 59 | ids.push(req.body.server_id); 60 | } 61 | 62 | loadStats = []; 63 | for (var loadVal in servers) { 64 | var tmpLoadVal = servers[loadVal].load; 65 | loadStats.push(tmpLoadVal); 66 | } 67 | 68 | loadStats.sort(function(a,b){return a-b}); 69 | 70 | 71 | var prettyPrintOptions = { 72 | keysColor: 'magenta', 73 | dashColor: 'cyan', 74 | stringColor: 'green', 75 | numberColor: 'blue' 76 | }; 77 | 78 | jetty.clear(); 79 | jetty.moveTo([0,0]); 80 | jetty.text("Ringo Server Load Balancer\n\n".bold.underline.white + "SERVER IDs: ".bold.white + JSON.stringify(ids).cyan + "\n" + "SERVER LOAD: ".bold.white + JSON.stringify(loadStats).green+"\n\n" + "FULL SERVER STATS: ".white.bold + "\n" + prettyjson.render(servers, prettyPrintOptions).blue); 81 | 82 | 83 | 84 | res.send("Successfully registered server in the load balancer."); 85 | } else { 86 | res.send(500, "There was an error registering the server with the load balancer. Missing load parameter."); 87 | } // end if req.body.load 88 | 89 | } else { 90 | res.send(500, "There was an error registering the server with the load balancer. Missing server_id parameter."); 91 | } 92 | 93 | } else { 94 | res.send(500, "There was an error registering the server with the load balancer. Missing tunnel parameter."); 95 | } 96 | } else { 97 | res.send(500, "Error. Unauthorized request."); 98 | }// end auth 99 | }); 100 | 101 | 102 | 103 | // When a server dies, the load balancer should know 104 | app.post('/unregister-server', function (req, res) { 105 | if (req.body.key == process.env.BALANCER_AUTH_KEY) { // more security 106 | if (req.body.server_id) { 107 | var idIndex = ids.indexOf(req.body.server_id); 108 | if (idIndex !== -1) { // make sure it exists 109 | ids.splice(idIndex, 1); // delete it from the server id array 110 | 111 | // delete the load value from the loadStats array 112 | for (var i = 0; i < loadStats.length; i++) { 113 | if (loadStats[i] == servers[req.body.server_id].load) { 114 | loadStats.splice(i, 1); 115 | } 116 | } 117 | 118 | delete servers[req.body.server_id]; // delete from the json object 119 | 120 | console.log(servers); 121 | console.log(ids); 122 | 123 | res.send(200, "Server successfully removed from load balancer registry."); 124 | 125 | } else { 126 | res.send(500, "You cannot unregister servers that have not already been registered."); 127 | } 128 | } else { 129 | res.send(500, "Invalid parameters. Error unregistering server with load balancer."); 130 | } 131 | } 132 | }); 133 | 134 | 135 | var server = app.listen(port, function () { 136 | var host = server.address().address; 137 | var port = server.address().port; 138 | 139 | //console.log(('Ringo load balancer listening at http://0.0.0.0:'+ port)); 140 | }); 141 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Ringo Core Build Server 2 | // Copyright 2015 Gautam Mittal under MIT License 3 | 4 | // Dependencies: NODE.JS + Xcode 5 | // You will also need to populate the .env file with the necessary environment variables in order for this script to run effectively 6 | 7 | var dotenv = require('dotenv'); 8 | dotenv.load(); 9 | var bodyParser = require('body-parser'); 10 | var colors = require('colors'); 11 | var express = require('express'); 12 | var fs = require('fs'); 13 | var getIP = require('external-ip')(); 14 | var Keen = require("keen.io"); 15 | var request = require('request'); 16 | var satelize = require('satelize'); 17 | var serialNumber = require('serial-number'); 18 | serialNumber.preferUUID = true; 19 | require('shelljs/global'); 20 | 21 | 22 | var sendgrid; 23 | if (process.env.REPORT_TO && process.env.SEND_REPORTS == "YES") { 24 | sendgrid = require('sendgrid')(process.env.SENDGRID_KEY); 25 | } 26 | var client; 27 | if (process.env.KEEN_PROJECT_ID) { 28 | console.log("Keen analytics starting up...".magenta); 29 | client = Keen.configure({ 30 | projectId: process.env.KEEN_PROJECT_ID, 31 | writeKey: process.env.KEEN_WRITE_KEY 32 | }); 33 | } 34 | 35 | 36 | var reportBalancerTimer; 37 | var exec = require('child_process').exec; 38 | //var ngrok = require('ngrok'); 39 | 40 | var app = express(); 41 | app.use(bodyParser.json({limit: '50mb', extended: true})); 42 | app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); 43 | app.use(express.static(__dirname + '/build-projects')); // serve the files within build-projects 44 | app.use(function(req, res, next) { 45 | res.header("Access-Control-Allow-Origin", "*"); 46 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 47 | next(); 48 | }); 49 | 50 | var port = 3000; 51 | 52 | var server = app.listen(port, function () { 53 | var host = server.address().address; 54 | var port = server.address().port; 55 | console.log(('Ringo core server listening at http://0.0.0.0:'+ port).blue); 56 | }); 57 | 58 | var build_serverURL = process.env.HOSTNAME; 59 | var secure_serverURL = process.env.SECURE_HOSTNAME; 60 | 61 | getIP(function (err, ip) { 62 | if (err) { 63 | console.log(err); 64 | } 65 | 66 | if (typeof client != "undefined") { 67 | // satelize.satelize({ip:ip}, function(err, geoData) { 68 | if (err) { 69 | // do something 70 | } else { 71 | 72 | var obj = JSON.parse(geoData); 73 | 74 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 75 | var isp = obj.isp; 76 | var country = obj.country; 77 | var timezone = obj.timezone; 78 | client.addEvent("on_start_server", {"location": location, "isp": isp, "country": country, "timezone": timezone}); 79 | 80 | 81 | } 82 | } 83 | }); 84 | 85 | 86 | 87 | //ngrok.connect(port, function (err, url) { 88 | // console.log("Tunnel open: " + (url).red + " at "+ new Date()); 89 | // process.env["SECURE_HOSTNAME"] = url; 90 | // process.env["HOSTNAME"] = url.replace('https', 'http'); 91 | // build_serverURL = process.env.HOSTNAME; 92 | // secure_serverURL = process.env.SECURE_HOSTNAME; 93 | 94 | reportBalancerTimer = setInterval(reportToLoadBalancer, 1000); // send data to load balancer 95 | //}); 96 | 97 | 98 | 99 | function reportToLoadBalancer() { 100 | if (process.env.LOAD_BALANCER_URL) { 101 | serialNumber(function (err, value) { 102 | if (err) { 103 | console.log('Error getting the server unique ID, will have difficulty registering with the load balancer'.red); 104 | } 105 | 106 | // get the amount of stress on the server 107 | getServerLoad(function (server_load) { 108 | request({ 109 | url: process.env.LOAD_BALANCER_URL + '/register-server/', //URL to hit 110 | method: 'POST', 111 | json: { 112 | server_id: value, 113 | tunnel: process.env.HOSTNAME, 114 | load:server_load, 115 | key: process.env.BALANCER_AUTH_KEY 116 | } 117 | }, function(error, response, body){ 118 | if(error) { 119 | // console.log(error); 120 | } 121 | 122 | }); 123 | }); 124 | }); 125 | } 126 | } 127 | 128 | // Get the ngrok tunnel url 129 | // GET (no parameters) 130 | app.get('/get-secure-tunnel', function (req, res) { 131 | res.setHeader('Content-Type', 'application/json'); 132 | res.send({"tunnel_url": process.env.HOSTNAME}); 133 | }); 134 | 135 | 136 | // Run an Xcode Swift sandbox 137 | // POST {'code':string} 138 | app.post('/build-sandbox', function (req, res) { 139 | cd(buildProjects_path); 140 | 141 | if (req.body.code && (req.body.code).length != 0) { 142 | var ip = req.connection.remoteAddress; 143 | if (typeof client != "undefined") { // only run if the user has set up analytics 144 | satelize.satelize({ip:ip}, function(err, geoData) { 145 | // if data is JSON, we may wrap it in js object 146 | if (err) { 147 | // console.log("There was an error getting the user's location."); 148 | } else { 149 | // console.log(geoData); 150 | 151 | var obj = JSON.parse(geoData); 152 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 153 | var isp = obj.isp; 154 | var country = obj.country; 155 | var timezone = obj.timezone; 156 | var lengthOfCode = (req.body.code).length 157 | 158 | client.addEvent("built_sandbox", {"location": location, "isp": isp, "country": country, "timezone": timezone, "code_length": lengthOfCode}); 159 | } // end error handling 160 | }); // end satelize 161 | } 162 | 163 | 164 | console.log('Sandbox executed at '+ new Date()); 165 | 166 | 167 | fs.writeFile("code.swift", req.body.code, function(err) { 168 | if(err) { 169 | return console.log(err); 170 | } 171 | 172 | exec("swift code.swift", function (err, out, stderror) { 173 | if (stderror) { // if user has buggy code, tell them what they did wrong 174 | res.send(stderror); 175 | } else { // if user doesn't, show them the given output 176 | res.send(out); 177 | } 178 | }); 179 | 180 | 181 | }); 182 | 183 | } else { 184 | res.send("Nothing to compile."); 185 | } 186 | }); 187 | 188 | 189 | 190 | var buildProjects_path = ""; 191 | exec('cd build-projects', function (err, out, stderror) { 192 | // set the build-projects path 193 | process.env["BUILD_PROJECTS_PATH"] = pwd() + "/build-projects"; 194 | 195 | buildProjects_path = process.env.BUILD_PROJECTS_PATH; 196 | 197 | if (err) { // if error, assume that the directory is non-existent 198 | console.log('build-projects directory does not exist! creating one instead.'.red); 199 | console.log('downloading renameXcodeProject.sh...'.cyan); 200 | console.log('downloading XcodeProjAdder...'.cyan) 201 | 202 | exec('mkdir build-projects', function (err, out, stderror) { 203 | cd('build-projects'); 204 | 205 | // download the great 206 | exec('wget http://cdn.rawgit.com/gmittal/ringoPeripherals/master/cli-helpers/renameXcodeProject.sh && wget http://cdn.rawgit.com/gmittal/ringoPeripherals/master/cli-helpers/XcodeProjAdder', function (err, out, stderror) { 207 | console.log(out); 208 | 209 | exec('chmod 755 renameXcodeProject.sh && chmod a+x XcodeProjAdder', function (err, out, stderror) { 210 | console.log(('Successfully downloaded renameXcodeProject.sh at ' + new Date()).green); 211 | console.log(('Successfully downloaded XcodeProjAdder at ' + new Date()).green); 212 | 213 | cleanBuildProjects(); 214 | setInterval(cleanBuildProjects, 60000); // clean the build-projects directory every minute 215 | 216 | }); 217 | 218 | }); 219 | }); 220 | 221 | 222 | } else { 223 | console.log('build-projects directory was found.'.green); 224 | cleanBuildProjects(); 225 | setInterval(cleanBuildProjects, 60000); // clean the build-projects directory every one minute 226 | 227 | } // end if err 228 | 229 | 230 | 231 | }); 232 | 233 | 234 | 235 | 236 | 237 | 238 | // Destroy any projects that haven't been touched for more than 48 hours 239 | function cleanBuildProjects() { 240 | cd(buildProjects_path); 241 | 242 | // console.log("Checking the build-projects directory"); 243 | var projects = ls(); 244 | 245 | var i = 0; 246 | 247 | loopProjects(); 248 | 249 | function loopProjects() { 250 | 251 | if (projects[i] != "XcodeProjAdder") { 252 | if (projects[i] != "renameXcodeProject.sh") { 253 | 254 | fs.stat(projects[i], function (err, stats) { 255 | // console.log(stats); 256 | 257 | var lastModifiedTime = (new Date(stats.mtime)).getTime(); 258 | var currentTime = (new Date()).getTime(); 259 | 260 | // if the time since now and the time when the file was last modified is greater than 172800s (48h) -> destroy! 261 | if (currentTime - lastModifiedTime > 172800000) { 262 | console.log((projects[i] + " is too old. Destroying now.").red); 263 | 264 | // destroy the directory 265 | rm('-rf', projects[i]); 266 | } 267 | 268 | 269 | if (i < projects.length) { 270 | i++; 271 | loopProjects(); 272 | } 273 | 274 | }); 275 | } // end if not renameXcodeProject.sh 276 | } // end if not XcodeProjAdder 277 | 278 | } // end loopFiles() 279 | } // end cleanBuildProjects 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | // Request to make a new Xcode project 288 | // POST {'projectName':string, 'template':string} 289 | app.post('/create-project', function(req, res) { 290 | cd(buildProjects_path); 291 | // only execute if they specify the required parameters 292 | if (req.body.projectName) { 293 | // analytics 294 | var ip = req.connection.remoteAddress; 295 | // console.log("Request made from: " + ip); 296 | 297 | if (typeof client != "undefined") { 298 | satelize.satelize({ip:ip}, function(err, geoData) { 299 | // if data is JSON, we may wrap it in js object 300 | if (err) { 301 | // console.log("There was an error getting the user's location."); 302 | } else { 303 | // console.log(geoData); 304 | 305 | var obj = JSON.parse(geoData); 306 | 307 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 308 | var isp = obj.isp; 309 | var country = obj.country; 310 | var timezone = obj.timezone; 311 | 312 | // console.log(location); 313 | 314 | var project_nomen = req.body.projectName 315 | 316 | client.addEvent("project_created", {"location": location, "isp": isp, "country": country, "timezone": timezone, "name": project_nomen}, function(err, res) { 317 | // if (err) { 318 | // // console.log("Oh no, an error logging project_created".red); 319 | // } else { 320 | // // console.log("Event project_created logged".green); 321 | // } 322 | }); // end client addEvent 323 | 324 | 325 | 326 | } // end error handling 327 | }); // end satelize 328 | } 329 | 330 | var projectName = req.body.projectName; 331 | var project_uid = generatePushID(); 332 | project_uid = project_uid.substr(1, project_uid.length); 333 | 334 | res.setHeader('Content-Type', 'application/json'); 335 | 336 | // Using node's child_process.exec causes asynchronous issues... callbacks are my friend 337 | exec('mkdir '+ project_uid, function (err, out, stderror) { 338 | cd(project_uid); 339 | 340 | var template = req.body.template; 341 | var exec_cmd = ''; // there is a different command that needs to be executed based on the template the user chooses 342 | 343 | if (template == "game") { // generate a SpriteKit game 344 | exec_cmd = 'git clone https://github.com/gmittal/ringoTemplate && .././renameXcodeProject.sh ringoTemplate "'+ projectName +'" && rm -rf ringoTemplate'; 345 | 346 | } else if (template == "mda") { // generates a Master-Detail application 347 | exec_cmd = 'git clone https://github.com/gmittal/ringoMDATemplate && .././renameXcodeProject.sh ringoMDATemplate "'+ projectName +'" && rm -rf ringoMDATemplate'; 348 | 349 | } else if (template == "sva") { // generates a Single View application 350 | exec_cmd = 'git clone https://github.com/gmittal/ringoSVATemplate && .././renameXcodeProject.sh ringoSVATemplate "'+ projectName +'" && rm -rf ringoSVATemplate'; 351 | 352 | } else if (template == "pba") { // generates a Page-based application 353 | exec_cmd = 'git clone https://github.com/gmittal/ringoPBATemplate && .././renameXcodeProject.sh ringoPBATemplate "'+ projectName +'" && rm -rf ringoPBATemplate'; 354 | 355 | } else if (template == "ta") { // generates a Tabbed application 356 | exec_cmd = 'git clone https://github.com/gmittal/ringoTATemplate && .././renameXcodeProject.sh ringoTATemplate "'+ projectName +'" && rm -rf ringoTATemplate'; 357 | 358 | } 359 | 360 | exec(exec_cmd, function (err, out, stderror) { 361 | // console.log(out); 362 | // console.log(err); 363 | console.log('Successfully created '+(project_uid).magenta+' at ' + new Date() + '\n'); 364 | 365 | // don't send back the code until its actually done 366 | res.send({"uid": project_uid}); 367 | 368 | }); 369 | }); 370 | } else { 371 | res.statusCode = 500; 372 | res.send({"Error": "Invalid parameters."}); 373 | } 374 | }); 375 | 376 | 377 | 378 | // download your project code in a ZIP file 379 | // GET /download/project/{ID_STRING} 380 | app.get('/download-project/:id', function (req, res) { 381 | cd(buildProjects_path); 382 | 383 | 384 | // analytics 385 | var ip = req.connection.remoteAddress; 386 | // console.log("Request made from: " + ip); 387 | 388 | if (typeof client != "undefined") { 389 | satelize.satelize({ip:ip}, function(err, geoData) { 390 | // if data is JSON, we may wrap it in js object 391 | if (err) { 392 | // console.log("There was an error getting the user's location."); 393 | } else { 394 | // console.log(geoData); 395 | 396 | var obj = JSON.parse(geoData); 397 | 398 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 399 | var isp = obj.isp; 400 | var country = obj.country; 401 | var timezone = obj.timezone; 402 | 403 | // console.log(location); 404 | 405 | 406 | client.addEvent("project_code_downloaded", {"location": location, "isp": isp, "country": country, "timezone": timezone, "project_id": req.params.id}, function(err, res) { 407 | // if (err) { 408 | // console.log("Oh no, an error logging project_code_downloaded".red); 409 | // } else { 410 | // console.log("Event project_code_downloaded logged".green); 411 | // } 412 | }); // end client addEvent 413 | 414 | 415 | 416 | } // end error handling 417 | }); // end satelize 418 | } // end if undefined 419 | 420 | 421 | 422 | 423 | cd(req.params.id); 424 | 425 | 426 | var name = ls()[0]; 427 | 428 | // before the user can download their file, you have to wipe the project's build directory 429 | cd(name); 430 | 431 | // console.log(pwd()); 432 | // console.log(ls()) 433 | 434 | exec('rm -rf build', function (err, out, stderror) { 435 | // console.log(out); 436 | 437 | console.log('Successfully cleaned up the build directory from the project that will be downloaded.'); 438 | 439 | // now that we've removed the build projects directory, we need to move back up to the ID directory 440 | cd(buildProjects_path + '/' + req.params.id); 441 | 442 | exec('zip -r "'+name+'" "'+name+'"', function (err, out, stderror) { 443 | // console.log(out.cyan); 444 | 445 | res.sendFile(buildProjects_path+"/"+req.params.id+"/"+name+".zip"); 446 | 447 | 448 | }); 449 | 450 | }); 451 | 452 | 453 | }); 454 | 455 | 456 | 457 | 458 | // Upload an Xcode project to be edited 459 | // POST {'file':base64_file_string} 460 | app.post('/upload-project-zip', function (req, res) { 461 | cd(buildProjects_path); 462 | 463 | res.setHeader('Content-Type', 'application/json'); 464 | 465 | if (req.body.file) { 466 | 467 | // analytics 468 | var ip = req.connection.remoteAddress; 469 | console.log("Request made from: " + ip); 470 | 471 | 472 | if (typeof client != "undefined") { 473 | satelize.satelize({ip:ip}, function(err, geoData) { 474 | // if data is JSON, we may wrap it in js object 475 | if (err) { 476 | console.log("There was an error getting the user's location."); 477 | } else { 478 | // console.log(geoData); 479 | 480 | var obj = JSON.parse(geoData); 481 | 482 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 483 | var isp = obj.isp; 484 | var country = obj.country; 485 | var timezone = obj.timezone; 486 | 487 | // console.log(location); 488 | 489 | var fileSize = ((req.body.file.length*3)/4)/1000000; 490 | fileSize = Math.round(fileSize*2)/2; 491 | 492 | // console.log(fileSize + "MB"); 493 | 494 | client.addEvent("upload_project_zip", {"location": location, "isp": isp, "country": country, "timezone": timezone, "size_mb": fileSize}, function(err, res) { 495 | if (err) { 496 | console.log("Oh no, an error logging upload_project_zip".red); 497 | } else { 498 | console.log("Event upload_project_zip logged".green); 499 | } 500 | }); // end client addEvent 501 | 502 | } // end error handling 503 | }); // end satelize 504 | } // end if undefined 505 | 506 | cd(buildProjects_path); 507 | 508 | // create a unique ID where this awesome project will live 509 | var project_uid = generatePushID(); 510 | project_uid = project_uid.substr(1, project_uid.length); 511 | 512 | console.log(project_uid); 513 | 514 | // console.log(req.body.file); 515 | 516 | exec('mkdir '+ project_uid, function (err, out, stderror) { 517 | cd(project_uid); 518 | 519 | var base64Data = req.body.file.replace(/^data:application\/zip;base64,/, ""); 520 | 521 | require("fs").writeFile("anonymous_project.zip", base64Data, 'base64', function(err) { 522 | if (err) { 523 | console.log(err); 524 | } 525 | 526 | console.log('User ZIP project successfully received.'.magenta); 527 | 528 | exec('unzip anonymous_project.zip && rm -rf anonymous_project.zip', function (err, out, stderror) { 529 | console.log('Took out the garbage.'.yellow); 530 | 531 | console.log('Verifying that the project file tree is compliant with Xcode standards...'.yellow); 532 | 533 | cd(buildProjects_path); 534 | 535 | // sometimes operating systems like OS X generate a __MACOSX directory which confuses the system 536 | rm('-rf', project_uid + "/__MACOSX"); 537 | 538 | var id_dir = ls(project_uid)[0]; 539 | 540 | 541 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 542 | 543 | for (var z = 0; z < ls(project_uid + "/" + id_dir).length; z++) { 544 | if (ls(project_uid + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 545 | xc_projName = ls(project_uid + "/" + id_dir)[z].replace('.xcodeproj', ''); 546 | } 547 | } 548 | 549 | if (xc_projName.length !== 0) { 550 | res.send({"id": project_uid}); 551 | } else { 552 | console.log("Does not comply with the standard Xcode project file tree...".red); 553 | res.statusCode = 500; 554 | res.send({"Error": "Invalid parameters"}); 555 | } 556 | }); 557 | 558 | }); // end write ZIP file 559 | 560 | }); // end create project directory 561 | 562 | } else { 563 | res.statusCode = 500; 564 | res.send({"Error": "Invalid parameters"}); 565 | } 566 | 567 | }); 568 | 569 | 570 | // Clone a git project to edited 571 | // POST {'url':string} 572 | app.post('/clone-git-project', function (req, res) { 573 | cd(buildProjects_path); 574 | res.setHeader('Content-Type', 'application/json'); 575 | 576 | if (req.body.url) { 577 | console.log('Received request to git clone a file.'); 578 | 579 | // analytics 580 | var ip = req.connection.remoteAddress; 581 | console.log("Request made from: " + ip); 582 | 583 | if (typeof client != "undefined") { 584 | satelize.satelize({ip:ip}, function(err, geoData) { 585 | // if data is JSON, we may wrap it in js object 586 | if (err) { 587 | console.log("There was an error getting the user's location."); 588 | } else { 589 | var obj = JSON.parse(geoData); 590 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 591 | var isp = obj.isp; 592 | var country = obj.country; 593 | var timezone = obj.timezone; 594 | 595 | var source = "unknown"; 596 | if ((req.body.url).substr(0, 4) == "http") { 597 | source = (req.body.url).split("/")[2]; 598 | } 599 | 600 | // console.log(source); 601 | 602 | client.addEvent("clone_git_project", {"location": location, "isp": isp, "country": country, "timezone": timezone, "source": source}, function(err, res) { 603 | if (err) { 604 | // console.log("Oh no, an error logging clone_git_project".red); 605 | } else { 606 | // console.log("Event clone_git_project logged".green); 607 | } 608 | }); // end client addEvent 609 | } // end error handling 610 | }); // end satelize 611 | } // end if undefined 612 | 613 | // create a unique ID where this awesome project will live 614 | var project_uid = generatePushID(); 615 | project_uid = project_uid.substr(1, project_uid.length); 616 | 617 | console.log(project_uid); 618 | 619 | exec('mkdir '+ project_uid, function (err, out, stderror) { 620 | cd(project_uid); 621 | 622 | // clone the repository 623 | exec('git clone ' + req.body.url, function (err, out, stderror) { 624 | if (out !== undefined) { 625 | console.log(out.cyan); 626 | } 627 | 628 | if (err) { 629 | console.log(err.red); 630 | res.statusCode = 500; 631 | res.send({"Error":"Something did not go as expected."}); 632 | } else { 633 | res.send({"uid": project_uid}); 634 | } 635 | 636 | }); 637 | 638 | }); // end $ mkdir project_uid 639 | 640 | } else { 641 | res.statusCode = 500; 642 | res.send({"Error" : "Invalid parameters"}); 643 | } 644 | 645 | }); 646 | 647 | 648 | 649 | // Save the files with updated content -- assumes end user has already made a request to /get-project-contents 650 | // POST {'id':string, 'files':object_array} 651 | app.post('/update-project-contents', function (req, res) { 652 | cd(buildProjects_path); 653 | 654 | var project_id = req.body.id; 655 | var files = req.body.files; 656 | 657 | console.log(files.length + " files need to be saved for "+ project_id.magenta); 658 | 659 | cd(buildProjects_path); 660 | 661 | var id_dir = ls(project_id)[0]; 662 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 663 | 664 | for (var z = 0; z < ls(project_id + "/" + id_dir).length; z++) { 665 | if (ls(project_id + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 666 | xc_projName = ls(project_id + "/" + id_dir)[z].replace('.xcodeproj', ''); 667 | } 668 | } 669 | 670 | var j = 0; 671 | 672 | writeFiles(); 673 | 674 | function writeFiles() { 675 | var file = files[j]; 676 | 677 | fs.writeFile(project_id+"/"+id_dir+"/"+xc_projName+"/"+file.name, file.data, function (err) { 678 | if (err) { 679 | return console.log(err); 680 | } 681 | 682 | // console.log(file.name +" was saved at "+ new Date()); 683 | 684 | if (j < files.length-1) { 685 | j++; 686 | writeFiles(); 687 | } else { 688 | res.send("Complete"); 689 | } 690 | 691 | }); 692 | 693 | } // end writeFiles() 694 | 695 | }); 696 | 697 | 698 | 699 | // Get all of the files and their contents within an Xcode project 700 | // POST {'id':string} 701 | app.post('/get-project-contents', function(req, res) { 702 | cd(buildProjects_path); 703 | 704 | var project_id = req.body.id; 705 | var id_dir = ls(project_id)[0]; 706 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 707 | 708 | for (var z = 0; z < ls(project_id + "/" + id_dir).length; z++) { 709 | if (ls(project_id + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 710 | xc_projName = ls(project_id + "/" + id_dir)[z].replace('.xcodeproj', ''); 711 | } 712 | } 713 | 714 | // console.log(('Xcode Project File Name: ' + xc_projName).red); 715 | 716 | // crawl the file tree 717 | walk(buildProjects_path + "/" + project_id+"/"+id_dir+"/"+xc_projName, function(err, results) { 718 | if (err) throw err; 719 | 720 | var filtered = []; 721 | 722 | // filter out all the stuff that is useless 723 | for (var i = 0; i < results.length; i++) { 724 | var tmp = results[i]; 725 | 726 | tmp = tmp.split("/"); 727 | 728 | // find all of the unnecessary top level directories 729 | var dirCount = 0; 730 | 731 | for (var k = 0; k < tmp.length; k++) { 732 | if (tmp[k] == project_id) { 733 | break; 734 | } else { 735 | dirCount++; 736 | } 737 | } 738 | 739 | // console.log(dirCount+3) // should be the number of directories that need to be removed 740 | 741 | for (var j = 0; j < dirCount+3; j++) { // remove the parent directories of the file 742 | tmp.shift(); 743 | } 744 | 745 | tmp = tmp.join("/"); 746 | 747 | 748 | if (!(tmp.indexOf(".xcassets") > -1)) { 749 | if (!(tmp.indexOf(".DS_Store") > -1)) { 750 | if (!(tmp.indexOf(".sks") > -1)) { 751 | if (!(tmp.indexOf(".playground") > -1)) { 752 | if (!(tmp.indexOf(".png") > -1)) { 753 | filtered.push(tmp); 754 | } 755 | 756 | } 757 | } 758 | 759 | } 760 | } // end filters 761 | 762 | 763 | } // end for loop 764 | 765 | var files = filtered; 766 | 767 | res.setHeader('Content-Type', 'application/json'); 768 | 769 | var i = 0; 770 | 771 | loopFiles(); 772 | 773 | res.write("["); 774 | // uses file streams to grab contents of each file without maxing out RAM 775 | function loopFiles() { 776 | var file = files[i]; 777 | var contentForFile = {}; 778 | contentForFile["name"] = file; 779 | contentForFile["data"] = ""; 780 | 781 | var fileChunks = fs.createReadStream(project_id+"/"+id_dir+"/"+xc_projName+"/"+file, {encoding: 'utf-8'}); 782 | 783 | fileChunks.on('data', function (chunk) { 784 | contentForFile["data"] += chunk; 785 | }); 786 | 787 | fileChunks.on('end', function() { 788 | console.log(contentForFile.name); 789 | 790 | 791 | if (i < files.length-1) { 792 | res.write(JSON.stringify(contentForFile)+", "); 793 | i++; 794 | loopFiles(); 795 | } else { 796 | res.write(JSON.stringify(contentForFile)); 797 | res.write(', {"count": '+ files.length + '}]'); 798 | res.send(); 799 | cd(buildProjects_path); 800 | } 801 | 802 | 803 | }); 804 | 805 | } 806 | 807 | }); 808 | 809 | }); 810 | 811 | 812 | 813 | // allows you to add a new Xcode image asset to the project asset catalog (requires PNG file) 814 | // POST {'id':string, 'assetName':string, 'file':base64_file_string} 815 | app.post('/add-image-xcasset', function (req, res) { 816 | cd(buildProjects_path); // always need this 817 | 818 | if (req.body.id) { 819 | var project_id = req.body.id; 820 | var newImage = req.body.file; 821 | var xcassetName = req.body.assetName; 822 | var id_dir = ls(project_id)[0]; 823 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 824 | 825 | for (var z = 0; z < ls(project_id + "/" + id_dir).length; z++) { 826 | if (ls(project_id + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 827 | xc_projName = ls(project_id + "/" + id_dir)[z].replace('.xcodeproj', ''); 828 | } 829 | } 830 | 831 | // console.log(('Xcode Project File Name: ' + xc_projName).red); 832 | 833 | var xcassetsDirName = ""; 834 | 835 | // contents of the xcode project files directory (one level below the .xcodeproj file's directory) 836 | var xcProjDirectory = ls(project_id + "/" + id_dir + "/" + xc_projName); 837 | 838 | for (var z = 0; z < xcProjDirectory.length; z++) { 839 | if (xcProjDirectory[z].indexOf('.xcassets') > -1) { 840 | xcassetsDirName = xcProjDirectory[z]; 841 | } 842 | } 843 | 844 | // console.log(('.xcassets Directory Name: ' + xcassetsDirName).cyan); 845 | 846 | var base64Data = req.body.file.replace(/^data:image\/png;base64,/, ""); 847 | 848 | cd(project_id + "/" + id_dir + "/" + xc_projName + "/" + xcassetsDirName); 849 | 850 | exec('mkdir "'+xcassetName+'.imageset"', function (err, out, stderror) { 851 | if (err) { 852 | console.log(err); 853 | } 854 | cd(buildProjects_path + "/"+project_id + "/" + id_dir + "/" + xc_projName + "/" + xcassetsDirName); // lets take it from the top 855 | 856 | fs.writeFile(xcassetName + ".imageset/"+xcassetName+".png", base64Data, 'base64', function (err) { 857 | // console.log(ls()); 858 | 859 | if (err) { 860 | console.log(err); 861 | 862 | res.statusCode = 500; 863 | res.send({"Error": "There was an error creating your xcasset"}); 864 | } else { 865 | 866 | var imageSetJSON = '{\n\ 867 | "images" : [\n\ 868 | {\n\ 869 | "idiom" : "universal",\n\ 870 | "scale" : "1x",\n\ 871 | "filename" : "'+ xcassetName +'.png"\n\ 872 | },\n\ 873 | {\n\ 874 | "idiom" : "universal",\n\ 875 | "scale" : "2x"\n\ 876 | },\n\ 877 | {\n\ 878 | "idiom" : "universal",\n\ 879 | "scale" : "3x"\n\ 880 | }\n\ 881 | ],\n\ 882 | "info" : {\n\ 883 | "version" : 1,\n\ 884 | "author" : "xcode"\n\ 885 | }\n\ 886 | }'; 887 | 888 | fs.writeFile(xcassetName + ".imageset/Contents.json", imageSetJSON, function(err) { 889 | if (err) { 890 | console.log(err); 891 | res.statusCode = 500; 892 | res.send({"Error": "There was an error creating your xcasset"}); 893 | } else { 894 | res.send({"Success":"Image xcasset successfully added."}); 895 | 896 | } 897 | 898 | }); // end writeFile JSON 899 | } 900 | 901 | }); // end writeFile PNG 902 | }); // end exec mkdir 903 | 904 | } else { 905 | res.statusCode = 500; 906 | res.send({"Error": "Invalid parameters"}); 907 | 908 | } // end if req.body.id 909 | 910 | }); 911 | 912 | 913 | // get the xcasset files 914 | // POST {'id':string} 915 | app.post('/get-image-xcassets', function (req, res) { 916 | cd(buildProjects_path); 917 | 918 | 919 | if (req.body.id) { 920 | var project_id = req.body.id; 921 | var id_dir = ls(project_id)[0]; 922 | 923 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 924 | 925 | for (var z = 0; z < ls(project_id + "/" + id_dir).length; z++) { 926 | if (ls(project_id + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 927 | xc_projName = ls(project_id + "/" + id_dir)[z].replace('.xcodeproj', ''); 928 | } 929 | } 930 | 931 | // console.log(('Xcode Project File Name: ' + xc_projName).red); 932 | 933 | // crawl the file tree 934 | walk(buildProjects_path + "/" + project_id+"/"+id_dir+"/"+xc_projName, function(err, results) { 935 | if (err) throw err; 936 | 937 | var filtered = []; 938 | 939 | // filter out all the stuff that is useless 940 | for (var i = 0; i < results.length; i++) { 941 | var tmp = results[i]; 942 | 943 | tmp = tmp.split("/"); 944 | 945 | // find all of the unnecessary top level directories 946 | var dirCount = 0; 947 | 948 | for (var k = 0; k < tmp.length; k++) { 949 | if (tmp[k] == project_id) { 950 | break; 951 | } else { 952 | dirCount++; 953 | } 954 | } 955 | 956 | 957 | for (var j = 0; j < dirCount+3; j++) { // remove the parent directories 958 | tmp.shift(); 959 | } 960 | 961 | tmp = tmp.join("/"); 962 | 963 | // filter through all of the stuff that we don't want 964 | if (!(tmp.indexOf(".swift") > -1)) { 965 | if (!(tmp.indexOf(".lproj") > -1)) { 966 | if (!(tmp.indexOf(".sks") > -1)) { 967 | if (!(tmp.indexOf(".playground") > -1)) { 968 | if (!(tmp.indexOf(".plist") > -1)) { 969 | if (!(tmp.indexOf(".m") > -1)) { 970 | if (!(tmp.indexOf(".h") > -1)) { 971 | if (!(tmp.indexOf(".json") > -1)) { 972 | if (!(tmp.indexOf(".DS_Store") > -1)) { 973 | filtered.push(tmp); 974 | } 975 | 976 | } 977 | 978 | } 979 | } 980 | 981 | } 982 | 983 | } 984 | } 985 | 986 | } 987 | } // end filters 988 | 989 | } // end for loop 990 | 991 | var files = []; 992 | 993 | for (var n = 0; n < filtered.length; n++) { 994 | var t = filtered[n]; 995 | 996 | // push the un-altered copy to the files array 997 | files.push(t); 998 | 999 | var imageSetPathSplit = t.split("/"); 1000 | var imageSetName = imageSetPathSplit[1]; // usually the second folder's name in the path hierarchy 1001 | 1002 | filtered[n] = imageSetName; 1003 | } 1004 | 1005 | // only present one of the many images that may be within an imageset 1006 | for (var o = 0; o < filtered.length; o++) { 1007 | var u = filtered[o]; 1008 | if (u == filtered[o+1]) { 1009 | filtered.splice(o, 1); 1010 | files.splice(o, 1) 1011 | } 1012 | } 1013 | 1014 | res.setHeader('Content-Type', 'application/json'); 1015 | 1016 | // console.log(filtered) 1017 | // console.log(files); 1018 | 1019 | var filesContents = []; // final array of json data 1020 | var i = 0; 1021 | 1022 | if (files.length > 0) { 1023 | loopFiles(); 1024 | } else { 1025 | console.log("No Xcode image assets were found".cyan); 1026 | res.send({"files": []}); 1027 | cd(buildProjects_path); 1028 | } 1029 | 1030 | 1031 | function loopFiles() { 1032 | var file = files[i]; 1033 | // console.log(file); 1034 | 1035 | fs.readFile(project_id+"/"+id_dir+"/"+xc_projName+"/"+file, function (err, data) { 1036 | if (err) { 1037 | return console.log(err); 1038 | } 1039 | 1040 | var contentForFile = {}; 1041 | contentForFile["name"] = filtered[i]; 1042 | 1043 | contentForFile["data"] = new Buffer(data).toString('base64'); 1044 | filesContents.push(contentForFile); 1045 | 1046 | if (i < files.length) { 1047 | loopFiles(); 1048 | i++; 1049 | } else { 1050 | res.send({"files": filesContents}); 1051 | cd(buildProjects_path); 1052 | } 1053 | }); 1054 | } 1055 | 1056 | }); 1057 | } 1058 | }); 1059 | 1060 | 1061 | // add new files to the project directory 1062 | // POST {'id':string, 'fileName':string} 1063 | app.post('/add-file', function (req, res) { 1064 | cd(buildProjects_path); 1065 | res.setHeader('Content-Type', 'application/json'); 1066 | 1067 | if (req.body.id) { 1068 | var project_uid = req.body.id; 1069 | var newFileName = req.body.fileName; 1070 | 1071 | //removeAllSpaces 1072 | newFileName = newFileName.split(" ").join(""); 1073 | 1074 | var id_dir = ls(project_uid)[0]; 1075 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 1076 | 1077 | for (var z = 0; z < ls(project_uid + "/" + id_dir).length; z++) { 1078 | if (ls(project_uid + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 1079 | xc_projName = ls(project_uid + "/" + id_dir)[z].replace('.xcodeproj', ''); 1080 | } 1081 | } 1082 | 1083 | var xcpath = buildProjects_path + "/" + project_uid + "/" + id_dir + "/"+ xc_projName + ".xcodeproj"; 1084 | cd(project_uid + "/" + id_dir + "/" + xc_projName); 1085 | 1086 | // make sure there are no copies of the exact same file 1087 | var currentFiles = ls(); 1088 | 1089 | for (var l = 0; l < currentFiles.length; l++) { 1090 | if (currentFiles[l] == newFileName + ".swift") { 1091 | console.log("The file that is being added already exists".red); 1092 | newFileName += "Copy"; 1093 | } 1094 | } 1095 | 1096 | // download a vanilla swift class file 1097 | exec('wget cdn.rawgit.com/gmittal/ringoPeripherals/master/new-class-templates/R6roHpOHU8qa3Z2TvHsG.swift', function (err, out, stderror) { 1098 | // rename file after downloading from GitHub 1099 | exec('mv "R6roHpOHU8qa3Z2TvHsG.swift" "'+ newFileName + '.swift"', function (err, out, stderror) { 1100 | var filePath = xc_projName + "/" + newFileName + '.swift'; 1101 | 1102 | // now the important step: adding the file reference to the .xcodeproj file 1103 | cd(buildProjects_path); 1104 | exec('./XcodeProjAdder -XCP "'+xcpath+'" -SCSV "'+ filePath + '"', function (err, out, stderror) { 1105 | // console.log(xcpath); 1106 | res.send({"Success":"Successfully added file named "+newFileName+".swift"}); 1107 | 1108 | }); // end exec 1109 | 1110 | }); 1111 | 1112 | }); 1113 | } else { 1114 | res.statusCode = 500; 1115 | res.send({"Error": "Invalid parameters"}); 1116 | 1117 | } 1118 | 1119 | // note: the file has to already have been made and added into the directory, the following command just links it to the .xcodeproj so Xcode can run its debuggers through it 1120 | // $ ./XcodeProjAdder -XCP PROJECT_ID/XC_PROJECT_NAME/XC_PROJECT_NAME.xcodeproj -SCSV PROJECT_NAME/NEW_FILE.swift 1121 | 1122 | }); 1123 | 1124 | 1125 | // Delete a file from the Xcode project directory 1126 | // POST {'id':string, 'fileName':string} 1127 | app.post('/delete-file', function (req, res) { 1128 | cd(buildProjects_path); 1129 | res.setHeader('Content-Type', 'application/json'); 1130 | 1131 | if (req.body.id) { 1132 | var project_uid = req.body.id; 1133 | var deleteFileName = req.body.fileName; 1134 | 1135 | var id_dir = ls(project_uid)[0]; 1136 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 1137 | 1138 | for (var z = 0; z < ls(project_uid + "/" + id_dir).length; z++) { 1139 | if (ls(project_uid + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 1140 | xc_projName = ls(project_uid + "/" + id_dir)[z].replace('.xcodeproj', ''); 1141 | } 1142 | } 1143 | 1144 | var xcpath = buildProjects_path + "/" + project_uid + "/" + id_dir + "/"+ xc_projName + ".xcodeproj"; 1145 | cd(project_uid + "/" + id_dir + "/" + xc_projName); 1146 | 1147 | // delete the file (this is way simpler than adding a new file) 1148 | exec('rm -rf "'+ deleteFileName +'"', function (err, out, stderror) { 1149 | console.log(('Attempting to remove file named '+deleteFileName).cyan); 1150 | // console.log(JSON.stringify(ls()).yellow); 1151 | 1152 | if (err) { 1153 | res.statusCode = 500; 1154 | res.send({"Error": "There was an error deleting the file."}); 1155 | 1156 | } else { 1157 | cd(buildProjects_path + "/" + project_uid + "/" + id_dir + "/" + xc_projName + ".xcodeproj"); 1158 | 1159 | // unfortunately we have to dig down to farthest depths of the project filetree to delete the file, as well as modify the core file of the .xcodeproj 1160 | fs.readFile("project.pbxproj", 'utf-8', function (err, data) { 1161 | if (err) { 1162 | res.statusCode = 500; 1163 | res.send({"Error": "There was an error deleting the file."}); 1164 | } else { 1165 | // console.log(data); 1166 | var lines = data.split('\n'); 1167 | console.log((lines.length + ' lines of code in the project.pbxproj').green); 1168 | 1169 | for (var h = 0; h < lines.length; h++) { 1170 | // find the various lines that contain the file we want to delete 1171 | if (lines[h].indexOf(deleteFileName) > -1) { 1172 | console.log(("Line "+ (h+1).toString() + " contains " + deleteFileName).red); 1173 | lines.splice(h, 1); // delete the line 1174 | 1175 | } 1176 | } 1177 | 1178 | var newFile = lines.join('\n'); 1179 | 1180 | fs.writeFile("project.pbxproj", newFile, function (err) { 1181 | if (err) { 1182 | res.statusCode = 500; 1183 | res.send({"Error": "There was an error deleting the file."}); 1184 | 1185 | } else { 1186 | console.log(('Successfully rewrote project.pbxproj and deleted file named '+deleteFileName).green); 1187 | res.send({"Success": "Successfully deleted file named "+deleteFileName}); // finally, after that long process, we can finally notify the user that all is well 1188 | 1189 | } // 1190 | }); // end writeFile 1191 | 1192 | } // end if err within readFile 1193 | 1194 | }); // end readFile 1195 | } // end if err rm -rf 1196 | 1197 | }); // end exec rm -rf 1198 | } else { 1199 | res.statusCode = 500; 1200 | res.send({"Error": "Invalid parameters"}); 1201 | 1202 | } 1203 | }); 1204 | 1205 | 1206 | // Build an Xcode Project using the appetize.io on-screen simulator 1207 | // POST {'id':string} 1208 | app.post('/build-project', function (req, res) { 1209 | // take the app back to the build-projects directory, as another route may have thrown the build server into a project directory instead 1210 | cd(buildProjects_path); 1211 | 1212 | if (req.body.id) { 1213 | // analytics 1214 | var ip = req.connection.remoteAddress; 1215 | // console.log("Request made from: " + ip); 1216 | 1217 | if (typeof client != "undefined") { 1218 | satelize.satelize({ip:ip}, function(err, geoData) { 1219 | // if data is JSON, we may wrap it in js object 1220 | if (err) { 1221 | // console.log("There was an error getting the user's location."); 1222 | } else { 1223 | var obj = JSON.parse(geoData); 1224 | var location = obj.city + ", " + obj.region_code + ", " + obj.country_code3; 1225 | var isp = obj.isp; 1226 | var country = obj.country; 1227 | var timezone = obj.timezone; 1228 | 1229 | client.addEvent("built_project", {"location": location, "isp": isp, "country": country, "timezone": timezone, "project_id": req.body.id}, function(err, res) { 1230 | // if (err) { 1231 | // console.log("Oh no, an error logging built_project".red); 1232 | // } else { 1233 | // console.log("Event built_project logged".green); 1234 | // } 1235 | }); // end client addEvent 1236 | 1237 | } // end error handling 1238 | }); // end satelize 1239 | } // end if undefined 1240 | 1241 | var projectID = req.body.id; 1242 | 1243 | // $ xcodebuild -sdk iphonesimulator -project XCODEPROJ_PATH 1244 | // this generates the build directory where you can zip up the file to upload to appetize 1245 | 1246 | var id_dir = ls(projectID)[0]; // project name e.g. WWDC 1247 | var project_dir = ls(projectID+"/"+id_dir); 1248 | 1249 | // go into the directory 1250 | cd(projectID+"/"+id_dir); 1251 | 1252 | // various methods of filtering through the success build logs 1253 | // $ xcodebuild -sdk iphonesimulator -configuration Debug -verbose > /dev/null 1254 | 1255 | // various methods of filtering the error logs 1256 | // $ xcodebuild -sdk iphonesimulator -configuration Release -verbose | egrep '^(/.+:[0-9+:[0-9]+:.(error|warning):|fatal|===)' - 1257 | // $ xcodebuild -sdk iphonesimulator -configuration Release -verbose | grep -A 5 error: 1258 | 1259 | console.log('Attempting to build the project...'.red); 1260 | 1261 | // build using Xcode 1262 | exec('xcodebuild -sdk iphonesimulator -configuration Release -verbose | grep -A 5 error:', function (err, xcode_out, stderror) { 1263 | //cd('build/'+id_dir+'.build/Release-iphonesimulator'); 1264 | console.log(xcode_out.green); 1265 | 1266 | cd(buildProjects_path); 1267 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 1268 | 1269 | for (var z = 0; z < ls(projectID + "/" + id_dir).length; z++) { 1270 | if (ls(projectID + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 1271 | xc_projName = ls(projectID + "/" + id_dir)[z].replace('.xcodeproj', ''); 1272 | } 1273 | } 1274 | 1275 | var normalized = xc_projName.split(' ').join('\ '); 1276 | // console.log('Normalized NAME: ' + normalized); 1277 | // well this is important 1278 | cd(projectID+"/"+id_dir +'/build/Release-iphonesimulator'); 1279 | 1280 | // zip up the simulator executable 1281 | exec('zip -r "'+projectID+'" "'+xc_projName+'.app"', function (err, out, stderror) { 1282 | cd(buildProjects_path); // enter build-projects once again (using absolute paths!) 1283 | // console.log(out.green); 1284 | 1285 | var path = projectID + "/" + id_dir + "/build/Release-iphonesimulator/" + projectID + ".zip"; 1286 | // console.log(buildProjects_path + path); 1287 | 1288 | var zip_dl_url = build_serverURL + "/" + path; 1289 | console.log(".zip of simulator executable: " + zip_dl_url.cyan); 1290 | // console.log(typeof xcode_out) 1291 | 1292 | // check if the build succeeded 1293 | if (xcode_out == "") { //.indexOf("** BUILD SUCCEEDED **") > -1) { 1294 | // use the 'request' module from npm 1295 | var request = require('request'); 1296 | request.post({ 1297 | url: 'https://api.appetize.io/v1/app/update', 1298 | json: { 1299 | token : process.env.APPETIZE_TOKEN, 1300 | url : zip_dl_url, 1301 | platform : 'ios', 1302 | } 1303 | }, function(err, message, response) { 1304 | if (err) { 1305 | // error 1306 | console.log(err); 1307 | res.send({'ERROR': err}); 1308 | 1309 | } else { 1310 | // success 1311 | // console.log(message.body); 1312 | // console.log(message.body.publicURL != null); 1313 | 1314 | if (message.body.publicURL != null) { 1315 | var public_key = (message.body.publicURL).split("/")[4]; 1316 | console.log("Simulator Public Key: " + public_key.yellow); 1317 | 1318 | var osVersion = "9.0"; // the version of iOS appetize should build for 1319 | 1320 | var screenEmbed = ''; 1321 | var deviceEmbed = ''; 1322 | 1323 | res.send({'simulatorURL': message.body.publicURL, "screenOnlyEmbedCode": screenEmbed, "fullDeviceEmbedCode": deviceEmbed, "console": xcode_out}); 1324 | } else { 1325 | res.send({"BUILD_FAILED": "There was an error building your application."}); 1326 | } 1327 | } 1328 | }); // end request 1329 | } else {// end if build succeeded 1330 | res.send({"BUILD_FAILED": xcode_out}); 1331 | 1332 | } 1333 | }); // end zip exec 1334 | }); // end xcodebuild exec 1335 | 1336 | /* SIMULATOR EMBED CODE: 1337 | 1338 | */ 1339 | 1340 | } else { 1341 | res.send({"Error": "Invalid parameters."}); 1342 | } 1343 | 1344 | }); 1345 | 1346 | 1347 | 1348 | 1349 | // allow user to grab project information such as name, bundle ID, etc. 1350 | // GET /get-project-details/{ID_STRING} 1351 | app.get('/get-project-details/:app_id', function (req, res) { 1352 | cd(buildProjects_path); 1353 | res.setHeader('Content-Type', 'application/json'); 1354 | 1355 | var project_id = req.params.app_id; 1356 | var id_dir = ls(project_id)[0]; // project directory 1357 | 1358 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 1359 | 1360 | for (var z = 0; z < ls(project_id + "/" + id_dir).length; z++) { 1361 | if (ls(project_id + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 1362 | xc_projName = ls(project_id + "/" + id_dir)[z].replace('.xcodeproj', ''); 1363 | } 1364 | } 1365 | 1366 | var fileList = ls(project_id + "/" + id_dir + "/" + xc_projName); 1367 | var assetCatalogDirname = ""; 1368 | for (var x = 0; x < fileList.length; x++) { 1369 | var fileobj = fileList[x]; 1370 | if (fileobj.indexOf(".xcassets") > -1) { 1371 | assetCatalogDirname = fileList[x]; 1372 | fileList.splice(x, 1); 1373 | } 1374 | } 1375 | 1376 | var assetCatalogList = ls(project_id + "/" + id_dir + "/" + xc_projName + "/" + assetCatalogDirname); 1377 | 1378 | res.send({"project": {"name": xc_projName, "file_count": fileList.length, "asset_count": assetCatalogList.length}}); 1379 | 1380 | }); 1381 | 1382 | 1383 | 1384 | // Route that generates an ad-hoc IPA file for the user to download onto their device (is this against Apple's terms?) 1385 | // POST {'id':string} 1386 | app.post('/create-ipa', function (req, res) { 1387 | // $ ipa build 1388 | // take the app back to the build-projects directory, as another route may have thrown the build server into a project directory instead 1389 | cd(buildProjects_path); 1390 | 1391 | // json headers 1392 | res.setHeader('Content-Type', 'application/json'); 1393 | 1394 | if (req.body.id) { 1395 | console.log('Attempting to generate a .ipa file.'); 1396 | 1397 | var projectID = req.body.id; 1398 | 1399 | var id_dir = ls(projectID)[0]; // project name e.g. WWDC 1400 | var project_dir = ls(projectID+"/"+id_dir); 1401 | 1402 | // go into the directory 1403 | cd(projectID+"/"+id_dir); 1404 | 1405 | exec('ipa build -c Release', function (err, out, stderror) { 1406 | 1407 | if (err) { 1408 | res.send({"Error": "There was an error generating your IPA file. Please double check that there are no syntax errors or other issues with your code."}); 1409 | } else { 1410 | // console.log(out); 1411 | // console.log("\n"); 1412 | var xc_projName = ""; // suprisingly enough, people like to name their repository name differently than their .xcodeproj name 1413 | 1414 | for (var z = 0; z < ls(projectID + "/" + id_dir).length; z++) { 1415 | if (ls(projectID + "/" + id_dir)[z].indexOf('.xcodeproj') > -1) { 1416 | xc_projName = ls(projectID + "/" + id_dir)[z].replace('.xcodeproj', ''); 1417 | } 1418 | } 1419 | // console.log(('Xcode Project File Name: ' + xc_projName).red); 1420 | console.log('IPA for project '+ projectID + ' generated at '+ new Date()); 1421 | var ipa_path = projectID +"/"+ id_dir + "/" + xc_projName + ".ipa"; 1422 | var ipa_dl_url = secure_serverURL + "/" + ipa_path; 1423 | console.log(ipa_dl_url.cyan); 1424 | console.log('Generating manifest.plist...'); 1425 | var manifest_plist_data = 'itemsassetskindsoftware-packageurl'+ ipa_dl_url +'metadatabundle-identifiercom.Ringo.'+ id_dir +'bundle-version1kindsoftwaretitle'+id_dir+''; 1426 | fs.writeFile("manifest.plist", manifest_plist_data, function(err) { 1427 | if(err) { 1428 | return console.log(err); 1429 | } 1430 | 1431 | var mainfest_plist_url = secure_serverURL + "/" + projectID +"/"+ id_dir + "/manifest.plist"; 1432 | // console.log(mainfest_plist_url); 1433 | 1434 | console.log('Successfully generated IPA manifest.plist.'); 1435 | 1436 | var signed_dl_url = "itms-services://?action=download-manifest&url="+encodeURIComponent(mainfest_plist_url); 1437 | console.log(signed_dl_url.cyan); 1438 | 1439 | // raw_ipa_url is the link that directly downloads the IPA file, the signed_dl_url allows you to download the IPA file on an iOS device 1440 | res.send({"raw_ipa_url": ipa_dl_url, "signed_dl_url": signed_dl_url}); 1441 | 1442 | }); 1443 | 1444 | } 1445 | 1446 | }); 1447 | } else { 1448 | res.send({"Error": "Invalid parameters."}); 1449 | } 1450 | 1451 | }); 1452 | 1453 | 1454 | 1455 | // what happens when someone kills the server 1456 | process.on('SIGINT', function() { 1457 | console.log("Killing Ringo Core server...".red); 1458 | 1459 | if (process.env.LOAD_BALANCER_URL) { // lets unregister this dead server 1460 | clearInterval(reportBalancerTimer); // stop sending events to the load balancer 1461 | 1462 | serialNumber(function (err, value) { // basically for generating a unique id 1463 | // console.log(value); 1464 | 1465 | request({ 1466 | url: process.env.LOAD_BALANCER_URL + '/unregister-server/', //URL to hit 1467 | method: 'POST', 1468 | //Lets post the following key/values as form 1469 | json: { 1470 | server_id: value, 1471 | key: process.env.BALANCER_AUTH_KEY 1472 | } 1473 | }, function(error, response, body){ 1474 | if(error) { 1475 | // console.log(error); 1476 | console.log("Uh oh! The load balancer is probably down. Exiting anyway...".red); 1477 | if (process.env.NGROK_TUNNEL_PID) { 1478 | // ngrok.stop(process.env.NGROK_TUNNEL_PID); 1479 | } 1480 | 1481 | process.exit(); // kill the application 1482 | 1483 | } else { 1484 | console.log((response.statusCode, body).green); 1485 | 1486 | if (process.env.NGROK_TUNNEL_PID) { 1487 | // ngrok.stop(process.env.NGROK_TUNNEL_PID); 1488 | } 1489 | 1490 | process.exit(); 1491 | } 1492 | }); // end request 1493 | 1494 | }); // end serial 1495 | } 1496 | 1497 | }); 1498 | 1499 | 1500 | // what happens when an error occurs 1501 | /*process.on('uncaughtException', function (uncaughterr) { 1502 | console.log(('Caught exception: ' + uncaughterr).red); 1503 | 1504 | if (typeof sendgrid !== "undefined") { 1505 | getIP(function (err, ip) { 1506 | if (err) { 1507 | // every service in the list has failed 1508 | console.log(err); 1509 | } else { 1510 | console.log("Attempting to send error report to " + process.env.REPORT_TO); 1511 | 1512 | sendgrid.send({ 1513 | to: process.env.REPORT_TO, 1514 | from: 'ringo-error@useringo.github.io', 1515 | subject: 'Ringo Internal Error', 1516 | text: 'The Ringo internal server error on machine with address ' + ip + ' ran into error: \n\n' + uncaughterr 1517 | }, function(err, json) { 1518 | if (err) { return console.error(err); } 1519 | // console.log(json); 1520 | }); 1521 | } 1522 | 1523 | }); // end getIP 1524 | } // end typeof sendgrid 1525 | 1526 | }) ;*/ 1527 | 1528 | 1529 | // function that returns the CPU load of the server (OS X compatible only) 1530 | // meant to be used as getServerLoad(function(out) { console.log(out); }); 1531 | function getServerLoad(callback) { 1532 | exec('uptime', function (err, out, stderror) { 1533 | var uptimeStr = out; 1534 | var sysValues = uptimeStr.split(', '); 1535 | 1536 | // find the amount of stress being put on the server CPU 1537 | var loadLastMin; 1538 | 1539 | for (var i = 0; i < sysValues.length; i++) { 1540 | if (sysValues[i].indexOf("load average") > -1) { 1541 | var loadAvg = sysValues[i]; 1542 | loadLastMin = parseFloat(sysValues[i].split(' ')[2], 10); 1543 | } 1544 | } 1545 | 1546 | // find the number of CPUs (OS X only command) -- this is why you get a Mac server 1547 | exec('sysctl -a | grep machdep.cpu | grep core_count', function (grepErr, grepOut, grepSTDError) { 1548 | var numCPU = parseInt(grepOut.split(' ')[1].replace('\n', ''), 10); 1549 | 1550 | var loadPercentage = (loadLastMin/numCPU)*100; 1551 | callback(loadPercentage); // return the load in a callback 1552 | 1553 | }); 1554 | 1555 | }); // end $ uptime 1556 | 1557 | } // end getServerLoad 1558 | 1559 | 1560 | // function that invokes a crawl through all of the directories and files 1561 | var walk = function(dir, done) { 1562 | var results = []; 1563 | fs.readdir(dir, function(err, list) { 1564 | if (err) return done(err); 1565 | var i = 0; 1566 | 1567 | (function next() { 1568 | var file = list[i++]; 1569 | if (!file) return done(null, results); 1570 | file = dir + '/' + file; 1571 | fs.stat(file, function(err, stat) { 1572 | if (stat && stat.isDirectory()) { 1573 | walk(file, function(err, res) { 1574 | results = results.concat(res); 1575 | next(); 1576 | }); 1577 | } else { 1578 | results.push(file); 1579 | next(); 1580 | } 1581 | }); 1582 | })(); 1583 | }); 1584 | }; 1585 | 1586 | 1587 | // remove array objects 1588 | Array.prototype.remove = function() { 1589 | var what, a = arguments, L = a.length, ax; 1590 | while (L && this.length) { 1591 | what = a[--L]; 1592 | while ((ax = this.indexOf(what)) !== -1) { 1593 | this.splice(ax, 1); 1594 | } 1595 | } 1596 | return this; 1597 | }; 1598 | 1599 | 1600 | /** 1601 | * Fancy ID generator that creates 20-character string identifiers with the following properties: 1602 | * 1603 | * 1. They're based on timestamp so that they sort *after* any existing ids. 1604 | * 2. They contain 72-bits of random data after the timestamp so that IDs won't collide with other clients' IDs. 1605 | * 3. They sort *lexicographically* (so the timestamp is converted to characters that will sort properly). 1606 | * 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, the 1607 | * latter ones will sort after the former ones. We do this by using the previous random bits 1608 | * but "incrementing" them by 1 (only in the case of a timestamp collision). 1609 | */ 1610 | generatePushID = (function() { 1611 | // Modeled after base64 web-safe chars, but ordered by ASCII. 1612 | var PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; 1613 | 1614 | // Timestamp of last push, used to prevent local collisions if you push twice in one ms. 1615 | var lastPushTime = 0; 1616 | 1617 | // We generate 72-bits of randomness which get turned into 12 characters and appended to the 1618 | // timestamp to prevent collisions with other clients. We store the last characters we 1619 | // generated because in the event of a collision, we'll use those same characters except 1620 | // "incremented" by one. 1621 | var lastRandChars = []; 1622 | 1623 | return function() { 1624 | var now = new Date().getTime(); 1625 | var duplicateTime = (now === lastPushTime); 1626 | lastPushTime = now; 1627 | 1628 | var timeStampChars = new Array(8); 1629 | for (var i = 7; i >= 0; i--) { 1630 | timeStampChars[i] = PUSH_CHARS.charAt(now % 64); 1631 | // NOTE: Can't use << here because javascript will convert to int and lose the upper bits. 1632 | now = Math.floor(now / 64); 1633 | } 1634 | if (now !== 0) throw new Error('We should have converted the entire timestamp.'); 1635 | 1636 | var id = timeStampChars.join(''); 1637 | 1638 | if (!duplicateTime) { 1639 | for (i = 0; i < 12; i++) { 1640 | lastRandChars[i] = Math.floor(Math.random() * 64); 1641 | } 1642 | } else { 1643 | // If the timestamp hasn't changed since last push, use the same random number, except incremented by 1. 1644 | for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) { 1645 | lastRandChars[i] = 0; 1646 | } 1647 | lastRandChars[i]++; 1648 | } 1649 | for (i = 0; i < 12; i++) { 1650 | id += PUSH_CHARS.charAt(lastRandChars[i]); 1651 | } 1652 | if(id.length != 20) throw new Error('Length should be 20.'); 1653 | 1654 | return id; 1655 | }; 1656 | })(); 1657 | --------------------------------------------------------------------------------