Thu Nov 16 2017
Copied to clipboard! Copy reply
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510
  • 511
  • 512
  • 513
  • 514
  • 515
  • 516
  • 517
  • 518
  • 519
  • 520
  • 521
  • 522
  • 523
  • 524
  • 525
  • 526
  • 527
  • 528
  • 529
  • 530
  • 531
  • 532
  • 533
  • 534
  • 535
  • 536
  • 537
  • 538
  • 539
  • 540
  • 541
  • 542
  • 543
  • 544
  • 545
  • 546
  • 547
  • 548
  • 549
  • 550
  • 551
  • 552
  • 553
  • 554
  • 555
  • 556
  • 557
  • 558
  • 559
  • 560
  • 561
  • 562
  • 563
  • 564
  • 565
  • 566
  • 567
  • 568
  • 569
  • 570
  • 571
  • 572
  • 573
  • 574
  • 575
  • 576
  • 577
  • 578
  • 579
  • 580
  • 581
  • 582
  • 583
  • 584
  • 585
  • 586
  • 587
  • 588
  • 589
  • 590
  • 591
  • 592
  • 593
  • 594
  • 595
  • 596
  • 597
  • 598
  • 599
  • 600
  • 601
  • 602
  • 603
  • 604
  • 605
  • 606
  • 607
  • 608
  • 609
  • 610
  • 611
  • 612
  • 613
  • 614
  • 615
  • 616
  • 617
  • 618
  • 619
  • 620
  • 621
  • 622
  • 623
  • 624
  • 625
  • 626
  • 627
  • 628
  • 629
  • 630
  • 631
  • 632
  • 633
  • 634
  • 635
  • 636
  • 637
  • 638
  • 639
  • 640
  • 641
  • 642
  • 643
  • 644
  • 645
  • 646
  • 647
  • 648
  • 649
  • 650
  • 651
  • 652
  • 653
  • 654
  • 655
  • 656
  • 657
  • 658
  • 659
  • 660
  • 661
  • 662
  • 663
  • 664
  • 665
  • 666
  • 667
  • 668
  • 669
  • 670
  • 671
  • 672
  • 673
  • 674
  • 675
  • 676
  • 677
  • 678
  • 679
  • 680
  • 681
  • 682
  • 683
  • 684
  • 685
  • 686
  • 687
  • 688
  • 689
  • 690
  • 691
  • 692
  • 693
  • 694
  • 695
  • 696
  • 697
  • 698
  • 699
  • 700
  • 701
  • 702
  • 703
  • 704
  • 705
  • 706
  • 707
  • 708
  • 709
  • 710
  • 711
  • 712
  • 713
  • 714
  • 715
  • 716
  • 717
  • 718
  • 719
  • 720
  • 721
  • 722
  • 723
  • 724
  • 725
  • 726
  • 727
  • 728
  • 729
  • 730
  • 731
  • 732
  • 733
  • 734
  • 735
  • 736
  • 737
  • 738
  • 739
  • 740
  • 741
  • 742
  • 743
 /**
  * Script to push pages to staging or production server.
  * At least 2 argument is required - an action(up or down) and pagename.
  * always pass the page names last
  * arguments & usage:
  * --action, -a: action to take. 'up' or 'down'. Defaults to up if omitted. I realise 'down' should've been 'delete'.
  *               * push sitewide to stg, all templates
  *               node swag -a up sitewide
  *               node swag sitewide
  *
  * --env, -e: environment to perform action. 'stg' or 'prod'. Defaults to 'stg' if omitted
  *            * push sitewide to prod, all templates
  *            node swag -e prod sitewide
  *
  * --mastheads, -m: pass to attempt to upload mastheads as well.
  *                  * push sitewide to stg, attempts to push its masthead
  *                  node swag -m sitewide
  *
  * --templates, -t: only upload pages for the templates specified. Defaults to all if omitted
  *                  * pushes sale to prod, only for ppc template
  *                  node swag -e prod -t ppc -p sale
  *
  * --pages, -p: which pages to push.
  *              * push sitewide, rmsitewide, bluemonday to stg, for every template
  *              node swag  -p sitewide rmsitewide
  *
  * Note that if you're specifying --templates right before the pages to upload,
  * you need to pass --pages (-p), because if you do something like
  * swag -a up -t sbs sitewide, it's going to think sitewide is a template, since -t
  * accepts multiple params. In the case above, the right way is
  * swag -a up -t sbs -p sitewide
  *
  * You will need to put your public ssh key on the server
  * if for some reason you can't or dont want to do that, use push.sh,
  * which works just like this script, but it's in bash and puts
  * files recursively instead of scp'ish command.
  * to put a put a public key on the server;
  * 1. ssh-keygen -t rsa
  * 2. ssh-copy-id lpstagingadmin@10.130.250.237 (or manually copy your key via ftp. do prod too)
  * 3. do steps above for prod as well.
  * only do step 1 if you don't already have a private key.
  * look in ~/.ssh/id_rsa to find out if you do.
  * Your private key will need to be stored in /home/{user}/.ssh/id_rsa
  *
  * I'm also using some ES6 syntax so you'll need to use harmony flags. Best and easiest way
  * to do this is to add the following alias to your terminal rc file:
  * alias node="node  $(node --v8-options | grep harm | awk '{print $1}' | xargs)"
  *
  * PS: swag down and swag -i aren't implemented yet.
  *
  * A NOTE on pushing mastheads... or passing -m in the command line
  * Not the most reliable thing. We determine which folders to check
  * for images based on folders that exist both in templates/ and img/mastheads/
  * So if both templates/sbs and img/mastheads/sbs exist, img/masthead/sbs/img.jpg
  * will be added to the list to push. This isn't reliable because we may have a template,
  * ie, templates/sbsa that pulls its masthead from img/masthead/sbs ... since we
  * don't have a 1-to-1 mapping on templates/ - mastheads/ dirs, this won't
  * always work.
  *
  */


 'use strict';


