--------------------------------------------------------------------------------
/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 | 
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 | 
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.
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.
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 | };
--------------------------------------------------------------------------------