├── docs ├── screens.png └── password-screen2.png ├── public ├── images │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── loading.gif │ ├── backup-screen.png │ ├── password-screen.png │ └── favicon.ico ├── powershell │ ├── removeBackup.ps1 │ ├── restoreBackup.ps1 │ ├── createBackup.ps1 │ └── updater.ps1 ├── css │ └── style.css └── scripts │ └── built.js ├── views ├── error.handlebars ├── layouts │ └── main.handlebars └── index.handlebars ├── config.js ├── .gitignore ├── updater ├── errors.js ├── index.js ├── backup.js └── filesfolders.js ├── updater_client ├── app.js ├── config.js ├── utils.js ├── validation.js ├── updater.js └── backup.js ├── license.md ├── package.json ├── app.js ├── index.html ├── readme.md ├── .jshintrc ├── gruntfile.js └── Ghost-Updater-Azure.njsproj /docs/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/docs/screens.png -------------------------------------------------------------------------------- /public/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/icon.icns -------------------------------------------------------------------------------- /public/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/icon.ico -------------------------------------------------------------------------------- /public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/icon.png -------------------------------------------------------------------------------- /views/error.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Ghost Updater: Error

3 |

{{error}}

4 |
-------------------------------------------------------------------------------- /docs/password-screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/docs/password-screen2.png -------------------------------------------------------------------------------- /public/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/loading.gif -------------------------------------------------------------------------------- /public/images/backup-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/backup-screen.png -------------------------------------------------------------------------------- /public/images/password-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/HEAD/public/images/password-screen.png -------------------------------------------------------------------------------- /public/powershell/removeBackup.ps1: -------------------------------------------------------------------------------- 1 | # Remove full site backup 2 | "Removing Full Site Backup" 3 | 4 | cd "D:\home\site\" 5 | If (Test-Path ./wwwroot-backup/){ 6 | Remove-Item -Path ./wwwroot-backup -Recurse 7 | } 8 | "All done" -------------------------------------------------------------------------------- /public/powershell/restoreBackup.ps1: -------------------------------------------------------------------------------- 1 | # Restore full site backup 2 | "Restoring Full Site Backup" 3 | 4 | cd "D:\home\site\" 5 | If (Test-Path ./wwwroot-backup/) { 6 | Remove-Item -Path ./wwwroot -Recurse 7 | Rename-Item D:\home\site\wwwroot-backup wwwroot 8 | } 9 | Else { 10 | "WARNING No backup found" 11 | } 12 | "All done" -------------------------------------------------------------------------------- /views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ghost Updater for Azure Websites 6 | 7 | 8 | 9 | 10 | 11 | {{{body}}} 12 | 13 | 14 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | website: process.env.website || '', 3 | user: process.env.user || '', 4 | password: process.env.password || '', 5 | latestGhost: process.env.latestGhost || '', 6 | zippath: './ghost.zip', 7 | standalone: true 8 | } 9 | 10 | config.auth = function () { 11 | return { 12 | 'user': config.username, 13 | 'pass': config.password 14 | } 15 | } 16 | 17 | module.exports = config; -------------------------------------------------------------------------------- /public/powershell/createBackup.ps1: -------------------------------------------------------------------------------- 1 | # Create full site backup 2 | 3 | cd "D:\home\site\" 4 | If (Test-Path ./wwwroot-backup/){ 5 | "Removing old backup" 6 | Remove-Item -Path ./wwwroot-backup -Recurse 7 | } 8 | 9 | "Creating Full Site Backup" 10 | Copy-Item D:\home\site\wwwroot -Destination D:\home\site\wwwroot-backup -Recurse 11 | If (Test-Path ./wwwroot-backup/){ 12 | "All done" 13 | } 14 | Else { 15 | "WARNING Backup not created" 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Builds 6 | builds 7 | cache 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript -------------------------------------------------------------------------------- /updater/errors.js: -------------------------------------------------------------------------------- 1 | function errorHandlers(app) { 2 | // catch 404 and forward to error handler 3 | app.use(function(req, res, next) { 4 | var err = new Error('Not Found'); 5 | err.status = 404; 6 | next(err); 7 | }); 8 | 9 | // development error handler 10 | // will print stacktrace 11 | if (app.get('env') === 'development') { 12 | app.use(function(err, req, res) { 13 | res.status(err.status || 500); 14 | res.render('error', { 15 | message: err.message, 16 | error: err 17 | }); 18 | }); 19 | } 20 | 21 | // production error handler 22 | // no stacktraces leaked to user 23 | app.use(function(err, req, res) { 24 | res.status(err.status || 500); 25 | res.render('error', { 26 | message: err.message, 27 | error: {} 28 | }); 29 | }); 30 | } 31 | 32 | module.exports = errorHandlers; -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- 1 |  ((  0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @ -------------------------------------------------------------------------------- /updater_client/app.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.init = function () { 4 | UpdaterClient.config.getConfig(); 5 | 6 | // Wire up buttons to actions 7 | $('input').bind('input', UpdaterClient.validation.validateConfig); 8 | $('#ghost-zip').click(function () { 9 | $('.ghostPackageLoader').show(); 10 | }); 11 | $('#ghost-zip').change(function () { 12 | $(this).attr('disabled', false); 13 | $('.ghostPackageLoader').hide(); 14 | }); 15 | 16 | // TODO: Defining actions and handlers here is okay, but feels dirty. 17 | // This allows us to define actions with the data-action attribute. 18 | $('body').on('click', '[data-action]', function() { 19 | var action = $(this).data('action'), 20 | split = (action) ? action.split('.') : null, 21 | fn = window; 22 | 23 | for (var i = 0; i < split.length; i++) { 24 | fn = (fn) ? fn[split[i]] : null; 25 | } 26 | 27 | if (typeof fn === 'function') { 28 | fn.apply(null, arguments); 29 | } 30 | }); 31 | 32 | $('#config').fadeIn(900); 33 | }; -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Felix Rieseberg 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ghost-Updater-Azure", 3 | "version": "0.6.1", 4 | "private": false, 5 | "main": "index.html", 6 | "maintainers": [ 7 | { 8 | "name": "Felix Rieseberg", 9 | "email": "felix.rieseberg@mirosoft.com", 10 | "web": "http://www.felixrieseberg.com" 11 | } 12 | ], 13 | "bugs": "https://github.com/felixrieseberg/Ghost-Updater-Azure/issues", 14 | "scripts": { 15 | "start": "node ./app.js" 16 | }, 17 | "dependencies": { 18 | "bluebird": "^2.3.4", 19 | "body-parser": "~1.8.1", 20 | "cookie-parser": "~1.3.3", 21 | "debug": "^2.0.0", 22 | "express": "~4.9.0", 23 | "morgan": "~1.3.0", 24 | "request": "^2.44.0", 25 | "request-enhanced": "^0.1.1", 26 | "serve-favicon": "~2.1.3", 27 | "express-handlebars": "^1.1.0", 28 | "underscore": "^1.7.0" 29 | }, 30 | "devDependencies": { 31 | "grunt": "^0.4.5", 32 | "grunt-concurrent": "^1.0.0", 33 | "grunt-contrib-concat": "^0.5.0", 34 | "grunt-contrib-jshint": "^0.10.0", 35 | "grunt-contrib-watch": "^0.6.1", 36 | "grunt-nodemon": "^0.3.0", 37 | "grunt-nw-builder": "^2.0.0", 38 | "jshint": "^2.9.1" 39 | }, 40 | "window": { 41 | "title": "Ghost Updater for Azure Websites", 42 | "icon": "/public/images/icon.png", 43 | "toolbar": false, 44 | "frame": true, 45 | "width": 880, 46 | "height": 680, 47 | "resizable": false, 48 | "position": "center" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/felixrieseberg/Ghost-Updater-Azure.git" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | exphbs = require('express-handlebars'), 3 | path = require('path'), 4 | favicon = require('serve-favicon'), 5 | logger = require('morgan'), 6 | cookieParser = require('cookie-parser'), 7 | bodyParser = require('body-parser'), 8 | debug = require('debug')('Ghost-Updater-Azure'), 9 | 10 | config = require('./config'), 11 | updater = require('./updater'), 12 | backup = require('./updater/backup'); 13 | 14 | var app = express(), errorHandlers; 15 | 16 | // view engine setup 17 | app.set('views', path.join(__dirname, 'views')); 18 | app.engine('handlebars', exphbs({defaultLayout: 'main'})); 19 | app.set('view engine', 'handlebars'); 20 | app.set('port', process.env.PORT || 3000); 21 | 22 | app.use(favicon(__dirname + '/public/images/favicon.ico')); 23 | app.use(logger('dev')); 24 | app.use(bodyParser.json()); 25 | app.use(bodyParser.urlencoded({ extended: false })); 26 | app.use(cookieParser()); 27 | app.use(express.static(__dirname + '/public')); 28 | app.get('/nw', function (req, res) { 29 | res.json({ isNodeWebkit: config.standalone }); 30 | }); 31 | app.use('/updater', updater); 32 | app.use('/backup', backup); 33 | 34 | app.get('/', function (req, res) { 35 | res.render('index'); 36 | }); 37 | 38 | errorHandlers = require('./updater/errors')(app); 39 | 40 | var server = app.listen(app.get('port'), function() { 41 | debug('Express server listening on port ' + server.address().port); 42 | }); 43 | 44 | module.exports = app; 45 | -------------------------------------------------------------------------------- /updater_client/config.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.config = { 4 | url: '', 5 | username: '', 6 | password: '', 7 | zippath: '', 8 | standalone: undefined, 9 | backup: false, 10 | 11 | /** 12 | * Takes the config entered by the user and hits the router configuration 13 | * endpoint, essentially telling the Node part of this app what the 14 | * configuration is. 15 | */ 16 | setConfig: function () { 17 | if (UpdaterClient.validation.validateConfig('default')) { 18 | $.ajax({ 19 | url: '/updater/config', 20 | data: { 21 | url: UpdaterClient.config.url, 22 | username: UpdaterClient.config.username, 23 | password: UpdaterClient.config.password, 24 | zippath: UpdaterClient.config.zippath 25 | } 26 | }) 27 | .done(function(response) { 28 | console.log(response); 29 | UpdaterClient.utils.switchPanel('#backupdisclaimer'); 30 | }); 31 | } 32 | }, 33 | 34 | /** 35 | * Ensures that we're running in NW.js - and show's the file 36 | * upload option, if that's the case 37 | * TODO: This seemed smart in the beginning, but pointless now. 38 | * We're always running as an app. 39 | */ 40 | getConfig: function () { 41 | $.ajax('/nw').done(function (response) { 42 | console.log(response); 43 | if (response.isNodeWebkit) { 44 | UpdaterClient.config.standalone = true; 45 | $('#ghost-zip-container').show(); 46 | } 47 | }); 48 | } 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 50 | 51 | 52 |
53 | 54 | -------------------------------------------------------------------------------- /public/powershell/updater.ps1: -------------------------------------------------------------------------------- 1 | # Unzip uploaded ghost.zip 2 | "Unzipping Ghost to /site/temp/latest" 3 | cd "D:\home\site\temp" 4 | If (Test-Path ./latest/){ 5 | Remove-Item -Path ./latest/* -Recurse 6 | } 7 | unzip -d ./latest/ ghost.zip 8 | 9 | # Delete index.js, package.json, ./core and ./content/themes/casper/* 10 | cd "D:\home\site\wwwroot" 11 | "Removing core" 12 | Remove-Item -Path ./core/* -Recurse 13 | "Removing Caspter theme" 14 | Remove-Item -Path ./content/themes/casper/* -Recurse 15 | "Removing index.js" 16 | Remove-Item -Path index.js 17 | "Removing package.json" 18 | Remove-Item -Path package.json 19 | 20 | # Move index.js, package.json, ./core and ./content/themes/casper/* 21 | cd "D:\home\site\temp\latest" 22 | "Moving core" 23 | Move-Item -Path ./core/* -Destination D:\home\site\wwwroot\core\ 24 | "Moving Casper" 25 | Move-Item -Path ./content/themes/casper/* -Destination D:\home\site\wwwroot\content\themes\casper\ 26 | "Moving index.js" 27 | Move-Item -Path ./index.js -Destination D:\home\site\wwwroot\ 28 | "Moving package.json" 29 | Move-Item -Path ./package.json -Destination D:\home\site\wwwroot\ 30 | "Creating required elements" 31 | New-Item -ItemType directory -Path D:\home\site\wwwroot\content\apps -ErrorAction SilentlyContinue 32 | 33 | # Cleanup NPM modules 34 | cd "D:\home\site\wwwroot" 35 | "Running npm install (production)" 36 | Stop-Process -processname node 37 | npm install --production 38 | 39 | # Install node-sqlite3 bindings for both the 32 and 64-bit Windows architecture. 40 | # node-sqlite3 will build the bindings using the system architecture and version of node that you're running the install 41 | 42 | # Force install of the 32-bit version, then move the lib to temporary location 43 | Write-Output "Installing SQLite3 x32 Module" 44 | & npm install sqlite3 --target_arch=ia32 45 | Move-Item ".\node_modules\sqlite3\lib\binding\node-v11-win32-ia32\" -Destination ".\temp" 46 | 47 | # Force install of the 64-bit version, then copy 32-bit back 48 | Write-Output "Installing SQLite3 x64 Module" 49 | & npm install sqlite3 --target_arch=x64 50 | Move-Item ".\temp" -Destination ".\node_modules\sqlite3\lib\binding\node-v11-win32-ia32\" 51 | 52 | # Cleanup 53 | "We're done, cleaning up!" 54 | cd "D:\home\site\temp\" 55 | "Removing temp Ghost folder" 56 | Remove-Item -Path ./latest -Recurse 57 | "Removing temp Ghost zip" 58 | Remove-Item -Path ./ghost.zip 59 | 60 | "All done" -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Ghost Updater for Microsoft Azure 2 | Microsoft Azure allows a one-click installation of the popular blogging platform Ghost, but there's currently no integrated update option. This desktop app automatically upgrades Ghost running on Azure Web Apps (formerly known as Azure Websites) in a few clicks. It is being maintained by members of the Microsoft DX team with :heart: for Ghost! 3 | 4 | ***Latest version known to update without errors: 0.8.0*** 5 | 6 | ![](https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/master/docs/screens.png) 7 | 8 | ### Download 9 | - [Windows](https://github.com/felixrieseberg/Ghost-Updater-Azure/releases/download/v0.6.1/GhostUpdater-0.6.1-win.zip) - [Mac OS X](https://github.com/felixrieseberg/Ghost-Updater-Azure/releases/download/v0.6.1/GhostUpdater-0.6.1-osx.dmg) - [Linux](https://github.com/felixrieseberg/Ghost-Updater-Azure/releases/download/v0.6.1/GhostUpdater-0.6.1-linux.zip) 10 | 11 | ### How To Update your Ghost Blog 12 | Good news: It's really simple. You only need two things: The [latest version of Ghost as a zip package](https://ghost.org/download) and your deployment credentials for your website. Those credentials are _not_ the user/password pair used to log into the Azure Management Portal, so let's quickly talk about how to get them: 13 | 14 | - Go to your website's dashboard in the Azure Management Portal. 15 | - Under *Quick Links* on the right, you'll see *Reset Deployment Credentials*. If you're using the new portal, you'll find a *Set Deployment Credentials* button in the dashboard right in the *Deployment* section. 16 | 17 | ![](https://raw.githubusercontent.com/felixrieseberg/Ghost-Updater-Azure/master/docs/password-screen2.png) 18 | 19 | > Please ensure that your Web App / Website has enough resources for the update. Should the update stall or abort while running, increase the available resources for your Web App (for instance by temporarily moving it to a bigger instance) and run the updater again. 20 | 21 | ### Support 22 | If you run into any issues, please go and report them in the [issue section of the GitHub repository](https://github.com/felixrieseberg/Ghost-Updater-Azure/issues). 23 | 24 | We at Microsoft love Ghost, which is why we release this code. It is not an official Microsoft product - there is no warranty of any kind. Please see License.md if you have any questions. 25 | 26 | ### License 27 | The MIT License (MIT), Copyright (c) 2014 Felix Rieseberg & Microsoft Corporation. Please see License.md for details. 28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jasmine": false, 4 | "spyOn": false, 5 | "it": false, 6 | "console": false, 7 | "describe": false, 8 | "expect": false, 9 | "beforeEach": false, 10 | "waits": false, 11 | "waitsFor": false, 12 | "runs": false, 13 | "Promise": true, 14 | "$": false, 15 | "jQuery": false 16 | }, 17 | 18 | "node" : true, 19 | "browser" : true, 20 | "jquery" : true, 21 | 22 | // Enforcing 23 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 24 | "camelcase" : false, // true: Identifiers must be in camelCase 25 | "curly" : true, // true: Require {} for every new block or scope 26 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 27 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 28 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 29 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 30 | "indent" : 4, // {int} Number of spaces to use for indentation 31 | "latedef" : false, // true: Require variables/functions to be defined before being used 32 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 33 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 34 | "noempty" : true, // true: Prohibit use of empty blocks 35 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 36 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 37 | "plusplus" : false, // true: Prohibit use of `++` & `--` 38 | "quotmark" : "single", // Quotation mark consistency: 39 | // false : do nothing (default) 40 | // true : ensure whatever is used is consistent 41 | // "single" : require single quotes 42 | // "double" : require double quotes 43 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 44 | "unused" : true, // true: Require all defined variables be used 45 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 46 | "maxparams" : false, // {int} Max number of formal params allowed per function 47 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 48 | "maxstatements" : false, // {int} Max number statements per function 49 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 50 | "maxlen" : false // {int} Max number of characters per line 51 | } -------------------------------------------------------------------------------- /updater_client/utils.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.utils = { 4 | 5 | /** 6 | * Switch the different 'panels' the app. Poor man's SPA. 7 | * @param {object} input - Input object with target information 8 | */ 9 | switchPanel: function (input) { 10 | var panel = (input.target) ? input.target.dataset.target : input; 11 | $('.wrapper').hide(); 12 | $(panel).show(); 13 | }, 14 | 15 | /** 16 | * Append text to the log element in the DOM. 17 | * @param {string} text - The text to append 18 | * @param {boolean} loading - Are we loading? 19 | * @param {boolean} error - Is this an error? 20 | * @param {element|string} target - The target object 21 | * @return {$.append} 22 | */ 23 | appendLog: function (text, loading, error, target) { 24 | var loader = '', 25 | errorText = (error) ? 'Error: ' : ''; 26 | 27 | if ($('#loading')) { 28 | $('#loading').remove(); 29 | } 30 | 31 | loader = (loading) ? ' ' : ''; 32 | return $(target).append('