var fs = require('fs')
  , _ = require('lodash')
  , Connection = require('ssh2')
  , colors = require('colors')
  , path = require('path')
  , cli = require('command-line-args')
  , blessed = require('blessed')
  ;


/*
 * Accepted arguments.
 * action: 'up' to upload, 'down' to remove
 * env: environment to upload to. stg | prod
 * mastheads: attempt to upload mastheads too? t|f
 * pages: pages to push.
 */
var args = cli([
  // Action: 'up' or 'down' (down == delete)
  {name: 'action', alias: 'a', type: String, defaultValue: 'up'},

  // env: 'stg' or 'prod'
  {name: 'env', alias: 'e', type: String, defaultValue: 'stg', required: true},

  // images: attempt to push mastheads. true|false
  {name: 'mastheads', alias: 'm', type: Boolean},

  // images: attempt to push mastheads. true|false
  {name: 'templates', alias: 't', type: String, multiple: true, defaultValue: []},

  // files: 'colorbii', etc. Pass these as last arguments
  {name: 'pages', alias: 'p', type: String, multiple: true, defaultOption: true}
]).parse()


// Only argument required are the pages to upload.
if (typeof args.pages == 'undefined') throw '** Must specify pages to upload';


// Needs a private key
if (!fs.existsSync(process.env.HOME + '/.ssh/id_rsa')) throw '** Needs private key in ~/.ssh/id_rsa'.red


// todo: make this a module? this is huge.
class Terminal {

