Skip to content
This repository has been archived by the owner on Oct 26, 2019. It is now read-only.

Commit

Permalink
Merge pull request #265 from jhpedemonte/208-delete-cache
Browse files Browse the repository at this point in the history
Adds REST API allow clearing of all or some cached dashboards
  • Loading branch information
jhpedemonte authored Jul 29, 2016
2 parents 976ace6 + 5212523 commit b2b140c
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 47 deletions.
10 changes: 9 additions & 1 deletion app/notebook-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ function _uncache(nbpath) {
delete _cache[nbpath];
}

function _resetCache() {
_cache = {};
}

module.exports = {
/**
* Loads, parses, and returns cells (minus code) of the notebook specified by nbpath
Expand All @@ -99,5 +103,9 @@ module.exports = {
* Removes the specified notebook from the cache
* @param {String} nbpath - path of notebook to remove from cache
*/
uncache: _uncache
uncache: _uncache,
/**
* Removes all entries from the cache
*/
resetCache: _resetCache
};
41 changes: 28 additions & 13 deletions app/public-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
222 changes: 222 additions & 0 deletions bin/jupyter-dashboard-admin
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/usr/bin/env node
/**
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/

/**
* Command-line script which allows clearing of the Dashboard Server cache.
*/

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');

// Config defaults are in an HJSON file in the root of the source tree
var defaultConfig = path.join(__dirname, '..', 'config.json');
var config = nconf.file({ file: defaultConfig, format: hjson });

// 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',
global: true,
type: 'string'
},
'auth-token': {
describe: 'authorization token for admin privelages, defaults to reading from config file',
global: true,
type: 'string'
}
};

// command to clear the dashboard server cache
var clear_cache_command = {
command: 'clear-cache [options]',
describe: 'Clear dashboard server cache',
handler: function(argv) {
var opts = {
url: urljoin(argv.host, '/_api/cache')
};

sendRequest('delete', argv, opts, 200)
.then(function() {
console.log('Cache reset');
process.exit(0);
})
.catch(function() {
console.error('ERROR: ', 'could not reset cache.', err || '');
process.exit(1);
});
}
};

// command to delete a dashboard
var delete_command = {
command: 'delete [options] <path>',
describe: 'Delete dashboard from server',
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)
};

sendRequest('delete', argv, opts, 204)
.then(function() {
console.log('Dashboard deleted');
process.exit(0);
})
.catch(function() {
console.error('ERROR: ', 'could not delete dashboard.', err || '');
process.exit(1);
});
}
};

// command to upload a dashboard
var upload_command = {
command: 'upload [options] <file> <pathname>',
describe: 'Upload a dashboard to the dashboard server',
builder: function(yargs) {
return yargs.option({
overwrite: {
default: false,
describe: 'overwrite existing dashboard/dir/file at <pathname>',
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) {
// ensure that destination doesn't still include notebook extension
var dest = argv.pathname;
if (dest.endsWith('.ipynb')) {
yargs.showHelp();
console.error('ERROR: <pathname> 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;
}

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 uploaded to: ' + destUrl);
process.exit(0);
})
.catch(function(err) {
console.error('ERROR:', 'could not upload dashboard.', err || '');
process.exit(1);
});
});
}
};

function sendRequest(method, argv, opts, expectedResponseCode) {
var authToken = argv['auth-token'] || // command line
config.get('AUTH_TOKEN'); // config.json

opts.headers = opts.headers || {};

if (authToken) {
opts.headers.Authorization = 'token ' + authToken;
}

return new Promise(function(resolve, reject) {
request[method](opts, function(err, res, body) {
if (err || res.statusCode !== expectedResponseCode) {
if (!err) {
try {
var m = JSON.parse(body);
err = m.message;
} catch(e) {}
}
reject(err);
}
resolve();
});
});
}

var yargs = yargs.usage('Usage: $0 <command> [options]')
.options(commonOpts)
.command(clear_cache_command)
.command(delete_command)
.command(upload_command)
.demand(1)
.strict()
.help('h')
.alias('h', 'help');
var argv = yargs.argv;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
"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",
"serve-favicon": "^2.3.0",
"tmp": "0.0.28",
"url-join": "0.0.1",
"websocket": "^1.0.22"
"websocket": "^1.0.22",
"yargs": "^4.8.1"
},
"devDependencies": {
"chai": "^3.4.1",
Expand Down
15 changes: 14 additions & 1 deletion routes/auth-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ var authToken = require('../app/auth-token');
var config = require('../app/config');
var link = require('../app/public-link');
var nbdelete = require('../app/notebook-delete');
var nbstore = require('../app/notebook-store');
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,
Expand All @@ -32,4 +34,15 @@ router.post('/notebooks(/*)', authToken, upload, function(req, res) {
/* DELETE /notebooks/* - delete a dashboard notebook */
router.delete('/notebooks(/*)', authToken, nbdelete);

/* DELETE /cache/* - reset the cache or remove a specific entry from cache */
router.delete('/cache(/*)?', authToken, function(req, res) {
var path = req.params[0];
if (path) {
nbstore.uncache(path);
} else {
nbstore.resetCache();
}
res.sendStatus(200);
});

module.exports = router;
Loading

0 comments on commit b2b140c

Please sign in to comment.