UNPKG

@awearsolutions/redis-commander

Version:

Redis web-based management tool written in node.js

771 lines (707 loc) 22.5 kB
'use strict'; var sf = require('sf'); var async = require('async'); var inflection = require('inflection'); var myutil = require('../util'); var foldingCharacter = ":"; var rootPattern; module.exports = function (app) { rootPattern = app.rootPattern; app.get('/apiv1/server/info', getServersInfo); app.get('/apiv1/key/:connectionId/:key/:index?', getKeyDetails); app.post('/apiv1/key/:connectionId/:key', postKey); app.post('/apiv1/keys/:connectionId/:key', postKeys); app.post('/apiv1/listvalue/', postAddListValue); app.post('/apiv1/setmember/', postAddSetMember); app.post('/apiv1/editListRow', postEditListRow); app.post('/apiv1/editSetMember', postEditSetMember); app.post('/apiv1/editZSetRow', postEditZSetRow); app.post('/apiv1/editHashRow', postEditHashRow); app.post('/apiv1/encodeString/:stringValue', encodeString); app.get('/apiv1/keystree/:connectionId/:keyPrefix', getKeysTree); app.get('/apiv1/keystree/:connectionId', getKeysTree); app.get('/apiv1/keys/:connectionId/:keyPrefix', getKeys); app.post('/apiv1/exec', postExec); app.get('/apiv1/connection', isConnected); }; function isConnected (req, res) { if (req.app.redisConnections[0]) { return res.send(true); } return res.send(false); } function getConnection (redisConnections, connectionId, callback) { var connectionIds = connectionId.split(":"); var desiredHost = connectionIds[0]; var desiredPort = connectionIds[1]; var desiredDb = connectionIds[2]; redisConnections.forEach(function (connection) { if (connection.options.host == desiredHost && connection.options.port == desiredPort && connection.options.db == desiredDb) { return callback(null, connection); } }); } function getServersInfo (req, res, next) { if (req.app.redisConnections.length > 0) { var allServerInfo = []; req.app.redisConnections.forEach(function (redisConnection, index) { getServerInfo(redisConnection, function (err, serverInfo) { if (err) { console.error(err); return next(err); } allServerInfo.push(serverInfo); if (index === req.app.redisConnections.length - 1) { var timeout = setInterval(function () { if (allServerInfo.length === req.app.redisConnections.length) { clearInterval(timeout); return res.send(JSON.stringify(allServerInfo)); } }, 100); } }); }); } else { return next("No redis connection"); } } function getServerInfo (redisConnection, callback) { redisConnection.info(function (err, serverInfo) { if (err) { console.error('getServerInfo', err); return callback(err); } var infoLines = serverInfo .split('\n') .map(function (line) { line = line.trim(); var parts = line.split(':'); return { key: inflection.humanize(parts[0]), value: parts.slice(1).join(':') }; }); var connectionInfo = { label: redisConnection.label, host: redisConnection.options.host, port: redisConnection.options.port, db: redisConnection.options.db, info: infoLines }; return callback(null, connectionInfo); }); } function postExec (req, res) { var cmd = req.body.cmd; var connectionId = req.body.connection; var redisConnection; getConnection(req.app.redisConnections, connectionId, function (err, connection) { redisConnection = connection; }); var parts = myutil.split(cmd); parts[0] = parts[0].toLowerCase(); var commandName = parts[0].toLowerCase(); if (!(commandName in redisConnection)) { return res.send('ERROR: Invalid Command'); } var args = parts.slice(1); args.push(function (err, results) { if (err) { return res.send(err.message); } return res.send(JSON.stringify(results)); }); redisConnection[commandName].apply(redisConnection, args); } function getKeyDetails (req, res, next) { var connectionId = req.params.connectionId; var key = req.params.key; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { console.log(sf('loading key "{0}" from "{1}"', key, connectionId)); redisConnection.type(key, function (err, type) { if (err) { console.error('getKeyDetails', err); return next(err); } switch (type) { case 'string': return getKeyDetailsString(key, redisConnection, res, next); case 'list': return getKeyDetailsList(key, redisConnection, req, res, next); case 'zset': return getKeyDetailsZSet(key, redisConnection, req, res, next); case 'hash': return getKeyDetailsHash(key, redisConnection, res, next); case 'set': return getKeyDetailsSet(key, redisConnection, res, next); } var details = { key: key, type: type }; res.send(JSON.stringify(details)); }); }); } function sendWithTTL(details, key, redisConnection, res) { redisConnection.ttl(key, function (err, ttl) { if (err) { // TTL is not fatal console.error(err); } myutil.encodeHTMLEntities(JSON.stringify(Object.assign({ ttl }, details)), function (string) { res.send(string); }); }); } function getKeyDetailsString (key, redisConnection, res, next) { redisConnection.get(key, function (err, val) { if (err) { console.error('getKeyDetailsString', err); return next(err); } var details = { key: key, type: 'string', value: val }; sendWithTTL(details, key, redisConnection, res); }); } function getKeyDetailsList (key, redisConnection, req, res, next) { var startIdx = parseInt(req.params.index, 10); if (typeof(startIdx) == 'undefined' || isNaN(startIdx) || startIdx < 0) { startIdx = 0; } var endIdx = startIdx + 19; redisConnection.lrange(key, startIdx, endIdx, function (err, items) { if (err) { console.error('getKeyDetailsList', err); return next(err); } var i = startIdx; items = items.map(function (item) { return { number: i++, value: item } }); redisConnection.llen(key, function (err, length) { if (err) { console.error('getKeyDetailsList', err); return next(err); } var details = { key: key, type: 'list', items: items, beginning: startIdx <= 0, end: endIdx >= length - 1, length: length }; sendWithTTL(details, key, redisConnection, res); }); }); } function getKeyDetailsHash (key, redisConnection, res, next) { redisConnection.hgetall(key, function (err, fieldsAndValues) { if (err) { console.error('getKeyDetailsHash', err); return next(err); } var details = { key: key, type: 'hash', data: fieldsAndValues }; sendWithTTL(details, key, redisConnection, res); }); } function getKeyDetailsSet (key, redisConnection, res, next) { redisConnection.smembers(key, function (err, members) { if (err) { console.error('getKeyDetailsSet', err); return next(err); } var details = { key: key, type: 'set', members: members }; sendWithTTL(details, key, redisConnection, res); }); } function getKeyDetailsZSet (key, redisConnection, req, res, next) { var startIdx = parseInt(req.params.index, 10); if (typeof(startIdx) == 'undefined' || isNaN(startIdx) || startIdx < 0) { startIdx = 0; } var endIdx = startIdx + 19; redisConnection.zrevrange(key, startIdx, endIdx, 'WITHSCORES', function (err, items) { if (err) { console.error('getKeyDetailsZSet', err); return next(err); } items = mapZSetItems(items); var i = startIdx; items.forEach(function (item) { item.number = i++; }); redisConnection.zcount(key, "-inf", "+inf", function (err, length) { var details = { key: key, type: 'zset', items: items, beginning: startIdx <= 0, end: endIdx >= length - 1, length: length }; sendWithTTL(details, key, redisConnection, res); }); }); } function postAddListValue (req, res, next) { var key = req.body.key; var value = req.body.stringValue; var type = req.body.type; var connectionId = req.body.listConnectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { addListValue(redisConnection, key, value, type, res, next); }); } function postEditListRow (req, res, next) { var key = req.body.listKey; var index = req.body.listIndex; var value = req.body.listValue; var connectionId = req.body.listConnectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { editListRow(redisConnection, key, index, value, res, next); }); } function postEditSetMember (req, res, next) { var key = req.body.setKey; var member = req.body.setMember; var oldMember = req.body.setOldMember; var connectionId = req.body.setConnectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { editSetMember(redisConnection, key, member, oldMember, res, next); }); } function postEditZSetRow (req, res, next) { var key = req.body.zSetKey; var score = req.body.zSetScore; var value = req.body.zSetValue; var oldValue = req.body.zSetOldValue; var connectionId = req.body.zSetConnectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { editZSetRow(redisConnection, key, score, value, oldValue, res, next); }); } function postEditHashRow (req, res, next) { var key = req.body.hashKey; var field = req.body.hashField; var value = req.body.hashFieldValue; var connectionId = req.body.hashConnectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { editHashRow(redisConnection, key, field, value, res, next); }); } function postAddSetMember (req, res, next) { var key = req.body.setKey; var member = req.body.setMemberName; var connectionId = req.body.setConnectionId; getConnection(req.app.redisConnections, connectionId, function(err, redisConnection) { addSetMember(redisConnection, key, member, res, next); }); } function addSetMember (redisConnection, key, member, res, next) { myutil.decodeHTMLEntities(member, function (decodedString) { member = decodedString; }); return redisConnection.sadd(key, member, function (err) { if (err) { console.error('addSetMember', err); return next(err); } res.send('ok'); }); } function addSortedSetValue (redisConnection, key, score, value, res, next) { return redisConnection.zadd(key, score, value, function (err) { if (err) { console.error('addZSetValue', err); return next(err); } res.send('ok'); }); } function addListValue (redisConnection, key, value, type, res, next) { var callback = function (err) { if (err) { console.error('addListValue', err); return next(err); } return res.send('ok'); }; myutil.decodeHTMLEntities(value, function (decodedString) { value = decodedString; }); switch (type) { case 'lpush': return redisConnection.lpush(key, value, callback); case 'rpush': return redisConnection.rpush(key, value, callback); default: var err = new Error("invalid type"); console.error('addListValue', err); return next(err); } } function editListRow (redisConnection, key, index, value, res, next) { myutil.decodeHTMLEntities(value, function (decodedString) { value = decodedString; redisConnection.lset(key, index, value, function (err) { if (err) { console.error('editListRow', err); return next(err); } if (value === "REDISCOMMANDERTOMBSTONE") { redisConnection.lrem(key, 0, value, function (err) { if (err) { console.error('removeListRow', err); return next(err); } res.send('ok'); }); } else { res.send('ok'); } }); }); } function editSetMember (redisConnection, key, member, oldMember, res, next) { myutil.decodeHTMLEntities(oldMember, function (decodedString) { oldMember = decodedString; redisConnection.srem(key, oldMember, function (err) { if (err) { console.error('editSetMember', err); return next(err); } if (member === "REDISCOMMANDERTOMBSTONE") { return res.send('ok'); } else { myutil.decodeHTMLEntities(member, function (decodedString) { member = decodedString; redisConnection.sadd(key, member, function (err) { if (err) { console.error('editSetMember', err); return next(err); } return res.send('ok'); }); }); } }); }); } function editZSetRow (redisConnection, key, score, value, oldValue, res, next) { myutil.decodeHTMLEntities(oldValue, function (decodedString) { oldValue = decodedString; redisConnection.zrem(key, oldValue, function (err) { if (err) { console.error('editZSetRow', err); return next(err); } if (value === "REDISCOMMANDERTOMBSTONE") { return res.send('ok'); } else { myutil.decodeHTMLEntities(value, function (decodedString) { value = decodedString; redisConnection.zadd(key, score, value, function (err) { if (err) { console.error('editZSetRow', err); return next(err); } return res.send('ok'); }); }); } }); }); } function editHashRow (redisConnection, key, field, value, res, next) { myutil.decodeHTMLEntities(field, function (decodedField) { myutil.decodeHTMLEntities(value, function (decodedValue) { if (value === "REDISCOMMANDERTOMBSTONE") { redisConnection.hdel(key, decodedField, function (err) { if (err) { console.error('editHashRow', err); return next(err); } return res.send('ok'); }); } else { redisConnection.hset(key, decodedField, decodedValue, function (err) { if (err) { console.error('editHashRow', err); return next(err); } return res.send('ok'); }) } }); }); } function postKey (req, res, next) { var connectionId = req.params.connectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { if (req.query.action === 'delete') { deleteKey(redisConnection, req, next, res); } else if (req.query.action === 'decode') { decodeKey(redisConnection, req, next, res); } else { saveKey(redisConnection, req, next, res); } }); } function saveKey (redisConnection, req, next, res) { var key = req.params.key; console.log(sf('saving key "{0}"', key)); redisConnection.type(key, function (err, type) { if (err) { console.error('saveKey', err); return next(err); } var value = req.body.stringValue; myutil.decodeHTMLEntities(value, function (decodedString) { value = decodedString; }); var score = parseInt(req.body.keyScore, 10); var formType = req.body.keyType; type = typeof(formType) == 'undefined' ? type : formType; switch (type) { case 'string': case 'none': return posKeyDetailsString(redisConnection, key, req, res, next); case 'list': return addListValue(redisConnection, key, value, 'lpush', res, next); case 'set': return addSetMember(redisConnection, key, value, res, next); case 'zset': return addSortedSetValue(redisConnection, key, score, value, res, next); default: return next(new Error("Unhandled type " + type)); } }); } function decodeKey (redisConnection, req, next, res) { var key = req.params.key; redisConnection.get(key, function (err, val) { if (err) { console.error('decodeKey', err); return next(err); } var decoded = "" if (typeof Buffer.toString === "function") { // Node 5.10+ decoded = Buffer(val, "base64").toString("ascii"); } else { // older Node versions decoded = new Buffer(val, "base64").toString("ascii"); } return res.send(decoded) }); } function encodeString (req, res, next) { var val = req.params.stringValue; var encoded = "" if (typeof Buffer.from === "function") { // Node 5.10+ encoded = Buffer(val).toString('base64'); } else { // older Node versions encoded = new Buffer(val).toString('base64'); } return res.send(encoded) } function deleteKey (redisConnection, req, next, res) { var key = req.params.key; console.log(sf('deleting key "{0}"', key)); redisConnection.del(key, function (err) { if (err) { console.error('deleteKey', err); return next(err); } return res.send('ok'); }); } function posKeyDetailsString (redisConnection, key, req, res, next) { var val = req.body.stringValue; myutil.decodeHTMLEntities(val, function (decodedString) { val = decodedString; console.log("after:", val); }); redisConnection.set(key, val, function (err) { if (err) { console.error('posKeyDetailsString', err); return next(err); } res.send('OK'); }); } function getKeys (req, res, next) { var connectionId = req.params.connectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { var prefix = req.params.keyPrefix; var limit = req.params.limit || 100; console.log(sf('loading keys by prefix "{0}"', prefix)); redisConnection.keys(prefix, function (err, keys) { if (err) { console.error('getKeys', err); return next(err); } console.log(sf('found {0} keys for "{1}"', keys.length, prefix)); if (keys.length > 1) { keys = myutil.distinct(keys.map(function (key) { var idx = key.indexOf(foldingCharacter, prefix.length); if (idx > 0) { return key.substring(0, idx + 1); } return key; })); } if (keys.length > limit) { keys = keys.slice(0, limit); } keys = keys.sort(); res.send(JSON.stringify(keys)); }); }); } function getKeysTree (req, res, next) { var connectionId = req.params.connectionId; var prefix = req.params.keyPrefix; console.log(sf('loading keys by prefix "{0}"', prefix)); var search; if (prefix) { search = prefix + foldingCharacter + '*'; } else { search = rootPattern; } getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { redisConnection.keys(search, function (err, keys) { if (err) { console.error('getKeys', err); return next(err); } console.log(sf('found {0} keys for "{1}"', keys.length, prefix)); var lookup = {}; var reducedKeys = []; keys.forEach(function (key) { var fullKey = key; if (prefix) { key = key.substr((prefix + foldingCharacter).length); } var parts = key.split(foldingCharacter); var firstPrefix = parts[0]; if (lookup.hasOwnProperty(firstPrefix)) { lookup[firstPrefix].count++; } else { lookup[firstPrefix] = { attr: { id: firstPrefix }, count: parts.length === 1 ? 0 : 1 }; lookup[firstPrefix].fullKey = fullKey; if (parts.length === 1) { lookup[firstPrefix].leaf = true; } reducedKeys.push(lookup[firstPrefix]); } }); reducedKeys.forEach(function (data) { if (data.count === 0) { data.data = data.attr.id; } else { data.data = data.attr.id + ":* (" + data.count + ")"; data.state = "closed"; } }); async.forEachLimit(reducedKeys, 10, function (keyData, callback) { if (keyData.leaf) { redisConnection.type(keyData.fullKey, function (err, type) { if (err) { return callback(err); } keyData.attr.rel = type; var sizeCallback = function (err, count) { if (err) { return callback(err); } else { keyData.data += " (" + count + ")"; callback(); } }; if (type == 'list') { redisConnection.llen(keyData.fullKey, sizeCallback); } else if (type == 'set') { redisConnection.scard(keyData.fullKey, sizeCallback); } else if (type == 'zset') { redisConnection.zcard(keyData.fullKey, sizeCallback); } else { callback(); } }); } else { callback(); } }, function (err) { if (err) { console.error('getKeys', err); return next(err); } reducedKeys = reducedKeys.sort(function (a, b) { return a.data > b.data ? 1 : -1; }); res.send(JSON.stringify(reducedKeys)); }); }); }); } function postKeys (req, res, next) { var key = req.params.key; var connectionId = req.params.connectionId; getConnection(req.app.redisConnections, connectionId, function (err, redisConnection) { if (req.query.action === 'delete') { deleteKeys(key, redisConnection, res, next); } else { next(new Error("Invalid action '" + req.query.action + "'")); } }); } function deleteKeys (keyQuery, redisConnection, res, next) { redisConnection.keys(keyQuery, function (err, keys) { if (err) { console.error('deleteKeys', err); return next(err); } async.forEachLimit(keys, 10, function (key, callback) { redisConnection.del(key, callback); }, function (err) { if (err) { console.error('deleteKeys', err); return next(err); } return res.send('ok'); }) }); } function mapZSetItems (items) { var results = []; for (var i = 0; i < items.length; i += 2) { results.push({ score: items[i + 1], value: items[i] }); } return results; }