  constructor() {
    this.screen = blessed.screen({
      smartCSR: true,
      title: 'swag..?'
    });

    this.mainbox = blessed.box({
      left: 0,
      top: 0,
      width: this.screen.width,
      height: this.screen.height,
    })

    // Top part of the screen
    this.title = blessed.box({
      top: 0,
      left: 'center',
      width: '100%',
      height: 'shrink',
      tags: true,
      content: `Pushing to {bold}${args.env}!{/bold}`,
      style: {
        fg: 'white',
        bg: function() {return args.env == 'stg' ? 'blue' : 'magenta'}()
      }
    })

    // Shows log messages about success/error pushes.
    this.log = blessed.log({
      top: 2,
      height: '100%-5',
      mouse: true,
      tags: true,
      scrollbar: {
        track: {
          bg: 'black',
          fg: 'cyan'
        },
        style: {
          bg: 'red'
        }
      }
    })

    this.deleteForm = blessed.Form({
      keys: true,
      left: 'center',
      top: 'center',
      width: '60%',
      shrink: true,
      bg: 'white',
      fg: 'black',
      content: `Delete from ${args.env}?`,
      padding: {
        left: 2,
        right: 2,
        top: 1,
        bottom: 1
      }
    })

    this.deleteCancel = blessed.button({
      parent: this.deleteForm,
      mouse: true,
      keys: true,
      shrink: true,
      right: 2,
      width: '30%',
      top: 2,
      name: 'cancel',
      content: 'Cancel',
      style: {
        bg: 'yellow',
        fg: 'black'
      },
      padding: {
        left: 1,
        right: 1,
        top: 1,
        bottom: 1
      },

    })

    this.deleteOK = blessed.button({
      parent: this.deleteForm,
      mouse: true,
      keys: true,
      shrink: true,
      left: 2,
      top: 2,
      width: '30%',
      tags: true,
      name: 'ok',
      content: ' Yes',
      style: {
        bg: 'yellow',
        fg: 'black',
        hover: { bg: 'red' },
        focus: { bg: 'green'}
      },
      padding: {
        left: 1,
        right: 1,
        top: 1,
        bottom: 1
      }
    })

    // Progress bar on bottom
    this.progressbar = blessed.ProgressBar({
      bottom: 0,
      left: 0,
      height: 'shrink',
      width: '100%',
      filled: 1,
      content: '',
      border: {
        type: 'line'
      },
      style: {
        bg: 'black',
        bar: {
          bg: 'green',
          fg: 'black'
        },
      }
    })

    // A floating box displaying some status/info to user.
    this.statusBox = blessed.box({
      top: '50%-4',
      left: 'center',
      height: 'shrink',
      padding: 1,
      tags: true,
      content: '{bold}Creating directories...{/bold}',
      style: {
        fg: 'cyan',
        bg: 'black'
      },
      border: {
        type: 'line'
      }
    })


    this.screen.key(['escape', 'q', 'C-c'], function(ch, key) {
      return process.exit(0);
    });

  }


  /**
   * Build layout for when we are uploading files.
   * Typically a title bar on top, with log in the
   * middle, and progress bar on bottom
   */
  uploadLayout() {
    this.screen.append(this.mainbox)
    this.mainbox.append(this.title)
    this.mainbox.append(this.log)
    this.mainbox.append(this.progressbar)
    this.screen.render()
  }

  /**
   * Layout for delete form. Shown when
   * deleting pages
   */
  deleteLayout() {
    this.screen.append(this.mainbox)
    this.mainbox.append(this.log)
    this.mainbox.append(this.deleteForm)
    this.screen.render()
  }


  /**
   * Updates upload progress. So it updates BOTH
   * ProgressBar and Log!!. then rerenders the screen
   * @param alue {number} - number to increment progress bar by
   * @param msg {string} - message to append to log window
   * @innerValue {string} - msg to show inside bar (ie, 7/100)
  */
  updateProgress(innerValue, value, logmsg) {
    this.progressbar.progress(value)
    this.progressbar.setContent(innerValue)
    this.log.add(logmsg)
    this.screen.render()
  }

  /**
   * A floating box in the middle of the screen that displays a msg.
   * the scritp starts creating directories on remote server.\
   * If kill is true, kill the box.
   * @param msg {string}: message to display in the box
   * @param kill {bool}: if true, hide the box
   */
  toggleStatusBox(msg, kill) {
    if (kill) {
      this.statusBox.hide()
    } else {
      this.statusBox.show()
      this.statusBox.setContent(msg)
      this.mainbox.append(this.statusBox)
    }
    this.screen.render()
  }


}


