├── .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 |
--------------------------------------------------------------------------------