diff --git a/app/public-link.js b/app/public-link.js index 36231f2..bffe225 100644 --- a/app/public-link.js +++ b/app/public-link.js @@ -2,25 +2,40 @@ * Copyright (c) Jupyter Development Team. * Distributed under the terms of the Modified BSD License. */ -var config = require('./config'); - -var PUBLIC_LINK_PATTERN = config.get('PUBLIC_LINK_PATTERN'); var PROTOCOL = /\{protocol\}/g; var HOST = /\{host\}/g; var PORT = /\{port\}/g; /** - * Generates a base URL that can be used to build public links. Uses the pattern - * specified by the PUBLIC_LINK_PATTERN config parameter. - * @param {Request} req - Express request used to get protocol and hostname + * Generates a base URL that can be used to build public links. Uses a pattern, such as that + * specified by the PUBLIC_LINK_PATTERN config parameter. Eg: + * + * var url = require('public-link')(req, config.get('PUBLIC_LINK_PATTERN')) + * + * @param {Request|Object} req - Express request used to get protocol and hostname; or object + * containing `host` and `port` (both optional) properties + * @param {String} pattern - URL pattern * @return {String} public base URL - * @return {undefined} if PUBLIC_LINK_PATTERN is not set + * @return {undefined} if `pattern` is not set */ -module.exports = function(req) { - if (PUBLIC_LINK_PATTERN) { - return PUBLIC_LINK_PATTERN - .replace(PROTOCOL, req.protocol) - .replace(HOST, req.headers.host) - .replace(PORT, req.headers.host.split(':')[1] || ''); +module.exports = function(req, pattern) { + if (pattern) { + var protocol; + var host; + var port; + if (!!req.headers) { // is Express request + protocol = req.protocol; + host = req.headers.host; + port = req.headers.host.split(':')[1] || ''; + } else { + protocol = 'http'; + host = req.host || '127.0.0.1'; + port = req.port || ''; + } + + return pattern + .replace(PROTOCOL, protocol) + .replace(HOST, host) + .replace(PORT, port); } }; diff --git a/bin/jupyter-dashboard-admin b/bin/jupyter-dashboard-admin index d694fdc..8571232 100755 --- a/bin/jupyter-dashboard-admin +++ b/bin/jupyter-dashboard-admin @@ -12,6 +12,8 @@ var fs = require('fs'); var hjson = require('hjson'); var nconf = require('nconf'); var path = require('path'); +var prompt = require('prompt'); +var publicLink = require('../app/public-link'); var request = require('request'); var urljoin = require('url-join'); var yargs = require('yargs'); @@ -20,17 +22,35 @@ var yargs = require('yargs'); var defaultConfig = path.join(__dirname, '..', 'config.json'); var config = nconf.file({ file: defaultConfig, format: hjson }); -var DEFAULT_HOST = 'http://localhost:3000'; +// create default host from config options +var defaultHost = publicLink({ + host: config.get('IP'), + port: config.get('PORT') +}, config.get('PUBLIC_LINK_PATTERN')); + +// prompt settings +prompt.message = ''; +prompt.delimiter = ''; +prompt.start(); +var uploadPromptSettings = { + name: 'yesno', + message: 'Destination [{dest}] already exists. Do you want to overwrite?', + validator: /y[es]*|n[o]?/, + warning: 'Respond with yes or no', + default: 'no' +}; // common options shared by all commands var commonOpts = { 'host': { + default: defaultHost, describe: 'dashboard server host and port', - default: DEFAULT_HOST, + global: true, type: 'string' }, 'auth-token': { describe: 'authorization token for admin privelages, defaults to reading from config file', + global: true, type: 'string' } }; @@ -39,7 +59,6 @@ var commonOpts = { var clear_cache_command = { command: 'clear-cache [options]', describe: 'Clear dashboard server cache', - builder: commonOpts, handler: function(argv) { var opts = { url: urljoin(argv.host, '/_api/cache') @@ -47,7 +66,7 @@ var clear_cache_command = { sendRequest('delete', argv, opts, 200) .then(function() { - console.log('Cache reset!'); + console.log('Cache reset'); process.exit(0); }) .catch(function() { @@ -61,7 +80,9 @@ var clear_cache_command = { var delete_command = { command: 'delete [options] ', describe: 'Delete dashboard from server', - builder: commonOpts, + builder: function(yargs) { + return yargs.epilogue(' path path of dashboard to delete'); + }, handler: function(argv) { var opts = { url: urljoin(argv.host, '/_api/notebooks', argv.path) @@ -69,7 +90,7 @@ var delete_command = { sendRequest('delete', argv, opts, 204) .then(function() { - console.log('Dashboard deleted!'); + console.log('Dashboard deleted'); process.exit(0); }) .catch(function() { @@ -81,27 +102,85 @@ var delete_command = { // command to upload a dashboard var upload_command = { - command: 'upload [options] ', + command: 'upload [options] ', describe: 'Upload a dashboard to the dashboard server', - builder: commonOpts, + builder: function(yargs) { + return yargs.option({ + overwrite: { + default: false, + describe: 'overwrite existing dashboard/dir/file at ', + type: 'boolean' + } + }) + .epilogue(' file path to a local notebook\n' + + ' pathname path & name of destination dashboard;\n' + + ' should not include ".ipynb" extension'); + }, handler: function(argv) { - var datapath = path.resolve(argv.file); - var opts = { - url: urljoin(argv.host, '/_api/notebooks', argv.dest), - formData: { - file: fs.createReadStream(datapath) + // ensure that destination doesn't still include notebook extension + var dest = argv.pathname; + if (dest.endsWith('.ipynb')) { + yargs.showHelp(); + console.error('ERROR: should be the (optional) path and name of the destination dashboard -- it should not have an ".ipynb" extension.'); + process.exit(1); + } + var destUrl = urljoin(argv.host, '/dashboards', dest); + + // check is destination already exists + new Promise(function(resolve, reject) { + if (argv.overwrite) { + // user has already specified that he/she wishes to overwrite destination pathname + resolve(); + return; } - }; - sendRequest('post', argv, opts, 201) + request.get(destUrl, function(err, res, body) { + if (err) { + console.error('ERROR: failure contacting server: ' + err); + process.exit(1); + } + + if (res.statusCode === 404) { + // nothing at destination, so we can safely upload + resolve(); + return; + } + + // ask user if he/she wants to overwrite destination + uploadPromptSettings.message = uploadPromptSettings.message.replace('{dest}', destUrl); + prompt.get(uploadPromptSettings, function(err, result) { + if (err) { + console.error('ERROR: unknown error: ' + err); + process.exit(1); + } + if (result.yesno === 'y' || result.yesno === 'yes') { + resolve(); + } else { + reject(); + } + }); + }); + }) + .then(function() { + // we are good to upload + var datapath = path.resolve(argv.file); + var opts = { + url: urljoin(argv.host, '/_api/notebooks', dest), + formData: { + file: fs.createReadStream(datapath) + } + }; + + sendRequest('post', argv, opts, 201) .then(function() { - console.log('Dashboard successfully uploaded!'); + console.log('Dashboard uploaded to: ' + destUrl); process.exit(0); }) .catch(function(err) { console.error('ERROR:', 'could not upload dashboard.', err || ''); process.exit(1); }); + }); } }; @@ -131,9 +210,13 @@ function sendRequest(method, argv, opts, expectedResponseCode) { }); } -yargs.command(clear_cache_command) +var yargs = yargs.usage('Usage: $0 [options]') + .options(commonOpts) + .command(clear_cache_command) .command(delete_command) .command(upload_command) + .demand(1) .strict() - .help() - .argv; + .help('h') + .alias('h', 'help'); +var argv = yargs.argv; diff --git a/package.json b/package.json index 195c79a..7c14417 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "nconf": "^0.8.2", "passport": "^0.3.2", "passport-local": "^1.0.0", + "prompt": "^1.0.0", "request": "^2.67.0", "requirejs": "^2.1.21", "rimraf": "^2.5.2", diff --git a/routes/auth-routes.js b/routes/auth-routes.js index 1d44a6d..03b5596 100644 --- a/routes/auth-routes.js +++ b/routes/auth-routes.js @@ -14,11 +14,12 @@ var upload = require('../app/notebook-upload'); var router = require('express').Router(); var urljoin = require('url-join'); +var PUBLIC_LINK_PATTERN = config.get('PUBLIC_LINK_PATTERN'); var UPLOAD_MESSAGE = 'Notebook successfully uploaded'; /* POST /notebooks/* - upload a dashboard notebook */ router.post('/notebooks(/*)', authToken, upload, function(req, res) { - var publicLink = link(req); + var publicLink = link(req, PUBLIC_LINK_PATTERN); var resBody = { message: UPLOAD_MESSAGE, status: 201,