/*
 * Class to hold our settings.
 * For now, only holds connection settings.
 */
class Config {
  constructor(env) {

    this.stg = {
      host: '10.130.250.237',
      user: 'lpstagingadmin',
      deployPath: '/mnt/landingpagestg/autolandingpages/us'
    }

    this.prod = {
      host: '10.130.250.237',
      user: 'lpprodadmin',
      deployPath: '/mnt/landingpageprod/autolandingpages/us'
      /*host: '10.130.250.237',
      user: 'lpstagingadmin',
      deployPath: '/mnt/landingpagestg/autolandingpages/us'*/
    }

    this.env = env;

    // Max number of SSH2 simultaneous connections
    this.max_connections = 9;

    // So we can do config.foo isntead of config[args.env].foo
    this.deployPath = this[env].deployPath

    // SSH2 connection settings
    this.ssh2 = {
      host: this[env].host,
      port: 22,
      username: this[env].user,
      password: this[env].password,
      readyTimeout: 15000,
      keepaliveInterval: 0,
      privateKey: fs.readFileSync(process.env.HOME + '/.ssh/id_rsa'),
    }
  }
}




// Error codes for SSH2 -- not even being used right now
var errors = {
  ENETUNREACH: 'Connection was refused. Check your internet connection.',
  ENOTFOUND: 'Remote address not found.',
  ECONNRESET: 'Connection dropped or reset.'
}


// Instantiate config
var config = new Config(args.env)

// Templates directories
var templates = fs.readdirSync('../templates/')

// Pages array - (sitewide, redbbi, redbdi, etc)
var pages = _.intersection(fs.readdirSync('../deploy/sbsr/'), args.pages);


// If a page in the args isn't found, abort... can't let this pass silently
var fourOhfours = _.difference(args.pages, pages);

if (args.action == 'up' && fourOhfours.length) throw `*** Below pages not found:\n${fourOhfours}`.red


// Let's filter our templates to the ones we passed in the cli
if (args.templates.length) {

  // Quit if a template passed doesn't exist.
  var diff = _.difference(args.templates, templates)
  if (diff.length)
    throw `** Following template(s) not found:\n${diff}`.red

  templates = _.without(args.templates, templates)
}




var toDeploy = {

  // If -i was passed in arguments, we'll store mastheads uris here
  mastheads: [],

  /*
   * Holds all our local paths. We will send every file in here to the
   * remote server. While doing so, we pass each string through the
   * remotify() fn to convert the path from local to remote, like so
   * ../deploy/sbs/sitewide/index.html -> ashcclp/prod/sbs/sitewide/index.html
   */
  files: []
}



/*
 * Upload mastheads
 * 1. Grab all masthead images name from the pages we passed in args.
 * 2. Get all our mastheads dirs from globals/img/mastheads/
 * 3. Create all masthead dirs  in remote globals/img/mastheads/
 * 4. Push img from local to remote.
 * PS: if image exists, it'll be overwritten
 * PSS: Image is assumed to exist in all masteahds/* folders. You'll
 * get a yellow warning in the log otherwise
 * PSSS: Mastheads aren't critical and as noted above, iffy. So we
 * don't count them as part of the  progressbar
 */