' + errorText + text + loader + '

'); 33 | }, 34 | 35 | /** 36 | * A button that indicates how long ago we've last had contact to Kudu and the 37 | * Azure Web App. This is useful because we have virtually no way of telling 38 | * if something went horribly wrong - ie connection lost, server down, datacenter 39 | * on fire, etc. 40 | * @param {string} color - The color the button should be (red/yellow/grey/green) 41 | */ 42 | timerButton: function (color) { 43 | var timerCircle = $('.circle'), 44 | timerTooltip = $('.circle > span'), 45 | textKeepTrying = '\nThis tool will keep trying to reach the website.', 46 | textRed = 'We have not heard back from the websites within the last five minutes, which can indicate a problem.' + textKeepTrying, 47 | textYellow = 'We have not heard back from the website within the last two minutes.' + textKeepTrying, 48 | textGrey = 'The connection status to your Azure Website is currently unknown.', 49 | textGreen = 'We are connected to your Azure Website.'; 50 | 51 | switch (color) { 52 | case 'red': 53 | timerCircle.css('background-color', '#e55b5b'); 54 | timerTooltip.text(textRed); 55 | break; 56 | case 'yellow': 57 | timerCircle.css('background-color', '#ffe811'); 58 | timerTooltip.text(textYellow); 59 | break; 60 | case 'grey': 61 | timerCircle.css('background-color', '#7f7f7f'); 62 | timerTooltip.text(textGrey); 63 | break; 64 | case 'green': 65 | timerCircle.css('background-color', '#799a2e'); 66 | timerTooltip.text(textGreen); 67 | break; 68 | default: 69 | break; 70 | } 71 | } 72 | 73 | }; -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | 4 | pkg: grunt.file.readJSON('package.json'), 5 | 6 | jshint: { 7 | all: [ 'Gruntfile.js', 'updater_client/**/*.js', 'app.js', 'updater/**/*.js'], 8 | options: { 9 | jshintrc: true 10 | } 11 | }, 12 | 13 | concurrent: { 14 | dev: { 15 | tasks: ['nodemon', 'watch'], 16 | options: { 17 | logConcurrentOutput: true 18 | } 19 | } 20 | }, 21 | 22 | concat: { 23 | options: { 24 | separator: '', 25 | stripBanners: true, 26 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + 27 | '<%= grunt.template.today("yyyy-mm-dd") %> */' 28 | }, 29 | dist: { 30 | src: ['updater_client/*.js'], 31 | dest: 'public/scripts/built.js', 32 | }, 33 | }, 34 | 35 | watch: { 36 | all: { 37 | files: ['updater_client/**/*.js', 'updater/**/**.js'], 38 | tasks: [ 'concat' ] 39 | } 40 | }, 41 | 42 | nodemon: { 43 | dev: { 44 | script: 'app.js' 45 | } 46 | }, 47 | 48 | nwjs: { 49 | win: { 50 | options: { 51 | name: 'Ghost Updater for Azure', 52 | platforms: ['win'], 53 | buildDir: './builds', 54 | winIco: './public/images/icon.ico', 55 | version: '0.12.2' 56 | }, 57 | src: ['public/**/*', 'node_modules/**/*', '!node_modules/grunt**/**', 'updater/**/*', 'updater_client/**/*', 'views/**/*', '*.js', '*.html', '*.json'] // Your node-webkit app 58 | }, 59 | unix: { 60 | options: { 61 | name: 'Ghost Updater for Azure', 62 | platforms: ['osx', 'linux32'], 63 | buildDir: './builds', 64 | macIcns: './public/images/icon.icns', 65 | winIco: './public/images/icon.ico' 66 | }, 67 | src: ['public/**/*', 'node_modules/**/*', '!node_modules/grunt**/**', 'updater/**/*', 'updater_client/**/*', 'views/**/*', '*.js', '*.html', '*.json'] // Your node-webkit app 68 | } 69 | }, 70 | 71 | }); 72 | 73 | grunt.loadNpmTasks('grunt-contrib-jshint'); 74 | grunt.loadNpmTasks('grunt-contrib-concat'); 75 | grunt.loadNpmTasks('grunt-contrib-watch'); 76 | grunt.loadNpmTasks('grunt-nw-builder'); 77 | grunt.loadNpmTasks('grunt-concurrent'); 78 | grunt.loadNpmTasks('grunt-nodemon'); 79 | 80 | grunt.registerTask('test', ['jshint']); 81 | grunt.registerTask('buildwin', ['concat', 'nwjs:win']); 82 | grunt.registerTask('buildunix', ['concat', 'nwjs:unix']); 83 | grunt.registerTask('buildandrun', ['concat', 'nodemon']); 84 | grunt.registerTask('dev', ['concat', 'concurrent']); 85 | grunt.registerTask('default', ['jshint']); 86 | }; 87 | -------------------------------------------------------------------------------- /updater_client/validation.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.validation = { 4 | 5 | /** 6 | * One giant validation method, taking an event and running 7 | * some basic validation against a targeted input element. 8 | * @param {object} e - event 9 | */ 10 | validateConfig: function (e) { 11 | var urlRegex = /\**..(.azurewebsites.net)/, 12 | result = true, 13 | username, password, zippath, url; 14 | 15 | e = (e.target) ? e.target.id : e; 16 | 17 | switch (e) { 18 | case 'blog-url': 19 | UpdaterClient.config.url = $('#blog-url').val(); 20 | url = UpdaterClient.config.url; 21 | if (!url || !urlRegex.test(url)) { 22 | $('#blog-url').addClass('invalid'); 23 | result = false; 24 | } else if (urlRegex.test(url)) { 25 | $('#blog-url').removeClass('invalid'); 26 | } 27 | 28 | break; 29 | case 'blog-username': 30 | UpdaterClient.config.username = $('#blog-username').val(); 31 | username = UpdaterClient.config.username; 32 | if (!username) { 33 | $('#blog-username').addClass('invalid'); 34 | result = false; 35 | } else if (username) { 36 | $('#blog-username').removeClass('invalid'); 37 | } 38 | 39 | break; 40 | case 'blog-password': 41 | UpdaterClient.config.password = $('#blog-password').val(); 42 | password = UpdaterClient.config.password; 43 | if (!password) { 44 | $('#blog-password').addClass('invalid'); 45 | result = false; 46 | } else if (password) { 47 | $('#blog-password').removeClass('invalid'); 48 | } 49 | 50 | break; 51 | case 'ghost-zip': 52 | UpdaterClient.config.zippath = $('#ghost-zip').val(); 53 | zippath = UpdaterClient.config.zippath; 54 | if (!zippath) { 55 | $('#ghost-zip').addClass('invalid'); 56 | result = false; 57 | } else if (zippath) { 58 | $('#ghost-zip').removeClass('invalid'); 59 | } 60 | 61 | break; 62 | default: 63 | var testUrl = this.validateConfig('blog-url'), 64 | testPassword = this.validateConfig('blog-password'), 65 | testUsername = this.validateConfig('blog-username'), 66 | testZippath; 67 | 68 | if (UpdaterClient.config.standalone) { 69 | testZippath = this.validateConfig('ghost-zip'); 70 | } else { 71 | testZippath = true; 72 | } 73 | 74 | if (!testUrl || !testUsername || !testPassword || !testZippath) { 75 | result = false; 76 | } 77 | 78 | break; 79 | } 80 | 81 | return result; 82 | } 83 | }; -------------------------------------------------------------------------------- /views/index.handlebars: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Ghost Updater for Microsoft Azure Websites

4 |

This tool automatically updates Ghost on a Microsoft Azure Website. It's especially useful if you installed Ghost using the Gallery and just selected 'Ghost' in the Azure Management Portal while creating the website.

5 |
6 |
7 | 8 | 9 |

Please enter your azurewebsites.net address, which has the format yoursite.azurewebsites.net.

10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |

Do not use the username/password you use to log into Azure. Where do I find my deployment credentials?

19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 |

Save your stuff

33 |

It's unlikely that things go wrong, but it's better to be safe than sorry. We can create a full backup (including all files). Once the update is done, you can either restore or delete it. This is heavily recommended.

34 |

In either case, please stop changing your blog's content now.

35 | 36 | 37 |
38 | 39 |
40 |
41 | 42 | 43 | 44 |
45 |

Finding your deployment credentials

46 |

The username and password required for this tool are the so called deployment credentials - the same you use to upload files via FTP or Git. They are not the credentials you use to log into the Azure Management Portal!

47 |

In your web browser, visit the Azure Management Portal and open up your website's dashboard. There, click on 'Reset Your Deployment Credentials' to enter a username and password.

48 | 49 |
50 | 51 |
52 |
53 | 54 | 55 |
56 |

Backing up your ghost blog

57 |
58 |
59 |

Live Script Output

60 | 66 |
67 |
68 |

69 |
70 |
71 | 72 | 73 |
74 |

Updating your ghost blog

75 |
76 |
77 |

Live Script Output

78 | 84 |
85 |
86 |

87 |
88 |
89 | 90 | 91 |
92 |

All done!

93 |

We're all done. Please visit your blog and ensure that everything is working well (if the blog loads, you're good). If the blog doesn't load after a few tries or displays an error, you can go back to your backup.

94 |

You can also just close the tool and keep the backup (it's in a folder called wwwroot-backup on your site).

95 |
96 | 97 | 98 |
99 |
100 | 101 | 102 | -------------------------------------------------------------------------------- /Ghost-Updater-Azure.njsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | 2.0 6 | {8caf995a-6903-4e71-984f-b70eb0be478d} 7 | 8 | ShowAllFiles 9 | app.js 10 | . 11 | . 12 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 13 | 11.0 14 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 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 | False 76 | True 77 | 0 78 | / 79 | http://localhost:48022/ 80 | False 81 | True 82 | http://localhost:1337 83 | False 84 | 85 | 86 | 87 | 88 | 89 | 90 | CurrentPage 91 | True 92 | False 93 | False 94 | False 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | False 104 | False 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:300,400,700); 2 | 3 | html { 4 | font: normal 81.2%/1.65 'Open Sans', sans-serif; 5 | } 6 | 7 | body { 8 | font-family: 'Open Sans', sans-serif; 9 | font-size: 15px; 10 | font-weight: 300; 11 | font-style: normal; 12 | font-variant: normal; 13 | 14 | padding: 20px; 15 | 16 | color: #7d878a; 17 | background-color: rgb(237, 236, 228); 18 | } 19 | 20 | .wrapper { 21 | position: relative; 22 | 23 | display: none; 24 | 25 | width: 760px; 26 | height: 560px; 27 | margin-right: auto; 28 | margin-left: auto; 29 | padding: 30px; 30 | 31 | background: #fff; 32 | box-shadow: rgba(0,0,0,.05) 0 1px 5px; 33 | } 34 | 35 | .wrapper::selection { 36 | color: #242628; 37 | background: #b3d5f3; 38 | text-shadow: none; 39 | } 40 | 41 | #ghost-zip-container { 42 | display: none; 43 | } 44 | 45 | #confirmArea { 46 | position: absolute; 47 | bottom: 30px; 48 | } 49 | 50 | .col { 51 | float: left; 52 | 53 | width: 245px; 54 | margin-right: 28px; 55 | } 56 | 57 | .col:last-child { 58 | margin-right: 0; 59 | } 60 | 61 | .title { 62 | font-size: 1.6em; 63 | font-weight: normal; 64 | line-height: .8em; 65 | 66 | margin: 0 0 18px 0; 67 | padding: 0; 68 | 69 | text-transform: uppercase; 70 | 71 | color: #242628; 72 | border: none; 73 | 74 | text-rendering: optimizeLegibility; 75 | } 76 | 77 | .title::selection { 78 | color: #242628; 79 | background: #b3d5f3; 80 | } 81 | 82 | input { 83 | font-size: 1.1rem; 84 | font-weight: normal; 85 | 86 | display: block; 87 | 88 | width: calc(100% - 16px); 89 | padding: 8px 10px; 90 | padding: 7px; 91 | 92 | -webkit-transition: border-color,.15s linear; 93 | -moz-transition: border-color,.15s linear; 94 | transition: border-color,.15s linear; 95 | 96 | color: #242628; 97 | border: 1px solid #e0dfd7; 98 | border-radius: 2px; 99 | } 100 | 101 | label { 102 | text-align: left; 103 | } 104 | 105 | input.invalid { 106 | background-color: #f2c5c3; 107 | } 108 | 109 | input.valid { 110 | border-bottom: #caf2b4; 111 | } 112 | 113 | label { 114 | font-size: 1em; 115 | font-weight: bold; 116 | 117 | display: block; 118 | 119 | margin-bottom: 4px; 120 | 121 | color: #242628; 122 | } 123 | 124 | button { 125 | font-size: 11px; 126 | font-weight: 300; 127 | line-height: 13px; 128 | 129 | display: inline-block; 130 | 131 | width: auto; 132 | min-height: 35px; 133 | margin: 0; 134 | padding: .9em 1.37em; 135 | 136 | cursor: pointer; 137 | -webkit-transition: background .3s ease,border-color .3s ease; 138 | -moz-transition: background .3s ease,border-color .3s ease; 139 | transition: background .3s ease,border-color .3s ease; 140 | text-align: center; 141 | text-decoration: none; 142 | text-indent: 0; 143 | letter-spacing: 1px; 144 | text-transform: uppercase; 145 | 146 | color: #fff; 147 | border: rgba(0,0,0,.05) .1em solid; 148 | border-radius: .2em; 149 | background: #5ba4e5; 150 | box-shadow: none; 151 | text-shadow: none; 152 | 153 | -webkit-appearance: button; 154 | } 155 | 156 | button:hover { 157 | text-decoration: none; 158 | 159 | border-color: transparent; 160 | background: #2f8cde; 161 | box-shadow: none; 162 | 163 | will-change: border-color, background; 164 | } 165 | 166 | .outputArea img { 167 | margin-bottom: -3px; 168 | } 169 | 170 | .error { 171 | font-weight: bold; 172 | 173 | color: red; 174 | } 175 | 176 | .warning { 177 | background-color: #e55b5b; 178 | } 179 | 180 | .warning:hover { 181 | background-color: #cf2121; 182 | } 183 | 184 | #existingBackup { 185 | padding-top: 200px; 186 | } 187 | 188 | .scriptLogArea { 189 | font-size: 11px; 190 | 191 | position: absolute; 192 | bottom: 20px; 193 | 194 | display: none; 195 | overflow: scroll; 196 | overflow-x: visible; 197 | 198 | width: 765px; 199 | height: 200px; 200 | 201 | overflow-wrap: break-word; 202 | } 203 | 204 | .ghostPackageLoader { 205 | display: none; 206 | 207 | margin-right: 5px; 208 | } 209 | 210 | .scriptLogTitle { 211 | position: absolute; 212 | bottom: 210px; 213 | 214 | display: none; 215 | 216 | width: 760px; 217 | } 218 | 219 | .circle { 220 | width: 14px; 221 | height: 14px; 222 | border-radius: 50%; 223 | background-color: #7f7f7f; 224 | display: inline-block; 225 | position: absolute; 226 | top: 26px; 227 | right: -5px; 228 | } 229 | 230 | .logleft { 231 | display: block; 232 | float: left; 233 | 234 | width: 250px; 235 | 236 | text-align: left; 237 | } 238 | 239 | .logright { 240 | display: block; 241 | float: right; 242 | 243 | width: 250px; 244 | 245 | text-align: right; 246 | } 247 | 248 | .logright h4 { 249 | margin-right: 14px; 250 | display: inline-block; 251 | } 252 | 253 | a.circle span { 254 | font-size: 10px; 255 | position:absolute; 256 | z-index: 999; 257 | white-space:nowrap; 258 | bottom:9999px; 259 | right: 50%; 260 | background:#000; 261 | color:#e0e0e0; 262 | padding:0px 7px; 263 | line-height: 24px; 264 | height: 24px; 265 | 266 | opacity: 0; 267 | transition:opacity 0.4s ease-out; 268 | } 269 | 270 | a.circle span::before { 271 | content: ""; 272 | display: block; 273 | border-left: 6px solid #000000; 274 | border-top: 6px solid transparent; 275 | position: absolute; 276 | top: -6px; 277 | left: 0px; 278 | } 279 | 280 | a.circle:hover span { 281 | opacity: 1; 282 | bottom:-35px; 283 | } -------------------------------------------------------------------------------- /updater/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | debug = require('debug')('gu-updater'), 3 | _ = require('underscore'), 4 | router = express.Router(), 5 | 6 | config = require('../config'), 7 | filesfolders = require('./filesfolders'); 8 | 9 | var updaterScriptRunning, updaterScriptLog; 10 | 11 | /** 12 | * Router endpoint enabling configuration. 13 | * TODO: This probably shouldn't be a GET? 14 | * @param {Express request} req 15 | * @param {Express response} res 16 | * @return {Express response.json} - JSON describing the new set configuration 17 | */ 18 | router.get('/config', function (req, res) { 19 | var website, scmPosition; 20 | 21 | debug('Config variables received', req.query); 22 | 23 | // Clean URL 24 | website = 'https://' + req.query.url; 25 | scmPosition = website.indexOf('.azurewebsites.net'); 26 | website = website.substr(0, scmPosition) + '.scm' + website.substr(scmPosition); 27 | 28 | config.website = website; 29 | config.username = req.query.username; 30 | config.password = req.query.password; 31 | if (config.standalone && req.query.zippath) { 32 | config.zippath = req.query.zippath; 33 | } 34 | 35 | debug('Config is now:', config.website, config.username, config.password, config.zippath); 36 | 37 | res.json({ website: config.website, username: config.username, password: config.password, zippath: config.zippath }); 38 | }); 39 | 40 | /** 41 | * Router endpoint triggering the file upload of the locally selected Ghost zip file 42 | * @param {Express request} req 43 | * @param {Express response} res 44 | * @return {Express response.json} - JSON describing success or failure 45 | */ 46 | router.get('/upload', function (req, res) { 47 | debug('Uploading Ghost to Azure Website'); 48 | 49 | filesfolders.list('site/temp') 50 | .then(function (result) { 51 | var filteredResponse, 52 | response = JSON.parse(result[1]) || null; 53 | 54 | debug('Get List Response: ', response); 55 | 56 | filteredResponse = _.findWhere(response, {path: 'D:\\home\\site\\temp\\ghost.zip'}); 57 | debug('Filtered response: ', filteredResponse); 58 | if (filteredResponse) { 59 | debug('Ghost.zip already exists, deleting /temp folder.'); 60 | return filesfolders.rmDir('site/temp'); 61 | } 62 | return; 63 | }).then(function() { 64 | filesfolders.upload(config.zippath, 'site/temp/ghost.zip') 65 | .then(function (result) { 66 | debug('Upload done, result: ' + result); 67 | res.json(result); 68 | }).catch(function(error) { 69 | res.json({error: error}); 70 | }); 71 | }).catch(function (error) { 72 | debug(error); 73 | res.json({error: error}); 74 | }); 75 | }); 76 | 77 | /** 78 | * Router endpoint triggering the deployment of the 'updater' webjob to the 79 | * Kudu service. This script is the big one actually performing the upgrade. 80 | * @param {Express request} req 81 | * @param {Express response} res 82 | * @return {Express response.json} - JSON describing success or failure 83 | */ 84 | router.get('/deploy', function (req, res) { 85 | debug('Deploying Updater Webjob'); 86 | 87 | return filesfolders.uploadWebjob('./public/powershell/updater.ps1', 'updater.ps1') 88 | .then(function (result) { 89 | debug('Upload done, result: ' + result); 90 | res.json(result); 91 | }); 92 | }); 93 | 94 | /** 95 | * Router endpoint triggering the 'updater' webjob. Hit this endpoint and 96 | * Kudu will attempt upgrading the Ghost installation. 97 | * @param {Express request} req 98 | * @param {Express response} res 99 | * @return {Express response.json} - JSON describing success or failure 100 | */ 101 | router.get('/trigger', function (req, res) { 102 | debug('Triggering Updater Webjob'); 103 | 104 | return filesfolders.triggerWebjob('updater.ps1') 105 | .then(function (result) { 106 | debug('Trigger successful, result: ' + result); 107 | return res.json(result); 108 | }); 109 | }); 110 | 111 | /** 112 | * Router endpoint returning the current log and status of the 'updater' webjob, 113 | * shoudl said webjob be running 114 | * @param {Express request} req 115 | * @param {Express response} res 116 | * @return {Express response text/plain} - Plain text of the log 117 | */ 118 | router.get('/info', function (req, res) { 119 | debug('Getting log info'); 120 | 121 | var responseBody; 122 | 123 | if (!updaterScriptRunning && !updaterScriptLog) { 124 | return filesfolders.getWebjobInfo('updater.ps1') 125 | .then(function (result) { 126 | debug(result); 127 | 128 | if (result && result.statusCode === 200) { 129 | responseBody = JSON.parse(result.body); 130 | updaterScriptLog = (responseBody.latest_run && responseBody.latest_run.output_url) ? responseBody.latest_run.output_url : ''; 131 | updaterScriptRunning = (updaterScriptLog) ? true : false; 132 | } 133 | 134 | debug(updaterScriptLog); 135 | return updaterScriptLog; 136 | }).then(function () { 137 | return filesfolders.getWebjobLog(updaterScriptLog) 138 | .then(function (logcontent) { 139 | debug('We have content! Size: ' + logcontent.length); 140 | res.set('Content-Type', 'text/plain'); 141 | return res.send(logcontent); 142 | }); 143 | }); 144 | } else { 145 | return filesfolders.getWebjobLog(updaterScriptLog) 146 | .then(function (logcontent) { 147 | res.set('Content-Type', 'text/plain'); 148 | return res.send(logcontent); 149 | }); 150 | } 151 | }); 152 | 153 | module.exports = router; -------------------------------------------------------------------------------- /updater_client/updater.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.updater = { 4 | 5 | updateFinished: false, 6 | scriptRunning: false, 7 | scriptLogTitle: null, 8 | scriptLogArea: null, 9 | scriptLog: null, 10 | timerCircle: null, 11 | timerYellow: null, 12 | timerRed: null, 13 | 14 | /** 15 | * Appends the updater log with additional text 16 | * @param {string} text - Text to append 17 | * @param {boolean} loading - Are we loading 18 | * @param {boolean} error - Is this an error 19 | * @return {$ append} 20 | */ 21 | appendLog: function (text, loading, error) { 22 | return UpdaterClient.utils.appendLog(text, loading, error, '#updateOutputArea'); 23 | }, 24 | 25 | /** 26 | * Appends an error to the output log 27 | * @param {string} text - Error text to append to the log 28 | * @return {$ append} 29 | */ 30 | appendError: function (text) { 31 | return this.appendLog(text, false, true); 32 | }, 33 | 34 | /** 35 | * Hit's the 'upload' router endpoint, eventually attempting to 36 | * upload the user-defined zip-file to the Azure Web App 37 | * @param {boolean} propagate - Should we continue with deploying once this is done? 38 | */ 39 | uploadGhost: function (propagate) { 40 | var self = UpdaterClient.updater, 41 | nochanges = ' No changes to your site have been made.', 42 | error; 43 | 44 | this.appendLog('Uploading Ghost package to Azure Website (this might take a while)', true); 45 | 46 | $.ajax('/updater/upload').done(function(response) { 47 | 48 | if (response.error || response.statusCode >= 400) { 49 | console.log('Error: ', response); 50 | 51 | if (response.statusCode === 401) { 52 | error = 'Azure rejected the given credentials - username and password are incorrect,'; 53 | error += 'or are not correct for ' + UpdaterClient.config.url + '.' + nochanges; 54 | } else if (response.statusCode === 412) { 55 | error = 'The filesystem at ' + UpdaterClient.config.url + ' does not accept the upload of the Ghost package.'; 56 | error += nochanges; 57 | } else if (response.error.code === 'ENOTFOUND' || (response.error.message && response.error.message.indexOf('ENOTFOUND') > -1)) { 58 | error = 'Website ' + UpdaterClient.config.url + ' could not be found. Please ensure that you are connected to the Internet '; 59 | error += 'and that the address is correct and restart the updater.' + nochanges; 60 | } else { 61 | error = response.error + nochanges; 62 | } 63 | self.appendError(error); 64 | } else if (response.statusCode === 201) { 65 | self.appendLog('Ghost package successfully uploaded'); 66 | if (propagate) { 67 | self.deployScript(propagate); 68 | } 69 | } 70 | 71 | }); 72 | }, 73 | 74 | /** 75 | * Hit's the 'deploy updater' endpoint on the router, eventually 76 | * attempting to upload the updater webjobs to the Azure Web App 77 | * @param {boolean} propagate - Should we trigger the script once this is done? 78 | */ 79 | deployScript: function (propagate) { 80 | var self = this; 81 | this.appendLog('Deploying update script to Azure Website'); 82 | 83 | $.ajax('/updater/deploy').done(function(response) { 84 | if (response.statusCode >= 200 && response.statusCode <= 400) { 85 | var responseBody = JSON.parse(response.body); 86 | 87 | if (responseBody.url) { 88 | self.appendLog('Script successfully deployed (' + responseBody.name + ')'); 89 | if (propagate) { 90 | self.triggerScript(propagate); 91 | } 92 | } 93 | } 94 | }); 95 | }, 96 | 97 | /** 98 | * Hit's the 'trigger updater' endpoint on the router, eventually 99 | * attempting to trigger the 'updater' webjob on the Azure Web App 100 | * @param {boolean} propagate - Should we get the script's status once this is done? 101 | */ 102 | triggerScript: function (propagate) { 103 | var self = this; 104 | this.appendLog('Starting Update script on Azure Website', true); 105 | 106 | $.ajax('/updater/trigger').done(function(response) { 107 | if (response.statusCode >= 200 && response.statusCode <= 400) { 108 | if (propagate) { 109 | self.getScriptStatus(propagate); 110 | } 111 | } 112 | }); 113 | }, 114 | 115 | /** 116 | * Hit's the 'updater info' endpoint on the router, attempting to get 117 | * the log of the 'updater webjob'. This will only work if the script 118 | * is running. 119 | */ 120 | getScriptStatus: function () { 121 | var self = this; 122 | 123 | if (!this.scriptRunning) { 124 | this.appendLog('Trying to get status of update script on Azure Website', true); 125 | this.scriptRunning = true; 126 | } 127 | 128 | $.ajax({ 129 | url: '/updater/info', 130 | dataType: 'text' 131 | }).done(function (response) { 132 | if (response && !self.updateFinished) { 133 | clearTimeout(self.timerYellow); 134 | clearTimeout(self.timerRed); 135 | 136 | self.timerYellow = setTimeout(function () { 137 | UpdaterClient.utils.timerButton('yellow'); 138 | }, 120000); 139 | self.timerRed = setTimeout(function () { 140 | UpdaterClient.utils.timerButton('red'); 141 | }, 300000); 142 | UpdaterClient.utils.timerButton('green'); 143 | 144 | self.scriptLogTitle = self.scriptLogTitle || $('.scriptLogTitle'); 145 | self.scriptLogTitle.show(); 146 | self.scriptLog = self.scriptLog || $('#updateScriptLog'); 147 | self.scriptLog.text(response); 148 | self.scriptLog.show(); 149 | self.scriptLogArea = self.scriptLogArea || $('#updateScriptLogArea'); 150 | self.scriptLogArea.show(); 151 | self.scriptLogArea.scrollTop(self.scriptLogArea.scrollHeight); 152 | 153 | if (response.indexOf('Status changed to Success') > -1) { 154 | // We're done! 155 | self.scriptLogArea.hide(); 156 | self.scriptLogTitle.hide(); 157 | self.scriptLog.empty(); 158 | UpdaterClient.utils.timerButton('grey'); 159 | clearTimeout(self.timerYellow); 160 | clearTimeout(self.timerRed); 161 | self.appendLog('All done, your blog has been updated!', false); 162 | self.updateFinished = true; 163 | 164 | setTimeout(function() { UpdaterClient.utils.switchPanel('#updatefinished'); }, 500); 165 | } 166 | 167 | setTimeout(function() { self.getScriptStatus(); }, 800); 168 | } 169 | }).fail(function (error) { 170 | console.log(error); 171 | 172 | if (!self.updateFinished) { 173 | setTimeout(function() { self.getScriptStatus(); }, 1000); 174 | } 175 | }); 176 | 177 | }, 178 | 179 | /** 180 | * Kicks off the whole 'update Ghost' chain, involving all the methods 181 | * above. 182 | */ 183 | startInstallation: function () { 184 | UpdaterClient.utils.switchPanel('#update'); 185 | UpdaterClient.updater.uploadGhost(true); 186 | } 187 | }; -------------------------------------------------------------------------------- /updater/backup.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | debug = require('debug')('gu-backup'), 3 | router = express.Router(), 4 | Promise = require('bluebird'), 5 | 6 | filesfolders = require('./filesfolders'); 7 | 8 | var createScriptRunning, createScriptLog, 9 | restoreScriptRunning, restoreScriptLog, 10 | deleteScriptRunning, deleteScriptLog; 11 | 12 | /** 13 | * Router endpoint for 'deployment', initiating the deployment of all backup scripts 14 | * to Kudu's webjob api. 15 | * @param {Express request} req 16 | * @param {Express response} res 17 | * @return {Express response JSON} - JSON describing success or failure of deployment 18 | */ 19 | router.get('/deploy', function (req, res) { 20 | debug('Deploying Backup Scripts'); 21 | 22 | var create = filesfolders.uploadWebjob('./public/powershell/createBackup.ps1', 'createBackup.ps1'), 23 | remove = filesfolders.uploadWebjob('./public/powershell/removeBackup.ps1', 'removeBackup.ps1'), 24 | restore = filesfolders.uploadWebjob('./public/powershell/restoreBackup.ps1', 'restoreBackup.ps1'); 25 | 26 | return Promise.all([create, remove, restore]) 27 | .then(function (results) { 28 | debug('Upload done, result: ' + results); 29 | return res.json({ status: 'Scripts deployed' }); 30 | }).catch(function (error) { 31 | debug('Scripts deployment error: ', error); 32 | return res.json({ error: error }); 33 | }); 34 | }); 35 | 36 | /** 37 | * Router endpoint triggering the 'create backup' webjob, which will 38 | * instruct Kudu to make a backup of the page (essentially just copying files) 39 | * @param {Express request} req 40 | * @param {Express response} res 41 | * @return {Express response JSON} - JSON describing success or failure 42 | */ 43 | router.post('/create', function (req, res) { 44 | debug('Triggering Create Backup Webjob'); 45 | 46 | return filesfolders.triggerWebjob('createBackup.ps1') 47 | .then(function (result) { 48 | debug('Trigger successful, result: ' + result); 49 | return res.json(result); 50 | }); 51 | }); 52 | 53 | /** 54 | * Router endpoint triggering the 'restore backup' webjob, which will 55 | * instruct Kudu to restore a made backup (essentially just copying back files) 56 | * @param {Express request} req 57 | * @param {Express response} res 58 | * @return {Express response JSON} - JSON describing success or failure 59 | */ 60 | router.post('/restore', function (req, res) { 61 | debug('Triggering Restore Backup Webjob'); 62 | 63 | return filesfolders.triggerWebjob('restoreBackup.ps1') 64 | .then(function (result) { 65 | debug('Trigger successful, result: ' + result); 66 | return res.json(result); 67 | }); 68 | }); 69 | 70 | /** 71 | * Router endpoint triggering the 'delete backup' webjob, which will instruct Kudu 72 | * to delete a previously made backup 73 | * @param {Express request} req 74 | * @param {Express response} res 75 | * @return {Express response JSON} - JSON describing success or failure 76 | */ 77 | router.post('/delete', function (req, res) { 78 | debug('Triggering Delete Backup Webjob'); 79 | 80 | return filesfolders.triggerWebjob('removeBackup.ps1') 81 | .then(function (result) { 82 | debug('Trigger successful, result: ' + result); 83 | return res.json(result); 84 | }); 85 | }); 86 | 87 | /** 88 | * Router endpoint returning the current status and log of the 'create backup' 89 | * script 90 | * @param {Express request} req 91 | * @param {Express response} res 92 | * @return {Express response text/plain} - Plain text of the log 93 | */ 94 | router.get('/create', function (req, res) { 95 | debug('Getting create script info'); 96 | 97 | var responseBody; 98 | 99 | if (!createScriptRunning && !createScriptLog) { 100 | return filesfolders.getWebjobInfo('createBackup.ps1') 101 | .then(function (result) { 102 | debug(result); 103 | 104 | if (result && result.statusCode === 200) { 105 | responseBody = JSON.parse(result.body); 106 | createScriptLog = (responseBody.latest_run && responseBody.latest_run.output_url) ? responseBody.latest_run.output_url : ''; 107 | createScriptRunning = (createScriptLog) ? true : false; 108 | } 109 | 110 | debug(createScriptLog); 111 | return createScriptLog; 112 | }).then(function () { 113 | return filesfolders.getWebjobLog(createScriptLog) 114 | .then(function (logcontent) { 115 | debug('We have content! Size: ' + logcontent.length); 116 | res.set('Content-Type', 'text/plain'); 117 | return res.send(logcontent); 118 | }); 119 | }); 120 | } else { 121 | return filesfolders.getWebjobLog(createScriptLog) 122 | .then(function (logcontent) { 123 | res.set('Content-Type', 'text/plain'); 124 | return res.send(logcontent); 125 | }); 126 | } 127 | }); 128 | 129 | /** 130 | * Router endpoint returning the current status and log of the 'restore backup' 131 | * script 132 | * @param {Express request} req 133 | * @param {Express response} res 134 | * @return {Express response text/plain} - Plain text of the log 135 | */ 136 | router.get('/restore', function (req, res) { 137 | debug('Getting restore script info'); 138 | 139 | var responseBody; 140 | 141 | if (!restoreScriptRunning && !restoreScriptLog) { 142 | return filesfolders.getWebjobInfo('restoreBackup.ps1') 143 | .then(function (result) { 144 | debug(result); 145 | 146 | if (result && result.statusCode === 200) { 147 | responseBody = JSON.parse(result.body); 148 | restoreScriptLog = (responseBody.latest_run && responseBody.latest_run.output_url) ? responseBody.latest_run.output_url : ''; 149 | restoreScriptRunning = (restoreScriptLog) ? true : false; 150 | } 151 | 152 | debug(restoreScriptLog); 153 | return restoreScriptLog; 154 | }).then(function () { 155 | return filesfolders.getWebjobLog(restoreScriptLog) 156 | .then(function (logcontent) { 157 | debug('We have content! Size: ' + logcontent.length); 158 | res.set('Content-Type', 'text/plain'); 159 | return res.send(logcontent); 160 | }); 161 | }); 162 | } else { 163 | return filesfolders.getWebjobLog(restoreScriptLog) 164 | .then(function (logcontent) { 165 | res.set('Content-Type', 'text/plain'); 166 | return res.send(logcontent); 167 | }); 168 | } 169 | }); 170 | 171 | /** 172 | * Router endpoint returning the current status and log of the 'delete backup' 173 | * script 174 | * @param {Express request} req 175 | * @param {Express response} res 176 | * @return {Express response text/plain} - Plain text of the log 177 | */ 178 | router.get('/delete', function (req, res) { 179 | debug('Getting create script info'); 180 | 181 | var responseBody; 182 | 183 | if (!deleteScriptRunning && !deleteScriptLog) { 184 | return filesfolders.getWebjobInfo('removeBackup.ps1') 185 | .then(function (result) { 186 | debug(result); 187 | 188 | if (result && result.statusCode === 200) { 189 | responseBody = JSON.parse(result.body); 190 | deleteScriptLog = (responseBody.latest_run && responseBody.latest_run.output_url) ? responseBody.latest_run.output_url : ''; 191 | deleteScriptRunning = (deleteScriptLog) ? true : false; 192 | } 193 | 194 | debug(deleteScriptLog); 195 | return deleteScriptLog; 196 | }).then(function () { 197 | return filesfolders.getWebjobLog(deleteScriptLog) 198 | .then(function (logcontent) { 199 | debug('We have content! Size: ' + logcontent.length); 200 | res.set('Content-Type', 'text/plain'); 201 | return res.send(logcontent); 202 | }); 203 | }); 204 | } else { 205 | return filesfolders.getWebjobLog(deleteScriptLog) 206 | .then(function (logcontent) { 207 | res.set('Content-Type', 'text/plain'); 208 | return res.send(logcontent); 209 | }); 210 | } 211 | }); 212 | 213 | module.exports = router; -------------------------------------------------------------------------------- /updater_client/backup.js: -------------------------------------------------------------------------------- 1 | var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.backup = { 4 | 5 | scriptsDeployed: false, 6 | deletingOldBackup: false, 7 | creatingBackup: false, 8 | backupFinished: false, 9 | bScriptLogArea: null, 10 | bScriptLog: null, 11 | scriptLogTitle: null, 12 | 13 | /** 14 | * Appends the backup log with additional text 15 | * @param {string} text - Text to append 16 | * @param {boolean} loading - Are we loading 17 | * @param {boolean} error - Is this an error 18 | * @return {$ append} 19 | */ 20 | appendLog: function (text, loading, error) { 21 | return UpdaterClient.utils.appendLog(text, loading, error, '#backupOutputArea'); 22 | }, 23 | 24 | /** 25 | * Appends an error to the output log 26 | * @param {string} text - Error text to append to the log 27 | * @return {$ append} 28 | */ 29 | appendError: function (text) { 30 | return this.appendLog(text, false, true); 31 | }, 32 | 33 | /** 34 | * Creates UI indiciating that we're depoying backup scripts, but also 35 | * calls the GET 'deploy' endpoint, which will ultimately deploy the 36 | * backup scripts to Kudu 37 | * @param {Function} callback 38 | */ 39 | deployScripts: function (callback) { 40 | var self = this, 41 | nochanges = ' No changes to your site have been made.', 42 | error; 43 | 44 | this.appendLog('Deploying backup scripts to Azure Website', true); 45 | 46 | $.ajax('/backup/deploy').done(function (response) { 47 | if (response && response.error) { 48 | if (response.error.message && response.error.message.indexOf('ENOTFOUND') > -1) { 49 | error = 'Website ' + UpdaterClient.config.url + ' could not be found. Please ensure that you are connected to the Internet '; 50 | error += 'and that the address is correct and restart the updater.' + nochanges; 51 | return self.appendError(error); 52 | } else { 53 | return self.appendError(response.error); 54 | } 55 | } 56 | 57 | self.appendLog('Scripts successfully deployed'); 58 | self.scriptsDeployed = true; 59 | 60 | if (callback) { 61 | return callback.call(self); 62 | } 63 | }); 64 | }, 65 | 66 | /** 67 | * Creates UI indicating that we're creating a remote backup, but also calls 68 | * the router endpoint kicking off the webjob that will ultimately create the 69 | * backup 70 | */ 71 | makeBackup: function () { 72 | var self = this; 73 | this.appendLog('Instructing Azure to create backup (this might take a while)', true); 74 | 75 | $.post('/backup/create').done(function (response) { 76 | if (response) { 77 | console.log('Triggered create, getting status'); 78 | self.getScriptStatus('create'); 79 | } 80 | }); 81 | }, 82 | 83 | /** 84 | * Creates UI indicating that we're deleting a remote backup, but also calls 85 | * the router endpoint kicking off the webjob that will ultimately delete the 86 | * backup 87 | */ 88 | deleteBackup: function () { 89 | var self = UpdaterClient.backup; 90 | 91 | $('#backup > .title').text('Deleting Backup'); 92 | UpdaterClient.utils.switchPanel('#backup'); 93 | self.appendLog('Instructing Azure to delete backup', true); 94 | 95 | $.post('/backup/delete').done(function (response) { 96 | if (response) { 97 | self.getScriptStatus('delete'); 98 | } 99 | }); 100 | }, 101 | 102 | /** 103 | * Creates UI indicating that we're restoring a remote backup, but also calls 104 | * the router endpoint kicking off the webjob that will ultimately restore the 105 | * backup 106 | */ 107 | restoreBackup: function () { 108 | var self = UpdaterClient.backup; 109 | 110 | $('#backup > .title').text('Restoring Backup'); 111 | UpdaterClient.utils.switchPanel('#backup'); 112 | self.appendLog('Instructing Azure to restore backup (this might take a while)', true); 113 | 114 | $.post('/backup/restore').done(function (response) { 115 | if (response) { 116 | self.getScriptStatus('restore'); 117 | } 118 | }); 119 | }, 120 | 121 | /** 122 | * Helper function called by all three "kicking off a script" methods above, 123 | * getting the status for a specific script. This monster function gets the 124 | * log URL from Kudu, pulls the log, and repeats the pulling until the script 125 | * has exited 126 | * @param {string} script - Name of the script 127 | */ 128 | getScriptStatus: function (script) { 129 | var self = UpdaterClient.backup; 130 | 131 | $.ajax({ 132 | url: '/backup/' + script, 133 | dataType: 'text' 134 | }).done(function (response) { 135 | var repeat = false; 136 | 137 | if (response) { 138 | clearTimeout(self.timerYellow); 139 | clearTimeout(self.timerRed); 140 | 141 | self.timerYellow = setTimeout(function () { 142 | UpdaterClient.utils.timerButton('yellow'); 143 | }, 120000); 144 | self.timerRed = setTimeout(function () { 145 | UpdaterClient.utils.timerButton('red'); 146 | }, 300000); 147 | UpdaterClient.utils.timerButton('green'); 148 | 149 | self.scriptLogTitle = self.scriptLogTitle || $('.scriptLogTitle'); 150 | self.scriptLogTitle.show(); 151 | self.bScriptLog = self.bScriptLog || $('#backupScriptLog'); 152 | self.bScriptLog.text(response); 153 | self.bScriptLogArea = self.bScriptLogArea || $('#backupScriptLogArea'); 154 | self.bScriptLogArea.show(); 155 | self.bScriptLogArea.scrollTop(self.bScriptLogArea.scrollHeight); 156 | } 157 | 158 | if (response && !self.backupFinished && script === 'create') { 159 | // Done 160 | if (response.indexOf('Status changed to Success') > -1 && !self.backupFinished) { 161 | self.appendLog('All done, initiating update!', false); 162 | self.backupFinished = true; 163 | 164 | setTimeout(function() { 165 | UpdaterClient.updater.startInstallation(); 166 | self.bScriptLogArea.hide(); 167 | self.scriptLogTitle.hide(); 168 | self.bScriptLog.empty(); 169 | clearTimeout(self.timerYellow); 170 | clearTimeout(self.timerRed); 171 | UpdaterClient.utils.timerButton('grey'); 172 | $('#backupOutputArea').empty(); 173 | }, 300); 174 | } 175 | 176 | // Removing old backup 177 | if (response.indexOf('Removing old backup') > -1 && !self.deletingOldBackup) { 178 | self.appendLog('Removing old backup', true); 179 | self.deletingOldBackup = true; 180 | } 181 | 182 | // Copying folder 183 | if (response.indexOf('Creating Full Site Backup') > -1 && !self.creatingBackup) { 184 | self.appendLog('Backing up files', true); 185 | self.creatingBackup = true; 186 | } 187 | 188 | repeat = true; 189 | } 190 | 191 | if (response && script === 'delete') { 192 | // Done 193 | if (response.indexOf('Status changed to Success') > -1) { 194 | self.appendLog('All done, backup deleted!', false); 195 | self.appendLog('You can now close this tool.', false); 196 | } else { 197 | repeat = true; 198 | } 199 | } 200 | 201 | if (response && script === 'restore') { 202 | // Done 203 | if (response.indexOf('Status changed to Success') > -1) { 204 | self.appendLog('All done, backup restored. We\'re sorry that we could not update your blog, but everything is like it was before.', false); 205 | self.appendLog('You can now close this tool.', false); 206 | } else { 207 | repeat = true; 208 | } 209 | } 210 | 211 | if (repeat) { 212 | setTimeout(function() { self.getScriptStatus(script); }, 800); 213 | } 214 | }); 215 | }, 216 | 217 | /** 218 | * Starts the upgrade process *with* backup, as oppposed to starting it 219 | * without it. 220 | * TODO: This name is confusing 221 | */ 222 | startBackup: function () { 223 | UpdaterClient.config.backup = true; 224 | UpdaterClient.utils.switchPanel('#backup'); 225 | UpdaterClient.backup.deployScripts(UpdaterClient.backup.makeBackup); 226 | } 227 | }; -------------------------------------------------------------------------------- /updater/filesfolders.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'), 2 | debug = require('debug')('gu-filesfolders'), 3 | request = require('request'), 4 | Promise = require('bluebird'), 5 | fs = require('fs'); 6 | 7 | Promise.promisifyAll(request); 8 | Promise.promisifyAll(fs); 9 | 10 | /** 11 | * The 'filesfolders' module contains helper methods allowing the interaction 12 | * with Kudu's VFS API - enabling basic file operations on the website 13 | * @type {Object} 14 | */ 15 | var filesfolders = { 16 | /** 17 | * Creates a directory 18 | * @param {string} dir - Name of the directory 19 | * @return {promise} - Resolving to the VFS API's response 20 | */ 21 | mkDir: function (dir) { 22 | return this.mk(dir, true); 23 | }, 24 | 25 | /** 26 | * Creates a file 27 | * @param {string} dir - Name of the file 28 | * @return {promise} - Resolving to the VFS API's response 29 | */ 30 | mkFile: function (file) { 31 | return this.mk(file, false); 32 | }, 33 | 34 | /** 35 | * Removes a directory 36 | * @param {string} dir - Name of the directory 37 | * @return {promise} - Resolving to the VFS API's response 38 | */ 39 | rmDir: function (dir) { 40 | return this.rm(dir, true); 41 | }, 42 | 43 | /** 44 | * Creates a file 45 | * @param {string} dir - Name of the file 46 | * @return {promise} - Resolving to the VFS API's response 47 | */ 48 | rmFile: function (file) { 49 | return this.rm(file, false); 50 | }, 51 | 52 | /** 53 | * Creates an element 54 | * @param {string} target - Name of the element 55 | * @param {Boolean} isDir - Are we creating a directory? 56 | * @return {promise} - Resolving to the VFS API's response 57 | */ 58 | mk: function (target, isDir) { 59 | target = (isDir) ? target + '/' : target; 60 | 61 | return request.putAsync(config.website + '/api/vfs/' + target, { 62 | 'auth': config.auth(), 63 | }).then(function(response) { 64 | return response; 65 | }).catch(console.log); 66 | }, 67 | 68 | /** 69 | * Removes an element 70 | * @param {string} target - Name of the element 71 | * @param {Boolean} isDir - Are we removing a directory? 72 | * @return {promise} - Resolving to the VFS API's response 73 | */ 74 | rm: function (target, isDir) { 75 | target = (isDir) ? target + '/?recursive=true' : target; 76 | 77 | return request.delAsync(config.website + '/api/vfs/' + target, { 78 | 'auth': config.auth() 79 | }).then(function(response) { 80 | debug('Delete: ', response); 81 | return response; 82 | }).catch(console.log); 83 | }, 84 | 85 | /** 86 | * Lists a directory's content 87 | * @param {string} target - Name of the direcotry 88 | * @return {promise} - Resolving to the VFS API's response 89 | */ 90 | list: function (target) { 91 | return new Promise(function (resolve, reject) { 92 | var targetUrl = config.website + '/api/vfs/' + target + '/', 93 | errorCheck; 94 | 95 | debug('Listing dir for ' + targetUrl); 96 | request.getAsync(targetUrl, {'auth': config.auth()}) 97 | .then(function (response) { 98 | errorCheck = filesfolders.checkForError(response); 99 | if (errorCheck) { 100 | return reject(errorCheck); 101 | } 102 | 103 | resolve(response); 104 | }).catch(function (error) { 105 | debug('List: Request failed', error); 106 | reject(error); 107 | }); 108 | }); 109 | }, 110 | 111 | /** 112 | * Uploads a file to the Azure Web App 113 | * @param {string} source - Path to local file 114 | * @param {string} target - Path and name of the remote location 115 | * @return {promise} - Resolving to the VFS API's response 116 | */ 117 | upload: function (source, target) { 118 | return new Promise(function (resolve, reject) { 119 | var targetUrl = config.website + '/api/vfs/' + target, 120 | sourceStream, errorCheck; 121 | 122 | if (!fs.existsSync(source)) { 123 | return reject('The file ' + source + ' does not exist or cannot be read.'); 124 | } 125 | 126 | sourceStream = fs.createReadStream(source); 127 | 128 | debug('Uploading ' + source + ' to ' + target); 129 | 130 | sourceStream.pipe(request.put(targetUrl, {'auth': config.auth()}, 131 | function(error, result) { 132 | if (error) { 133 | debug('Upload Error: ', error); 134 | return reject(error); 135 | } 136 | return resolve(result); 137 | }) 138 | ); 139 | }); 140 | }, 141 | 142 | /** 143 | * Uploads a webjob to the Azure Web App's Kudu service 144 | * @param {string} source - Path to local script 145 | * @param {string} name - Name of the webjob 146 | * @return {promise} - Resolving to the VFS API's response 147 | */ 148 | uploadWebjob: function (source, name) { 149 | return new Promise(function (resolve, reject) { 150 | var targetUrl = config.website + '/api/triggeredwebjobs/' + name, 151 | sourceStream = fs.createReadStream(source), 152 | errorCheck; 153 | 154 | debug('Uploading Webjob ' + source + ' as ' + name); 155 | 156 | request.delAsync(targetUrl, {'auth': config.auth()}) 157 | .then(function () { 158 | sourceStream.pipe(request.put(targetUrl, { 159 | 'auth': config.auth(), 160 | 'headers': { 161 | 'Content-Disposition': 'attachement; filename=' + name 162 | } 163 | }, 164 | function(error, response, body) { 165 | if (error) { 166 | debug('Upload Webjob Error: ', error); 167 | reject(error); 168 | } 169 | 170 | debug('Upload Webjob Response: ', response); 171 | debug('Upload Webjob Body: ', body); 172 | 173 | errorCheck = filesfolders.checkForError(response); 174 | if (errorCheck) { 175 | return reject(errorCheck); 176 | } 177 | 178 | resolve(response); 179 | }) 180 | ); 181 | }).catch(function (error) { 182 | reject(error); 183 | }); 184 | }); 185 | }, 186 | 187 | /** 188 | * Hit's the Azure Web App's Kudu service's webjob api for the log and 189 | * status of a webjob 190 | * @param {string} name - Name of the webjob 191 | * @return {promise} - Resolves to the Kudu API response 192 | */ 193 | getWebjobInfo: function (name) { 194 | return new Promise(function (resolve, reject) { 195 | var targetUrl = config.website + '/api/triggeredwebjobs/' + name, 196 | errorCheck; 197 | 198 | request.get(targetUrl, {'auth': config.auth()}, 199 | function (error, response, body) { 200 | if (error) { 201 | debug('Get Webjob Info Error: ', error); 202 | reject(error); 203 | } 204 | 205 | debug('Get Webjob Info Response: ', response); 206 | debug('Get Webjob Info Body: ', body); 207 | 208 | errorCheck = filesfolders.checkForError(response); 209 | if (errorCheck) { 210 | return reject(errorCheck); 211 | } 212 | 213 | resolve(response); 214 | } 215 | ); 216 | }); 217 | }, 218 | 219 | /** 220 | * Takes a webjob log URL and returns the content as plain text 221 | * @param {string} targetUrl - Url of the webjob log 222 | * @return {promise} - Resolves to the Kudu API response 223 | */ 224 | getWebjobLog: function (targetUrl) { 225 | return new Promise(function (resolve, reject) { 226 | var errorCheck; 227 | 228 | request.get(targetUrl, {'auth': config.auth()}, 229 | function (error, response, body) { 230 | if (error) { 231 | debug('Get Webjob Log Error: ', error); 232 | reject(error); 233 | } 234 | 235 | debug('Get Webjob Log Response: ', response); 236 | debug('Get Webjob Log Body: ', body); 237 | 238 | errorCheck = filesfolders.checkForError(response); 239 | if (errorCheck) { 240 | return reject(errorCheck); 241 | } 242 | 243 | resolve(body); 244 | } 245 | ); 246 | }); 247 | }, 248 | 249 | /** 250 | * Trigger's a webjob on the Azure Web App's Kudu service 251 | * @param {string} name - Name of the webjob 252 | * @return {promise} - Resolves to the Kudu API response 253 | */ 254 | triggerWebjob: function (name) { 255 | return new Promise(function (resolve, reject) { 256 | var targetUrl = config.website + '/api/triggeredwebjobs/' + name + '/run', 257 | errorCheck; 258 | 259 | debug('Triggering Webjob ' + name); 260 | 261 | request.post(targetUrl, {'auth': config.auth()}, 262 | function (error, response, body) { 263 | if (error) { 264 | debug('Trigger Error: ', error); 265 | reject(error); 266 | } 267 | 268 | debug('Trigger Response: ', response); 269 | debug('Trigger Body: ', body); 270 | 271 | errorCheck = filesfolders.checkForError(response); 272 | if (errorCheck) { 273 | return reject(errorCheck); 274 | } 275 | 276 | resolve(response); 277 | } 278 | ); 279 | }); 280 | }, 281 | 282 | /** 283 | * Small helper function used in all methods above, checking the Azure response 284 | * for errors. This is required because Azure likes to return an HTML document 285 | * describing the error, but it returns said HTML document with status 200 - 286 | * the AJAX requests therefore think that everything is fine. 287 | * @param {AJAX response object} response - The response that should be checked 288 | * @return {boolean} - If there's no error, we return false 289 | */ 290 | checkForError: function (response) { 291 | // Azure shouldn't return HTML, so something is up 292 | response = (response[0] && response[0].headers) ? response[0] : response; 293 | 294 | if (response.headers && response.headers['content-type'] && response.headers['content-type'] === 'text/html') { 295 | debug('Azure returned text/html, checking for errors'); 296 | 297 | if (response.body && response.body.indexOf('401 - Unauthorized') > -1) { 298 | return 'Invalid Credentials: The Azure Website rejected the given username or password.'; 299 | } 300 | } 301 | 302 | return false; 303 | } 304 | }; 305 | 306 | module.exports = filesfolders; -------------------------------------------------------------------------------- /public/scripts/built.js: -------------------------------------------------------------------------------- 1 | /*! Ghost-Updater-Azure - v0.6.1 - 2016-03-18 */var UpdaterClient = UpdaterClient || {}; 2 | 3 | UpdaterClient.init = function () { 4 | UpdaterClient.config.getConfig(); 5 | 6 | // Wire up buttons to actions 7 | $('input').bind('input', UpdaterClient.validation.validateConfig); 8 | $('#ghost-zip').click(function () { 9 | $('.ghostPackageLoader').show(); 10 | }); 11 | $('#ghost-zip').change(function () { 12 | $(this).attr('disabled', false); 13 | $('.ghostPackageLoader').hide(); 14 | }); 15 | 16 | // TODO: Defining actions and handlers here is okay, but feels dirty. 17 | // This allows us to define actions with the data-action attribute. 18 | $('body').on('click', '[data-action]', function() { 19 | var action = $(this).data('action'), 20 | split = (action) ? action.split('.') : null, 21 | fn = window; 22 | 23 | for (var i = 0; i < split.length; i++) { 24 | fn = (fn) ? fn[split[i]] : null; 25 | } 26 | 27 | if (typeof fn === 'function') { 28 | fn.apply(null, arguments); 29 | } 30 | }); 31 | 32 | $('#config').fadeIn(900); 33 | };var UpdaterClient = UpdaterClient || {}; 34 | 35 | UpdaterClient.backup = { 36 | 37 | scriptsDeployed: false, 38 | deletingOldBackup: false, 39 | creatingBackup: false, 40 | backupFinished: false, 41 | bScriptLogArea: null, 42 | bScriptLog: null, 43 | scriptLogTitle: null, 44 | 45 | /** 46 | * Appends the backup log with additional text 47 | * @param {string} text - Text to append 48 | * @param {boolean} loading - Are we loading 49 | * @param {boolean} error - Is this an error 50 | * @return {$ append} 51 | */ 52 | appendLog: function (text, loading, error) { 53 | return UpdaterClient.utils.appendLog(text, loading, error, '#backupOutputArea'); 54 | }, 55 | 56 | /** 57 | * Appends an error to the output log 58 | * @param {string} text - Error text to append to the log 59 | * @return {$ append} 60 | */ 61 | appendError: function (text) { 62 | return this.appendLog(text, false, true); 63 | }, 64 | 65 | /** 66 | * Creates UI indiciating that we're depoying backup scripts, but also 67 | * calls the GET 'deploy' endpoint, which will ultimately deploy the 68 | * backup scripts to Kudu 69 | * @param {Function} callback 70 | */ 71 | deployScripts: function (callback) { 72 | var self = this, 73 | nochanges = ' No changes to your site have been made.', 74 | error; 75 | 76 | this.appendLog('Deploying backup scripts to Azure Website', true); 77 | 78 | $.ajax('/backup/deploy').done(function (response) { 79 | if (response && response.error) { 80 | if (response.error.message && response.error.message.indexOf('ENOTFOUND') > -1) { 81 | error = 'Website ' + UpdaterClient.config.url + ' could not be found. Please ensure that you are connected to the Internet '; 82 | error += 'and that the address is correct and restart the updater.' + nochanges; 83 | return self.appendError(error); 84 | } else { 85 | return self.appendError(response.error); 86 | } 87 | } 88 | 89 | self.appendLog('Scripts successfully deployed'); 90 | self.scriptsDeployed = true; 91 | 92 | if (callback) { 93 | return callback.call(self); 94 | } 95 | }); 96 | }, 97 | 98 | /** 99 | * Creates UI indicating that we're creating a remote backup, but also calls 100 | * the router endpoint kicking off the webjob that will ultimately create the 101 | * backup 102 | */ 103 | makeBackup: function () { 104 | var self = this; 105 | this.appendLog('Instructing Azure to create backup (this might take a while)', true); 106 | 107 | $.post('/backup/create').done(function (response) { 108 | if (response) { 109 | console.log('Triggered create, getting status'); 110 | self.getScriptStatus('create'); 111 | } 112 | }); 113 | }, 114 | 115 | /** 116 | * Creates UI indicating that we're deleting a remote backup, but also calls 117 | * the router endpoint kicking off the webjob that will ultimately delete the 118 | * backup 119 | */ 120 | deleteBackup: function () { 121 | var self = UpdaterClient.backup; 122 | 123 | $('#backup > .title').text('Deleting Backup'); 124 | UpdaterClient.utils.switchPanel('#backup'); 125 | self.appendLog('Instructing Azure to delete backup', true); 126 | 127 | $.post('/backup/delete').done(function (response) { 128 | if (response) { 129 | self.getScriptStatus('delete'); 130 | } 131 | }); 132 | }, 133 | 134 | /** 135 | * Creates UI indicating that we're restoring a remote backup, but also calls 136 | * the router endpoint kicking off the webjob that will ultimately restore the 137 | * backup 138 | */ 139 | restoreBackup: function () { 140 | var self = UpdaterClient.backup; 141 | 142 | $('#backup > .title').text('Restoring Backup'); 143 | UpdaterClient.utils.switchPanel('#backup'); 144 | self.appendLog('Instructing Azure to restore backup (this might take a while)', true); 145 | 146 | $.post('/backup/restore').done(function (response) { 147 | if (response) { 148 | self.getScriptStatus('restore'); 149 | } 150 | }); 151 | }, 152 | 153 | /** 154 | * Helper function called by all three "kicking off a script" methods above, 155 | * getting the status for a specific script. This monster function gets the 156 | * log URL from Kudu, pulls the log, and repeats the pulling until the script 157 | * has exited 158 | * @param {string} script - Name of the script 159 | */ 160 | getScriptStatus: function (script) { 161 | var self = UpdaterClient.backup; 162 | 163 | $.ajax({ 164 | url: '/backup/' + script, 165 | dataType: 'text' 166 | }).done(function (response) { 167 | var repeat = false; 168 | 169 | if (response) { 170 | clearTimeout(self.timerYellow); 171 | clearTimeout(self.timerRed); 172 | 173 | self.timerYellow = setTimeout(function () { 174 | UpdaterClient.utils.timerButton('yellow'); 175 | }, 120000); 176 | self.timerRed = setTimeout(function () { 177 | UpdaterClient.utils.timerButton('red'); 178 | }, 300000); 179 | UpdaterClient.utils.timerButton('green'); 180 | 181 | self.scriptLogTitle = self.scriptLogTitle || $('.scriptLogTitle'); 182 | self.scriptLogTitle.show(); 183 | self.bScriptLog = self.bScriptLog || $('#backupScriptLog'); 184 | self.bScriptLog.text(response); 185 | self.bScriptLogArea = self.bScriptLogArea || $('#backupScriptLogArea'); 186 | self.bScriptLogArea.show(); 187 | self.bScriptLogArea.scrollTop(self.bScriptLogArea.scrollHeight); 188 | } 189 | 190 | if (response && !self.backupFinished && script === 'create') { 191 | // Done 192 | if (response.indexOf('Status changed to Success') > -1 && !self.backupFinished) { 193 | self.appendLog('All done, initiating update!', false); 194 | self.backupFinished = true; 195 | 196 | setTimeout(function() { 197 | UpdaterClient.updater.startInstallation(); 198 | self.bScriptLogArea.hide(); 199 | self.scriptLogTitle.hide(); 200 | self.bScriptLog.empty(); 201 | clearTimeout(self.timerYellow); 202 | clearTimeout(self.timerRed); 203 | UpdaterClient.utils.timerButton('grey'); 204 | $('#backupOutputArea').empty(); 205 | }, 300); 206 | } 207 | 208 | // Removing old backup 209 | if (response.indexOf('Removing old backup') > -1 && !self.deletingOldBackup) { 210 | self.appendLog('Removing old backup', true); 211 | self.deletingOldBackup = true; 212 | } 213 | 214 | // Copying folder 215 | if (response.indexOf('Creating Full Site Backup') > -1 && !self.creatingBackup) { 216 | self.appendLog('Backing up files', true); 217 | self.creatingBackup = true; 218 | } 219 | 220 | repeat = true; 221 | } 222 | 223 | if (response && script === 'delete') { 224 | // Done 225 | if (response.indexOf('Status changed to Success') > -1) { 226 | self.appendLog('All done, backup deleted!', false); 227 | self.appendLog('You can now close this tool.', false); 228 | } else { 229 | repeat = true; 230 | } 231 | } 232 | 233 | if (response && script === 'restore') { 234 | // Done 235 | if (response.indexOf('Status changed to Success') > -1) { 236 | self.appendLog('All done, backup restored. We\'re sorry that we could not update your blog, but everything is like it was before.', false); 237 | self.appendLog('You can now close this tool.', false); 238 | } else { 239 | repeat = true; 240 | } 241 | } 242 | 243 | if (repeat) { 244 | setTimeout(function() { self.getScriptStatus(script); }, 800); 245 | } 246 | }); 247 | }, 248 | 249 | /** 250 | * Starts the upgrade process *with* backup, as oppposed to starting it 251 | * without it. 252 | * TODO: This name is confusing 253 | */ 254 | startBackup: function () { 255 | UpdaterClient.config.backup = true; 256 | UpdaterClient.utils.switchPanel('#backup'); 257 | UpdaterClient.backup.deployScripts(UpdaterClient.backup.makeBackup); 258 | } 259 | };var UpdaterClient = UpdaterClient || {}; 260 | 261 | UpdaterClient.config = { 262 | url: '', 263 | username: '', 264 | password: '', 265 | zippath: '', 266 | standalone: undefined, 267 | backup: false, 268 | 269 | /** 270 | * Takes the config entered by the user and hits the router configuration 271 | * endpoint, essentially telling the Node part of this app what the 272 | * configuration is. 273 | */ 274 | setConfig: function () { 275 | if (UpdaterClient.validation.validateConfig('default')) { 276 | $.ajax({ 277 | url: '/updater/config', 278 | data: { 279 | url: UpdaterClient.config.url, 280 | username: UpdaterClient.config.username, 281 | password: UpdaterClient.config.password, 282 | zippath: UpdaterClient.config.zippath 283 | } 284 | }) 285 | .done(function(response) { 286 | console.log(response); 287 | UpdaterClient.utils.switchPanel('#backupdisclaimer'); 288 | }); 289 | } 290 | }, 291 | 292 | /** 293 | * Ensures that we're running in NW.js - and show's the file 294 | * upload option, if that's the case 295 | * TODO: This seemed smart in the beginning, but pointless now. 296 | * We're always running as an app. 297 | */ 298 | getConfig: function () { 299 | $.ajax('/nw').done(function (response) { 300 | console.log(response); 301 | if (response.isNodeWebkit) { 302 | UpdaterClient.config.standalone = true; 303 | $('#ghost-zip-container').show(); 304 | } 305 | }); 306 | } 307 | }; 308 | 309 | var UpdaterClient = UpdaterClient || {}; 310 | 311 | UpdaterClient.updater = { 312 | 313 | updateFinished: false, 314 | scriptRunning: false, 315 | scriptLogTitle: null, 316 | scriptLogArea: null, 317 | scriptLog: null, 318 | timerCircle: null, 319 | timerYellow: null, 320 | timerRed: null, 321 | 322 | /** 323 | * Appends the updater log with additional text 324 | * @param {string} text - Text to append 325 | * @param {boolean} loading - Are we loading 326 | * @param {boolean} error - Is this an error 327 | * @return {$ append} 328 | */ 329 | appendLog: function (text, loading, error) { 330 | return UpdaterClient.utils.appendLog(text, loading, error, '#updateOutputArea'); 331 | }, 332 | 333 | /** 334 | * Appends an error to the output log 335 | * @param {string} text - Error text to append to the log 336 | * @return {$ append} 337 | */ 338 | appendError: function (text) { 339 | return this.appendLog(text, false, true); 340 | }, 341 | 342 | /** 343 | * Hit's the 'upload' router endpoint, eventually attempting to 344 | * upload the user-defined zip-file to the Azure Web App 345 | * @param {boolean} propagate - Should we continue with deploying once this is done? 346 | */ 347 | uploadGhost: function (propagate) { 348 | var self = UpdaterClient.updater, 349 | nochanges = ' No changes to your site have been made.', 350 | error; 351 | 352 | this.appendLog('Uploading Ghost package to Azure Website (this might take a while)', true); 353 | 354 | $.ajax('/updater/upload').done(function(response) { 355 | 356 | if (response.error || response.statusCode >= 400) { 357 | console.log('Error: ', response); 358 | 359 | if (response.statusCode === 401) { 360 | error = 'Azure rejected the given credentials - username and password are incorrect,'; 361 | error += 'or are not correct for ' + UpdaterClient.config.url + '.' + nochanges; 362 | } else if (response.statusCode === 412) { 363 | error = 'The filesystem at ' + UpdaterClient.config.url + ' does not accept the upload of the Ghost package.'; 364 | error += nochanges; 365 | } else if (response.error.code === 'ENOTFOUND' || (response.error.message && response.error.message.indexOf('ENOTFOUND') > -1)) { 366 | error = 'Website ' + UpdaterClient.config.url + ' could not be found. Please ensure that you are connected to the Internet '; 367 | error += 'and that the address is correct and restart the updater.' + nochanges; 368 | } else { 369 | error = response.error + nochanges; 370 | } 371 | self.appendError(error); 372 | } else if (response.statusCode === 201) { 373 | self.appendLog('Ghost package successfully uploaded'); 374 | if (propagate) { 375 | self.deployScript(propagate); 376 | } 377 | } 378 | 379 | }); 380 | }, 381 | 382 | /** 383 | * Hit's the 'deploy updater' endpoint on the router, eventually 384 | * attempting to upload the updater webjobs to the Azure Web App 385 | * @param {boolean} propagate - Should we trigger the script once this is done? 386 | */ 387 | deployScript: function (propagate) { 388 | var self = this; 389 | this.appendLog('Deploying update script to Azure Website'); 390 | 391 | $.ajax('/updater/deploy').done(function(response) { 392 | if (response.statusCode >= 200 && response.statusCode <= 400) { 393 | var responseBody = JSON.parse(response.body); 394 | 395 | if (responseBody.url) { 396 | self.appendLog('Script successfully deployed (' + responseBody.name + ')'); 397 | if (propagate) { 398 | self.triggerScript(propagate); 399 | } 400 | } 401 | } 402 | }); 403 | }, 404 | 405 | /** 406 | * Hit's the 'trigger updater' endpoint on the router, eventually 407 | * attempting to trigger the 'updater' webjob on the Azure Web App 408 | * @param {boolean} propagate - Should we get the script's status once this is done? 409 | */ 410 | triggerScript: function (propagate) { 411 | var self = this; 412 | this.appendLog('Starting Update script on Azure Website', true); 413 | 414 | $.ajax('/updater/trigger').done(function(response) { 415 | if (response.statusCode >= 200 && response.statusCode <= 400) { 416 | if (propagate) { 417 | self.getScriptStatus(propagate); 418 | } 419 | } 420 | }); 421 | }, 422 | 423 | /** 424 | * Hit's the 'updater info' endpoint on the router, attempting to get 425 | * the log of the 'updater webjob'. This will only work if the script 426 | * is running. 427 | */ 428 | getScriptStatus: function () { 429 | var self = this; 430 | 431 | if (!this.scriptRunning) { 432 | this.appendLog('Trying to get status of update script on Azure Website', true); 433 | this.scriptRunning = true; 434 | } 435 | 436 | $.ajax({ 437 | url: '/updater/info', 438 | dataType: 'text' 439 | }).done(function (response) { 440 | if (response && !self.updateFinished) { 441 | clearTimeout(self.timerYellow); 442 | clearTimeout(self.timerRed); 443 | 444 | self.timerYellow = setTimeout(function () { 445 | UpdaterClient.utils.timerButton('yellow'); 446 | }, 120000); 447 | self.timerRed = setTimeout(function () { 448 | UpdaterClient.utils.timerButton('red'); 449 | }, 300000); 450 | UpdaterClient.utils.timerButton('green'); 451 | 452 | self.scriptLogTitle = self.scriptLogTitle || $('.scriptLogTitle'); 453 | self.scriptLogTitle.show(); 454 | self.scriptLog = self.scriptLog || $('#updateScriptLog'); 455 | self.scriptLog.text(response); 456 | self.scriptLog.show(); 457 | self.scriptLogArea = self.scriptLogArea || $('#updateScriptLogArea'); 458 | self.scriptLogArea.show(); 459 | self.scriptLogArea.scrollTop(self.scriptLogArea.scrollHeight); 460 | 461 | if (response.indexOf('Status changed to Success') > -1) { 462 | // We're done! 463 | self.scriptLogArea.hide(); 464 | self.scriptLogTitle.hide(); 465 | self.scriptLog.empty(); 466 | UpdaterClient.utils.timerButton('grey'); 467 | clearTimeout(self.timerYellow); 468 | clearTimeout(self.timerRed); 469 | self.appendLog('All done, your blog has been updated!', false); 470 | self.updateFinished = true; 471 | 472 | setTimeout(function() { UpdaterClient.utils.switchPanel('#updatefinished'); }, 500); 473 | } 474 | 475 | setTimeout(function() { self.getScriptStatus(); }, 800); 476 | } 477 | }).fail(function (error) { 478 | console.log(error); 479 | 480 | if (!self.updateFinished) { 481 | setTimeout(function() { self.getScriptStatus(); }, 1000); 482 | } 483 | }); 484 | 485 | }, 486 | 487 | /** 488 | * Kicks off the whole 'update Ghost' chain, involving all the methods 489 | * above. 490 | */ 491 | startInstallation: function () { 492 | UpdaterClient.utils.switchPanel('#update'); 493 | UpdaterClient.updater.uploadGhost(true); 494 | } 495 | };var UpdaterClient = UpdaterClient || {}; 496 | 497 | UpdaterClient.utils = { 498 | 499 | /** 500 | * Switch the different 'panels' the app. Poor man's SPA. 501 | * @param {object} input - Input object with target information 502 | */ 503 | switchPanel: function (input) { 504 | var panel = (input.target) ? input.target.dataset.target : input; 505 | $('.wrapper').hide(); 506 | $(panel).show(); 507 | }, 508 | 509 | /** 510 | * Append text to the log element in the DOM. 511 | * @param {string} text - The text to append 512 | * @param {boolean} loading - Are we loading? 513 | * @param {boolean} error - Is this an error? 514 | * @param {element|string} target - The target object 515 | * @return {$.append} 516 | */ 517 | appendLog: function (text, loading, error, target) { 518 | var loader = '', 519 | errorText = (error) ? 'Error: ' : ''; 520 | 521 | if ($('#loading')) { 522 | $('#loading').remove(); 523 | } 524 | 525 | loader = (loading) ? ' ' : ''; 526 | return $(target).append('

' + errorText + text + loader + '

'); 527 | }, 528 | 529 | /** 530 | * A button that indicates how long ago we've last had contact to Kudu and the 531 | * Azure Web App. This is useful because we have virtually no way of telling 532 | * if something went horribly wrong - ie connection lost, server down, datacenter 533 | * on fire, etc. 534 | * @param {string} color - The color the button should be (red/yellow/grey/green) 535 | */ 536 | timerButton: function (color) { 537 | var timerCircle = $('.circle'), 538 | timerTooltip = $('.circle > span'), 539 | textKeepTrying = '\nThis tool will keep trying to reach the website.', 540 | textRed = 'We have not heard back from the websites within the last five minutes, which can indicate a problem.' + textKeepTrying, 541 | textYellow = 'We have not heard back from the website within the last two minutes.' + textKeepTrying, 542 | textGrey = 'The connection status to your Azure Website is currently unknown.', 543 | textGreen = 'We are connected to your Azure Website.'; 544 | 545 | switch (color) { 546 | case 'red': 547 | timerCircle.css('background-color', '#e55b5b'); 548 | timerTooltip.text(textRed); 549 | break; 550 | case 'yellow': 551 | timerCircle.css('background-color', '#ffe811'); 552 | timerTooltip.text(textYellow); 553 | break; 554 | case 'grey': 555 | timerCircle.css('background-color', '#7f7f7f'); 556 | timerTooltip.text(textGrey); 557 | break; 558 | case 'green': 559 | timerCircle.css('background-color', '#799a2e'); 560 | timerTooltip.text(textGreen); 561 | break; 562 | default: 563 | break; 564 | } 565 | } 566 | 567 | };var UpdaterClient = UpdaterClient || {}; 568 | 569 | UpdaterClient.validation = { 570 | 571 | /** 572 | * One giant validation method, taking an event and running 573 | * some basic validation against a targeted input element. 574 | * @param {object} e - event 575 | */ 576 | validateConfig: function (e) { 577 | var urlRegex = /\**..(.azurewebsites.net)/, 578 | result = true, 579 | username, password, zippath, url; 580 | 581 | e = (e.target) ? e.target.id : e; 582 | 583 | switch (e) { 584 | case 'blog-url': 585 | UpdaterClient.config.url = $('#blog-url').val(); 586 | url = UpdaterClient.config.url; 587 | if (!url || !urlRegex.test(url)) { 588 | $('#blog-url').addClass('invalid'); 589 | result = false; 590 | } else if (urlRegex.test(url)) { 591 | $('#blog-url').removeClass('invalid'); 592 | } 593 | 594 | break; 595 | case 'blog-username': 596 | UpdaterClient.config.username = $('#blog-username').val(); 597 | username = UpdaterClient.config.username; 598 | if (!username) { 599 | $('#blog-username').addClass('invalid'); 600 | result = false; 601 | } else if (username) { 602 | $('#blog-username').removeClass('invalid'); 603 | } 604 | 605 | break; 606 | case 'blog-password': 607 | UpdaterClient.config.password = $('#blog-password').val(); 608 | password = UpdaterClient.config.password; 609 | if (!password) { 610 | $('#blog-password').addClass('invalid'); 611 | result = false; 612 | } else if (password) { 613 | $('#blog-password').removeClass('invalid'); 614 | } 615 | 616 | break; 617 | case 'ghost-zip': 618 | UpdaterClient.config.zippath = $('#ghost-zip').val(); 619 | zippath = UpdaterClient.config.zippath; 620 | if (!zippath) { 621 | $('#ghost-zip').addClass('invalid'); 622 | result = false; 623 | } else if (zippath) { 624 | $('#ghost-zip').removeClass('invalid'); 625 | } 626 | 627 | break; 628 | default: 629 | var testUrl = this.validateConfig('blog-url'), 630 | testPassword = this.validateConfig('blog-password'), 631 | testUsername = this.validateConfig('blog-username'), 632 | testZippath; 633 | 634 | if (UpdaterClient.config.standalone) { 635 | testZippath = this.validateConfig('ghost-zip'); 636 | } else { 637 | testZippath = true; 638 | } 639 | 640 | if (!testUrl || !testUsername || !testPassword || !testZippath) { 641 | result = false; 642 | } 643 | 644 | break; 645 | } 646 | 647 | return result; 648 | } 649 | }; --------------------------------------------------------------------------------