if (args.mastheads && args.action == 'up') {
  var json = JSON.parse(fs.readFileSync('../data/pages.json', {encoding: 'utf8'}))

  var mastheads = pages
    .filter(page => !page.match(/js|assets|css/))
    .map(page => json[page].header.mastheadImage)

  // We only wants dirs that are both in templates/ and img/mastheads/
  var mastheadDirs = _.intersection(
    templates,
    fs.readdirSync('../deploy/globals/img/mastheads/')
  );

   // Array with all mastheads uris
  toDeploy.mastheads = mastheads.map((masthead, i) => {
    return mastheadDirs.map(dir => {
      return  `../deploy/globals/img/mastheads/${dir}/${masthead}`
    })
  })[0]

  // Create remote/mastheads/<dir> if needed
  var connection = new Connection()
  connection.connect(config.ssh2)
  connection.on('ready', () =>  createRemoteDirs(connection, toDeploy.mastheads))

  // Using close signal as assumption dirs got created
  connection.on('close', () => {

    // Open a new connection and push images
    var conn = new Connection()
    conn.connect(config.ssh2)

    conn.on('ready', (err, sftp) => {
      conn.sftp((err, sftp) => {
         toDeploy.mastheads.forEach(function(img, i) {
          sftp.fastPut(img, remotify(img), (err) => {
            var logMessage = `{yellow-fg}✔ ${img}{/yellow-bg}`
            if (err) logMessage = `{red-fg}{bold}✕ ${img}{/bold}{/red-bg}\n${err}`
            terminal.updateProgress(0, 0, logMessage)
          })
        })
      })

    })
  })

}




// All combinations of ../deploy/template/page/file.html paths.
// TODO use list comprehension when harmony flag arrives
for (var template of templates) {
  for (var page of pages) {
    let uri = `../deploy/${template}/${page}`
    try {
      fs.readdirSync(uri).forEach(file => toDeploy.files.push(`${uri}/${file}`))
    } catch (e) {
      throw `
      Deploy dirs are not in sync. run rsifast and try again.
      This error means some deploy/<template> has folders
      that some other template(s) dont, when in theory, all
      folders inside deploy should have the same pages.
      ie, if deploy/sbs/foobar exists, but deploy/newlang/foobar does not,
      this error happens
      `.red
    }
  }
}


/*
 * Appends model paths to toDeploy.files array.
 * Models are stored in, ie,  ../rsi/sitewide/models.js,
 * however, we'll store them in the server as
 * globals/models/sitewide.js
 */
var models = _.intersection(fs.readdirSync('../rsi'), args.pages)
models.every(model => toDeploy.files.push(`../deploy/globals/models/${model}.js`))



/**
 * Handles uploading of files (excluding mastheads)
 * to remote server.
 */
function up() {

  /*
   * Creates necessary directories on the remote server,
   * so we can push files to them.
   */
  var connection = new Connection()

  connection.connect(config.ssh2)

  connection.on('ready', (sftp) => createRemoteDirs(connection, toDeploy.files))

  // We'll use 'close' signal as a callback that createRemoteDirs is done.
  connection.on('close', () => deploy())

  /**
   * The main part of this script.
   * Deploy our files. First get each connection ready, then push.
   * Each connection opened will go through the toDeploy.files
   * array, removing and the first file on the stack and pushing it.
   * Each connection will keep doing this until files array exhausts.
   * Savage mode:  [x]on  []off
   */
  function deploy() {

    // Holds SSH2 connection objects. TODO: list comprehension
    let connections = []

    // We need this counter to know when all connections are closed
    let openConnections = 0

    for (let i = 0; i < config.max_connections; i++) {
      connections.push(new Connection())
    }

    terminal.toggleStatusBox(`creating ${config.max_connections} connections..`)

    // the whole purpose of 'i' is to debug
    connections.forEach((connection, i) => {
      connection.on('ready', () => {
        ++openConnections
        connection.sftp((err, sftp) => upload(connection, sftp,i))

        // remove floating message box,
        if (i == config.max_connections-1) terminal.toggleStatusBox(null, true)
      }).connect(config.ssh2)
    })



    // Total number of files to push and total number of files pushed. Used in progressbar
    var totalFiles = toDeploy.files.length
    var totalPushed = 0

    function upload(conn, sftp, i) {

      // Progressbar values
      var barValue = `${totalPushed}/${totalFiles}`
      var barProgress = (100/totalFiles)

      if (toDeploy.files.length) {

        var file = toDeploy.files.shift()

        // fake an error, dev purposes.
        //if (i == 3) file = file+'z'

        sftp.fastPut(file, remotify(file), (err) => {

          // Rename the file for log purposes. Remove '../deploy'
          file = file.slice(10)

          var logMessage;
          if (err) {
            logMessage = `{red-fg}{bold}✕ ${file}{/bold}{/red-bg}\n${err}`
          } else {
            ++totalPushed
            var logMessage = `{green-fg}✔ ${file}{/green-bg}`
          }

          //var barValue = `${totalPushed}/${totalFiles}`
          terminal.updateProgress(barValue, barProgress, logMessage)
          upload(conn, sftp,i)
        })
      } else {

        conn.end()
        --openConnections

        /*
         * Done with all pushing operations. Insert into the log a message to let
         * the user know we're done, and notify if there were errors, ie, some
         * files didn't make it to their destination
         */
        if (!openConnections) {
          var completedMessage = '{bold}-- Completed.{/bold}'

          if (totalPushed !== totalFiles)
            completedMessage += '\n{red-fg}-- Some files didn\'t make it.{/red-fg}'

          terminal.updateProgress(barValue, barProgress, completedMessage)
        }
      }
    }
  }
}




// Instantiate & initiate our blessed terminal
var terminal = new Terminal()

if (args.action == 'up') {
  terminal.uploadLayout()
  up()
}

if (args.action == 'down') {
  terminal.deleteLayout()
  terminal.deleteOK.on('press', () =>  down())
  terminal.deleteCancel.on('press', () => process.exit(0))
  terminal.screen.render()
}



/**
 * This is where we remove things from the remote server.
 * We delete the whole folder, as opposed to just the index
 * file inside.
 */
function down() {
  terminal.deleteForm.destroy()
  terminal.screen.render()


  terminal.updateProgress(0, 0, '{red-fg}{bold}deleting....{/bold}{red-fg}')

  // Yes, _.unique is needed. don't remove it
  // if we have ../deploy/sbs/index.html and ../sbs/foo.json
  // in toDeploy.files, we would end up having ../sbs/ listed here twice.
  var cmd = _.unique(toDeploy.files.map(uri => `'${path.dirname(uri)}'`))

  // Show all uris about to be deleted
  cmd.forEach(uri => terminal.updateProgress(0, 0, `{yellow-fg}{bold}${remotify(uri)}{/bold}{/yellow-bg}`))

  cmd = remotify(`rm -rfv ${cmd.join(' ')}`)



  var connection = new Connection()

  connection.on('ready', function() {
    connection.exec(cmd, function(err, stream) {
      if (err) throw 'ERROR! could not execute delete command'.red

      stream.on('data', function(data) {
        terminal.updateProgress(0, 0, data)
      })

      stream.on('close', function(code, signal) {
        terminal.updateProgress(0, 0, '-- done --')
      })

      stream.stderr.on('data', function(data) {
         terminal.updateProgress(0, 0, `{red-fg}data{/red-fg}`)
      })
    })
  }).connect(config.ssh2)
}



/**
 * Before we start pushing all files, we have to create the directories
 * trees in the remote server first. So open a new connection,
 * and do that.
 * @param connection {connection object}: a SSH2 connection object
 * @param dirsArray {array}: array of local paths to check & create
 * TODO possible bug - if the paths in array are actual directories,
 * then dirname() may go back one dir
 */
function createRemoteDirs(connection, dirsArray) {
  terminal.toggleStatusBox('Creating dirs..')

  // For each of our files in dirsArray, get the dirname().
  // so ../deploy/sbs/index.html becomes ../deploy/sbs
  let dirs =  dirsArray.map(uri => `'${path.dirname(uri)}'`)

  // Make sure globals/models/ exists too
  dirs.push('../deploy/globals/models/')

  let cmd = remotify(`mkdir -p ${dirs.join(' ')}`)

  connection.exec(cmd, (e, stream) => {
    if (e) throw errors[e].red

    terminal.toggleStatusBox(null,true)
    connection.end()
  })

}



/**
 * Converts local uris into remote ones. basically replaces
 * ../deploy/ with server's deploy path
 */
function remotify(uri) {
  return uri.replace(/\.\.\/deploy/gi, config.deployPath)
}



//todo sounds & crescendos