├── .gitignore ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── frontend ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .postcssrc.js ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ ├── webpack.prod.conf.js │ └── webpack.test.conf.js ├── bun.lockb ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package.json ├── server │ └── resources │ │ └── app │ │ ├── index.html │ │ └── static │ │ ├── css │ │ └── app.e3274ae6f0d48227a7adbf9864ab8fc8.css │ │ ├── fonts │ │ ├── ionicons.143146f.woff2 │ │ ├── ionicons.99ac330.woff │ │ └── ionicons.d535a25.ttf │ │ ├── img │ │ └── ionicons.a2c4a26.svg │ │ ├── js │ │ ├── 0.4666f486b90bf79d2ef2.js │ │ ├── app.fb5ffad28309b2c7234f.js │ │ ├── manifest.8254301c87566c398b52.js │ │ └── vendor.895190540fff83b33f3d.js │ │ └── redis.svg ├── src │ ├── App.vue │ ├── RedisManager │ │ ├── Index.vue │ │ ├── api.js │ │ ├── pages │ │ │ ├── MainPage.vue │ │ │ └── components │ │ │ │ ├── clientList.vue │ │ │ │ ├── infoTabs.vue │ │ │ │ ├── modals │ │ │ │ └── addRowModal.vue │ │ │ │ ├── serverInfo.vue │ │ │ │ └── slowLog.vue │ │ ├── rdm.less │ │ └── store │ │ │ ├── actions.js │ │ │ └── index.js │ ├── i18n │ │ └── i18n.js │ ├── main.js │ ├── routers.js │ └── vue-terminal-ui │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── VueTerminal.vue │ │ └── ptty.jquery.js │ │ └── webpack.config.js └── static │ ├── .gitkeep │ └── redis.svg ├── images ├── 1-min.png ├── 2-min.png ├── 3-min.png ├── 4-min.png ├── 5-min.png ├── 6-min.png ├── 7-min.png ├── 8-min.png └── README.md ├── rdm.code-workspace ├── server ├── go.mod ├── go.sum ├── handler │ ├── actions.go │ ├── handler.go │ └── helper.go ├── icon.ico ├── main.go ├── main.manifest ├── note.syso ├── resources │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── router │ └── router.go └── windows │ ├── path.go │ ├── path_windows.go │ ├── webview_darwin.go │ ├── webview_linux.go │ └── webview_windows.go └── vbuild.vsh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | output/ 4 | server/vendor/ 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | test/unit/coverage 9 | test/e2e/reports 10 | selenium-debug.log 11 | rdm-connections.json 12 | server/data.db 13 | server/resources/app/ 14 | server/resources/dist/ 15 | bind_*.go 16 | windows.syso 17 | /server/error.log 18 | /server/rdm 19 | /server/vbuild 20 | .lh 21 | .idea 22 | *.exe 23 | vls.log 24 | build/bin 25 | node_modules 26 | frontend/dist 27 | vbuild 28 | rdm 29 | dump.rdb 30 | 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "addrow", 4 | "childs", 5 | "dblist", 6 | "HGETALL", 7 | "llen", 8 | "removekey", 9 | "rowkey", 10 | "SCARD", 11 | "slowlog", 12 | "WITHSCORES", 13 | "ZCARD", 14 | "ZRANGEBYSCORE", 15 | "zset" 16 | ] 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Redis Manager 2 | A modern Redis management tool that works on the web or desktop. 3 | Web Redis Manager is a comprehensive web-based management client for Redis databases. It provides robust features and tools to simplify the process of managing and maintaining your Redis instances. 4 | 5 |

6 | 7 |

8 | 9 | #### Manage Multiple Redis Instances 10 | 11 | With Web Redis Manager, you can manage multiple Redis instances simultaneously. This makes it easy to monitor, maintain, and manage all your Redis instances from one central location. 12 | 13 | #### Slow Log Monitoring 14 | Our tool provides slow log monitoring to help you identify and address performance issues. By monitoring your slow logs, you can identify queries that are taking a long time to execute and take steps to optimize them. 15 | 16 | #### Server Information 17 | Web Redis Manager provides detailed server information at your fingertips. You can easily view and analyze key server metrics to ensure your Redis instances are running optimally. 18 | 19 | #### Configuration Management 20 | Our tool makes it easy to view and modify your Redis configuration settings. Whether you need to adjust memory usage, set replication settings, or tweak other configuration options, you can do it all from our user-friendly interface. 21 | 22 | #### CLI Mode 23 | For power users who prefer working from the command line, we offer a CLI mode. This gives you the flexibility to manage your Redis instances in a way that suits your workflow. 24 | 25 | ### Publish/Subscribe Mode 26 | 27 | Web Redis Manager supports the publish/subscribe messaging paradigm, allowing for real-time message communication between publishers and subscribers. 28 | 29 | ### Performance Chart Monitoring 30 | 31 | Our tool offers performance chart monitoring, providing you with visual insights into your Redis instances' performance. This feature makes it easier to track and optimize the performance of your Redis databases. 32 | 33 | ### Modern Design and Interface 34 | 35 | Our clean, user-friendly interface makes it easy to manage your Redis databases. You'll have all the tools you need at your fingertips. 36 | 37 | ### Web and Desktop Availability 38 | 39 | Whether you prefer working in a web interface or a standalone desktop application, we've got you covered. Our tool works seamlessly on both platforms. 40 | 41 | ### Comprehensive Redis Management Capabilities 42 | 43 | - **Data Visualization**: Easily view and navigate your data in a visual format. 44 | - **Data Editing**: Modify your data directly within the tool. 45 | - **Performance Monitoring**: Keep track of your Redis database's performance to identify and address issues promptly. 46 | - **Security Features**: We offer features like secure password protection to help keep your data safe. 47 | 48 | ## Installation 49 | 50 | ```shell 51 | git clone --depth=1 https://github.com/xiusin/web-redis-manager.git 52 | cd web-redis-manager 53 | git checkout develop 54 | 55 | yarn # install 56 | yarn build # build 57 | 58 | cd server 59 | 60 | go mod tidy # sync deps 61 | 62 | go build -o rdm.exe # compile windows 63 | go build -o rdm # *nix 64 | 65 | # setup with gui 66 | ./rdm.exe 67 | 68 | # basic auth setup (For password authorization login on the web) 69 | ./rdm.exe --username=admin --password=123456 70 | ``` 71 | 72 | ## Screenshots ## 73 | 74 | ### setup ### 75 | 76 | ![./images/1-min.png](./images/1-min.png) 77 | 78 | ### connection ### 79 | 80 | ![./images/2-min.png](./images/2-min.png) 81 | 82 | ### key list ### 83 | 84 | ![./images/3-min.png](./images/3-min.png) 85 | 86 | ### value ### 87 | 88 | ![./images/4-min.png](./images/4-min.png) 89 | 90 | ### configure ### 91 | 92 | ![./images/5-min.png](./images/5-min.png) 93 | 94 | ### server info ### 95 | 96 | ![./images/6-min.png](./images/6-min.png) 97 | 98 | ### slow log ### 99 | 100 | ![./images/7-min.png](./images/7-min.png) 101 | 102 | ### cli mode ### 103 | 104 | ![./images/8-min.png](./images/8-min.png) 105 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] 只读模式,屏蔽隐藏修改操作 2 | - [ ] 发布订阅问题处理 (Websocket异常关闭) 3 | - [ ] 大list/set/map造成页面卡顿优化 (组件最小化 https://www.jianshu.com/p/1ea5d87e06f9) 4 | - [ ] 连接失败时自动关闭(清理节点loading状态) 5 | - [ ] 连接服务器时需要校验是否需要auth信息 6 | - [ ] list / set / stream等数据优化 7 | - [ ] list / hash / zset编辑 8 | - [ ] list / set / stream查询请求后端 9 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "env": { 8 | "test": { 9 | "presets": ["env", "stage-2"], 10 | "plugins": [ "istanbul" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // disallow check for semicolon 21 | 'semi': 0, 22 | // disable indent rule 23 | "indent": "off", 24 | // allow paren-less arrow functions 25 | 'arrow-parens': 0, 26 | // 27 | 'space-before-function-paren': ['off', 'never'], 28 | // allow async-await 29 | 'generator-star-spacing': 0, 30 | // allow debugger during development 31 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /frontend/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: () => { 35 | console.log(process.env.NODE_ENV) 36 | } 37 | }) 38 | // force page reload when html-webpack-plugin template changes 39 | compiler.plugin('compilation', function (compilation) { 40 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | hotMiddleware.publish({ action: 'reload' }) 42 | cb() 43 | }) 44 | }) 45 | 46 | // proxy api requests 47 | Object.keys(proxyTable).forEach(function (context) { 48 | var options = proxyTable[context] 49 | if (typeof options === 'string') { 50 | options = { target: options } 51 | } 52 | app.use(proxyMiddleware(options.filter || context, options)) 53 | }) 54 | 55 | // handle fallback for HTML5 history API 56 | app.use(require('connect-history-api-fallback')()) 57 | 58 | // serve webpack bundle output 59 | app.use(devMiddleware) 60 | 61 | // enable hot-reload and state-preserving 62 | // compilation error display 63 | app.use(hotMiddleware) 64 | 65 | // serve pure static assets 66 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | var uri = 'http://localhost:' + port 70 | 71 | var _resolve 72 | var readyPromise = new Promise(resolve => { 73 | _resolve = resolve 74 | }) 75 | 76 | console.log('> Starting dev server...') 77 | devMiddleware.waitUntilValid(() => { 78 | console.log('> Listening at ' + uri + '\n') 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | _resolve() 84 | }) 85 | 86 | var server = app.listen(port) 87 | 88 | module.exports = { 89 | ready: readyPromise, 90 | close: () => { 91 | server.close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | publicPath: '../../', 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | var output = [] 63 | var loaders = exports.cssLoaders(options) 64 | for (var extension in loaders) { 65 | var loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: ['babel-polyfill', './src/main.js'] 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src') 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.(js|vue)$/, 32 | loader: 'eslint-loader', 33 | enforce: 'pre', 34 | include: [resolve('src'), resolve('test')], 35 | options: { 36 | formatter: require('eslint-friendly-formatter') 37 | } 38 | }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader', 42 | options: vueLoaderConfig 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src'), resolve('test')] 48 | }, 49 | { 50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 63 | } 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = process.env.NODE_ENV === 'testing' 13 | ? require('../config/test.env') 14 | : config.build.env 15 | 16 | var webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 27 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: process.env.NODE_ENV === 'testing' 56 | ? 'index.html' 57 | : config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | minify: { 61 | removeComments: true, 62 | collapseWhitespace: true, 63 | removeAttributeQuotes: true 64 | // more options: 65 | // https://github.com/kangax/html-minifier#options-quick-reference 66 | }, 67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 68 | chunksSortMode: 'dependency' 69 | }), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module, count) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /frontend/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('../config/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /frontend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/frontend/bun.lockb -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"', 6 | }) 7 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | 5 | 6 | 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../../server/resources/app/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../../server/resources/app/'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: './', 15 | productionSourceMap: false, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: 8899, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"', 3 | API_DOMAIN: '"http://localhost:8787"' 4 | } 5 | -------------------------------------------------------------------------------- /frontend/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | RedisDesktop - A modern redis management tool 9 | 10 | 31 | 32 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redisdesktop", 3 | "version": "2.0.0", 4 | "description": "redis database mamager", 5 | "author": "xiusin", 6 | "private": false, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "browser-update": "^2.1.4", 14 | "crypto-js": "^3.3.0", 15 | "echarts": "^5.2.2", 16 | "iview": "^3.5.5-rc.1", 17 | "jquery": "^3.4.1", 18 | "md5": "^2.2.1", 19 | "sass-loader": "^7.1.0", 20 | "vue": "^2.5.21", 21 | "vue-codemirror": "^4.0.6", 22 | "vue-echarts": "^6.0.0", 23 | "vue-i18n": "^9.6.5", 24 | "vue-progressbar": "^0.7.5", 25 | "vue-resource": "^1.5.1", 26 | "vue-router": "^2.3.1", 27 | "vuex": "^2.3.1", 28 | "vuex-persistedstate": "^1.4.1", 29 | "vuex-router-sync": "^4.2.0", 30 | "x-particles": "^1.0.8" 31 | }, 32 | "devDependencies": { 33 | "@vue/composition-api": "^1.4.3", 34 | "autoprefixer": "^6.7.2", 35 | "babel-core": "^6.22.1", 36 | "babel-eslint": "^7.1.1", 37 | "babel-loader": "^6.2.10", 38 | "babel-plugin-istanbul": "^4.1.1", 39 | "babel-plugin-transform-runtime": "^6.22.0", 40 | "babel-polyfill": "^6.23.0", 41 | "babel-preset-env": "^1.3.2", 42 | "babel-preset-stage-2": "^6.22.0", 43 | "babel-register": "^6.22.0", 44 | "chai": "^3.5.0", 45 | "chalk": "^1.1.3", 46 | "chromedriver": "^2.45.0", 47 | "compression-webpack-plugin": "^9.2.0", 48 | "connect-history-api-fallback": "^1.3.0", 49 | "copy-webpack-plugin": "^4.0.1", 50 | "cross-env": "^4.0.0", 51 | "cross-spawn": "^5.0.1", 52 | "css-loader": "^0.28.0", 53 | "eslint": "^3.19.0", 54 | "eslint-config-standard": "^6.2.1", 55 | "eslint-friendly-formatter": "^2.0.7", 56 | "eslint-loader": "^1.7.1", 57 | "eslint-plugin-html": "^2.0.0", 58 | "eslint-plugin-promise": "^3.4.0", 59 | "eslint-plugin-standard": "^2.0.1", 60 | "eventsource-polyfill": "^0.9.6", 61 | "express": "^4.14.1", 62 | "extract-text-webpack-plugin": "^2.0.0", 63 | "file-loader": "^0.11.1", 64 | "friendly-errors-webpack-plugin": "^1.1.3", 65 | "html-webpack-plugin": "^2.28.0", 66 | "http-proxy-middleware": "^0.20.0", 67 | "inject-loader": "^3.0.0", 68 | "karma": "^3.1.4", 69 | "karma-coverage": "^1.1.1", 70 | "karma-mocha": "^1.3.0", 71 | "karma-phantomjs-launcher": "^1.0.2", 72 | "karma-phantomjs-shim": "^1.4.0", 73 | "karma-sinon-chai": "^1.3.1", 74 | "karma-sourcemap-loader": "^0.3.7", 75 | "karma-spec-reporter": "0.0.30", 76 | "karma-webpack": "^2.0.2", 77 | "less": "^3.9.0", 78 | "less-loader": "^4.0.4", 79 | "lolex": "^1.5.2", 80 | "mocha": "^5.2.0", 81 | "nightwatch": "^1.0.16", 82 | "opn": "^4.0.2", 83 | "optimize-css-assets-webpack-plugin": "^1.3.0", 84 | "ora": "^1.2.0", 85 | "phantomjs-prebuilt": "^2.1.14", 86 | "rimraf": "^2.6.0", 87 | "selenium-server": "^3.0.1", 88 | "semver": "^5.3.0", 89 | "shelljs": "^0.7.6", 90 | "sinon": "^2.1.0", 91 | "sinon-chai": "^2.8.0", 92 | "url-loader": "^1.1.2", 93 | "vue-loader": "^12.1.0", 94 | "vue-style-loader": "^3.0.1", 95 | "vue-template-compiler": "^2.6.14", 96 | "webpack": "^2.6.1", 97 | "webpack-bundle-analyzer": "^2.2.1", 98 | "webpack-dev-middleware": "^1.10.0", 99 | "webpack-hot-middleware": "^2.18.0", 100 | "webpack-merge": "^4.1.5" 101 | }, 102 | "engines": { 103 | "node": ">= 4.0.0", 104 | "npm": ">= 3.0.0" 105 | }, 106 | "browserslist": [ 107 | "> 1%", 108 | "last 2 versions", 109 | "not ie <= 8" 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /frontend/server/resources/app/index.html: -------------------------------------------------------------------------------- 1 | RedisDesktop - Redis客户端管理工具
-------------------------------------------------------------------------------- /frontend/server/resources/app/static/fonts/ionicons.143146f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/frontend/server/resources/app/static/fonts/ionicons.143146f.woff2 -------------------------------------------------------------------------------- /frontend/server/resources/app/static/fonts/ionicons.99ac330.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/frontend/server/resources/app/static/fonts/ionicons.99ac330.woff -------------------------------------------------------------------------------- /frontend/server/resources/app/static/fonts/ionicons.d535a25.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/frontend/server/resources/app/static/fonts/ionicons.d535a25.ttf -------------------------------------------------------------------------------- /frontend/server/resources/app/static/js/app.fb5ffad28309b2c7234f.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([2],{168:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(189),u=n.n(r),a=n(82),o=n(435),c=n(434),s=(n.n(c),n(433)),i=n.n(s),p=n(431),f=n(430),d=n.n(f),m=n(421),l=n.n(m),v=n(428),h=n.n(v),y=n(186),w=n(418),x=(n.n(w),n(185)),b=n(419);n.n(b);a.default.config.debug=!0,a.default.config.devtools=!0,a.default.config.productionTip=!0,a.default.use(o.a),a.default.use(p.a),a.default.use(d.a),a.default.use(l.a),a.default.prototype.$Message.config({duration:3});var k=new p.a({routes:y.a,scrollBehavior:function(e,t,n){if(n)return n;var r={};return e.hash&&(r.selector=e.hash),e.matched.some(function(e){return e.meta.scrollToTop})&&(r.x=0,r.y=0),r}}),$=new o.a.Store({modules:u()({},x.a.moduleName,x.a.store),plugins:[i()({storage:window.sessionStorage})]});n.i(c.sync)($,k,{moduleName:"x-router"}),new a.default({store:$,router:k,render:function(e){return e(h.a)}}).$mount("#app")},172:function(e,t,n){"use strict";var r=n(123),u=n.n(r),a=n(122),o=n.n(a),c=n(82),s=this;t.a={connectionTest:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/test",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),connectionSave:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/save",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),connectionList:function(e){return c.default.prototype.$Websocket.post("/redis/connection/list",null,e)},removeConnection:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/remove",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),connectionServer:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/server",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),removeKey:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/removekey",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),removeRow:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/removerow",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),addKey:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/addkey",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),deleteKey:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/deleteKey",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),renameKey:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/renameKey",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),flushDB:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/flushDB",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),updateKey:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/updatekey",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),sendCommand:function(e,t){return c.default.prototype.$Websocket.post("/redis/connection/command",e,t)},getCommand:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/get-command",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),pubSub:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/pubsub",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),info:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$Websocket.post("/redis/connection/info",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),serverInfo:function(){var e=o()(u.a.mark(function e(t,n){return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.default.prototype.$http.get("/redis/command",t,n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}()}},184:function(e,t,n){"use strict";var r=n(123),u=n.n(r),a=n(122),o=n.n(a),c=n(172),s=this;t.a={connectionTest:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.connectionTest(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),redisConfigSave:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.connectionSave(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),removeConnection:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.removeConnection(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),connectionList:function(e,t){e.commit;return c.a.connectionList(t)},removeKey:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.removeKey(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),removeRow:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.removeRow(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),addkey:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.addKey(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),deletekey:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.deleteKey(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),flushDB:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.flushDB(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),updateKey:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.updateKey(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),connectionServer:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.connectionServer(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),pubSub:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.pubSub(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}(),renameKey:function(){var e=o()(u.a.mark(function e(t,n){t.commit;return u.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,c.a.renameKey(n);case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,s)}));return function(t,n){return e.apply(this,arguments)}}()}},185:function(e,t,n){"use strict";var r=n(184);t.a={moduleName:"RedisManager",store:{namespaced:!0,actions:r.a}}},186:function(e,t,n){"use strict";t.a=[{path:"/",name:"index",component:function(e){return n.e(0).then(function(){var t=[n(440)];e.apply(null,t)}.bind(this)).catch(n.oe)},meta:{title:"首页",requiresAuth:!1}}]},187:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"App"}},418:function(e,t){},419:function(e,t){},420:function(e,t){},428:function(e,t,n){function r(e){n(420)}var u=n(170)(n(187),n(429),r,null,null);e.exports=u.exports},429:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("router-view")},staticRenderFns:[]}},437:function(e,t,n){n(169),e.exports=n(168)}},[437]); -------------------------------------------------------------------------------- /frontend/server/resources/app/static/js/manifest.8254301c87566c398b52.js: -------------------------------------------------------------------------------- 1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,f){for(var i,u,a,s=0,l=[];s -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/Index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/api.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | connectionTest: async (data, callback) => { 5 | return await Vue.prototype.$Websocket.post('/redis/connection/test', data, callback) 6 | }, 7 | connectionSave: async (data, callback) => { 8 | return await Vue.prototype.$Websocket.post('/redis/connection/save', data, callback) 9 | }, 10 | connectionList: (callback) => { 11 | return Vue.prototype.$Websocket.post('/redis/connection/list', null, callback) 12 | }, 13 | removeConnection: async (data, callback) => { 14 | return await Vue.prototype.$Websocket.post('/redis/connection/remove', data, callback) 15 | }, 16 | connectionServer: async (data, callback) => { 17 | return await Vue.prototype.$Websocket.post('/redis/connection/server', data, callback) 18 | }, 19 | removeKey: async (data, callback) => { 20 | return await Vue.prototype.$Websocket.post('/redis/connection/removekey', data, callback) 21 | }, 22 | removeRow: async (data, callback) => { 23 | return await Vue.prototype.$Websocket.post('/redis/connection/removerow', data, callback) 24 | }, 25 | addKey: async (data, callback) => { 26 | return await Vue.prototype.$Websocket.post('/redis/connection/addkey', data, callback) 27 | }, 28 | deleteKey: async (data, callback) => { 29 | return await Vue.prototype.$Websocket.post('/redis/connection/deleteKey', data, callback) 30 | }, 31 | renameKey: async (data, callback) => { 32 | return await Vue.prototype.$Websocket.post('/redis/connection/renameKey', data, callback) 33 | }, 34 | moveKey: async (data, callback) => { 35 | return await Vue.prototype.$Websocket.post('/redis/connection/moveKey', data, callback) 36 | }, 37 | dumpKey: async (data, callback) => { 38 | return await Vue.prototype.$Websocket.post('/redis/connection/dumpKey', data, callback) 39 | }, 40 | 41 | flushDB: async (data, callback) => { 42 | return await Vue.prototype.$Websocket.post('/redis/connection/flushDB', data, callback) 43 | }, 44 | updateKey: async (data, callback) => { 45 | return await Vue.prototype.$Websocket.post('/redis/connection/updatekey', data, callback) 46 | }, 47 | sendCommand: (data, callback) => { 48 | return Vue.prototype.$Websocket.post('/redis/connection/command', data, callback) 49 | }, 50 | getCommand: async (data, callback) => { 51 | return await Vue.prototype.$Websocket.post('/redis/connection/get-command', data, callback) 52 | }, 53 | pubSub: async (data, callback) => { 54 | return await Vue.prototype.$Websocket.post('/redis/connection/pubsub', data, callback) 55 | }, 56 | info: async (data, callback) => { 57 | return await Vue.prototype.$Websocket.post('/redis/connection/info', data, callback) 58 | }, 59 | // 获取redis服务器信息 60 | serverInfo: async (data, callback) => { 61 | return await Vue.prototype.$http.get('/redis/command', data, callback) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/pages/components/clientList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/pages/components/infoTabs.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 511 | 512 | 538 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/pages/components/modals/addRowModal.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 50 | 51 | 54 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/pages/components/serverInfo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/pages/components/slowLog.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/rdm.less: -------------------------------------------------------------------------------- 1 | @primary-color: #17233d; 2 | 3 | @border-width-base : 0px; // width of the border for a component 4 | 5 | @btn-border-radius: 0px; 6 | 7 | @btn-border-radius-small: 0px; 8 | 9 | @border-radius-base: 0px; 10 | 11 | @border-radius-small: 0px; 12 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/store/actions.js: -------------------------------------------------------------------------------- 1 | // 导入api 2 | import Api from '../api' 3 | 4 | export default { 5 | // 获取应用分类列表 6 | 'connectionTest': async ({ commit }, payload) => { 7 | // 调接口 8 | return await Api.connectionTest(payload) 9 | }, 10 | // 保存配置 11 | 'redisConfigSave': async ({ commit }, payload) => { 12 | // 调接口 13 | return await Api.connectionSave(payload) 14 | }, 15 | // 保存配置 16 | 'removeConnection': async ({ commit }, payload) => { 17 | // 调接口 18 | return await Api.removeConnection(payload) 19 | }, 20 | 'connectionList': ({ commit }, callback) => { 21 | // 调接口 22 | return Api.connectionList(callback) 23 | }, 24 | // 保存配置 25 | 'removeKey': async ({ commit }, payload) => { 26 | return await Api.removeKey(payload) 27 | }, 28 | // 保存配置 29 | 'removeRow': async ({ commit }, payload) => { 30 | return await Api.removeRow(payload) 31 | }, 32 | // 保存配置 33 | 'addkey': async ({ commit }, payload) => { 34 | return await Api.addKey(payload) 35 | }, 36 | 'deletekey': async ({ commit }, payload) => { 37 | return await Api.deleteKey(payload) 38 | }, 39 | 'flushDB': async ({ commit }, payload) => { 40 | return await Api.flushDB(payload) 41 | }, 42 | // 保存配置 43 | 'updateKey': async ({ commit }, payload) => { 44 | // 调接口 45 | return await Api.updateKey(payload) 46 | }, 47 | // 保存配置 48 | 'connectionServer': async ({ commit }, payload) => { 49 | return await Api.connectionServer(payload) 50 | }, 51 | // 发布订阅 52 | 'pubSub': async ({ commit }, payload) => { 53 | return await Api.pubSub(payload) 54 | }, 55 | // 重命名key 56 | 'renameKey': async ({ commit }, payload) => { 57 | return await Api.renameKey(payload) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/RedisManager/store/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions' 2 | 3 | export default { 4 | moduleName: 'RedisManager', 5 | store: { 6 | namespaced: true, 7 | actions 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/i18n/i18n.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cn: { 3 | connect: '连接服务器', 4 | pubsub: '发布订阅', 5 | cli: '命令行', 6 | info: '服务信息', 7 | delete: '删除', 8 | refresh: '刷新', 9 | rename: '重命名', 10 | reset_ttl: '重置TTL', 11 | edit: '编辑', 12 | create: '创建', 13 | save: '保存', 14 | cancel: '取消', 15 | close: '关闭', 16 | server: { 17 | info: '服务器信息', 18 | config: '配置信息', 19 | slowlog: '慢日志', 20 | client: '客户端', 21 | chart: '图表' 22 | } 23 | }, 24 | en: { 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { sync } from 'vuex-router-sync' 4 | import createPersistedState from 'vuex-persistedstate' 5 | import VueRouter from 'vue-router' 6 | import VueProcessbar from 'vue-progressbar' 7 | // 导入UI库 8 | import iView from 'iview' 9 | // 导入 App组件 10 | import App from './App' 11 | // 导入 系统路由 12 | import routers from './routers' 13 | import './RedisManager/rdm.less' 14 | import 'iview/dist/styles/iview.css' 15 | // import i18n from './i18n/i18n' 16 | 17 | import RedisManager from './RedisManager/store' 18 | 19 | Vue.config.debug = true 20 | 21 | Vue.config.devtools = true 22 | 23 | Vue.config.productionTip = true 24 | 25 | // 注册插件 26 | Vue.use(Vuex) 27 | 28 | // Vue.use(createI18n({ 29 | // locale: 'cn', // set locale 30 | // fallbackLocale: 'en', // set fallback locale 31 | // i18n 32 | // })) 33 | 34 | Vue.use(VueRouter) 35 | 36 | Vue.use(VueProcessbar) 37 | 38 | Vue.use(iView) 39 | 40 | // 配置 iView $Message 41 | Vue.prototype.$Message.config({ 42 | duration: 3 43 | }) 44 | 45 | // 创建 router 实例 46 | const routerInstance = new VueRouter({ 47 | routes: routers, 48 | scrollBehavior: (to, from, savedPosition) => { 49 | if (savedPosition) { 50 | return savedPosition 51 | } else { 52 | const position = {} 53 | if (to.hash) { 54 | position.selector = to.hash 55 | } 56 | if (to.matched.some(m => m.meta.scrollToTop)) { 57 | position.x = 0 58 | position.y = 0 59 | } 60 | return position 61 | } 62 | } 63 | }) 64 | 65 | // 创建 store 实例 66 | const storeInstance = new Vuex.Store({ 67 | modules: { 68 | [RedisManager.moduleName]: RedisManager.store 69 | }, 70 | plugins: [ 71 | createPersistedState({ 72 | storage: window.sessionStorage 73 | }) 74 | ] 75 | }) 76 | 77 | // router & store 同步 78 | sync(storeInstance, routerInstance, { moduleName: 'x-router' }) 79 | 80 | // 启动应用 81 | new Vue({ 82 | store: storeInstance, 83 | router: routerInstance, 84 | render: h => h(App) 85 | }).$mount('#app') 86 | -------------------------------------------------------------------------------- /frontend/src/routers.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | // 平台首页 3 | { 4 | path: '/', 5 | name: 'index', 6 | component: resolve => require(['./RedisManager/Index'], resolve), 7 | meta: { 8 | title: '首页', 9 | requiresAuth: false 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | /webpack.config.js 3 | src/src/ptty.jquery.js -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | globals: { 13 | "Vue": true, 14 | "jQuery": true 15 | }, 16 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 17 | extends: 'standard', 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'html' 21 | ], 22 | // add your custom rules here 23 | 'rules': { 24 | // disallow check for semicolon 25 | 'semi': 0, 26 | // disable indent rule 27 | "indent": "off", 28 | // allow paren-less arrow functions 29 | 'arrow-parens': 0, 30 | 'space-before-function-paren': ['off', 'never'], 31 | // allow async-await 32 | 'generator-star-spacing': 0, 33 | // allow debugger during development 34 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 35 | 'no-unused-vars': 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/index.js: -------------------------------------------------------------------------------- 1 | import VueTerminal from './src/VueTerminal.vue' 2 | 3 | export default VueTerminal 4 | 5 | export {VueTerminal} 6 | -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-terminal-ui", 3 | "version": "0.1.6", 4 | "description": "terminal UI for VueJs", 5 | "main": "index.js", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "lint": "eslint --ext .vue src", 9 | "build": "webpack" 10 | }, 11 | "dependencies": { 12 | "jquery": "^3.3.1", 13 | "vue": "^2.5.16" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^3.0.0-beta.12", 17 | "@vue/cli-plugin-eslint": "^3.0.0-beta.12", 18 | "@vue/cli-service": "^3.0.0-beta.12", 19 | "babel-preset-latest": "^6.24.1", 20 | "eslint": "^3.14.1", 21 | "eslint-config-standard": "^6.2.1", 22 | "eslint-friendly-formatter": "^2.0.7", 23 | "eslint-loader": "^1.6.1", 24 | "eslint-plugin-html": "^2.0.1", 25 | "eslint-plugin-promise": "^3.4.0", 26 | "eslint-plugin-standard": "^2.0.1", 27 | "uglify-es-webpack-plugin": "^0.10.0", 28 | "uglifyjs-webpack-plugin": "^0.4.6", 29 | "vue-template-compiler": "^2.5.16", 30 | "vue-loader": "^12.0.3", 31 | "webpack": "^3.4.1" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "rules": {}, 43 | "parserOptions": { 44 | "parser": "babel-eslint" 45 | } 46 | }, 47 | "postcss": { 48 | "plugins": { 49 | "autoprefixer": {} 50 | } 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/shershen08/vue-terminal-ui.git" 55 | }, 56 | "author": "Mikhail Kuznetcov", 57 | "bugs": { 58 | "url": "https://github.com/shershen08/vue-terminal-ui/issues" 59 | }, 60 | "homepage": "https://github.com/shershen08/vue-terminal-ui", 61 | "keywords": [ 62 | "vue", 63 | "vuejs", 64 | "terminal", 65 | "component" 66 | ], 67 | "license": "MIT", 68 | "browserslist": [ 69 | "> 1%", 70 | "last 2 versions", 71 | "not ie <= 8" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/src/VueTerminal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 112 | 113 | 362 | -------------------------------------------------------------------------------- /frontend/src/vue-terminal-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const UglifyEsPlugin = require('uglify-es-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | 4 | const libraryName = 'vue-terminal-ui'; 5 | const buildTarget = process.env.TARGET === 'window' ? 'window' : 'umd'; 6 | const outputFile = `${libraryName}-${buildTarget}.js`; 7 | 8 | module.exports = { 9 | entry: './index.js', 10 | output: { 11 | path: __dirname, 12 | filename: 'dist/vue-terminal-ui.js', 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.vue$/, 18 | loader: 'vue-loader' 19 | },{ 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: ['latest'] 25 | } 26 | }] 27 | }, 28 | output: { 29 | path: __dirname + '/dist', 30 | filename: outputFile, 31 | library: libraryName, 32 | libraryTarget: buildTarget, 33 | umdNamedDefine: true 34 | }, 35 | plugins: [ 36 | new UglifyEsPlugin(), 37 | new webpack.BannerPlugin({ 38 | banner: "Vue.js Terminal UI emulator \n https://github.com/shershen08/vue-terminal-ui/ \n file:[file]" 39 | }) 40 | ] 41 | } -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/frontend/static/.gitkeep -------------------------------------------------------------------------------- /frontend/static/redis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/1-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/1-min.png -------------------------------------------------------------------------------- /images/2-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/2-min.png -------------------------------------------------------------------------------- /images/3-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/3-min.png -------------------------------------------------------------------------------- /images/4-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/4-min.png -------------------------------------------------------------------------------- /images/5-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/5-min.png -------------------------------------------------------------------------------- /images/6-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/6-min.png -------------------------------------------------------------------------------- /images/7-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/7-min.png -------------------------------------------------------------------------------- /images/8-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/images/8-min.png -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | images -------------------------------------------------------------------------------- /rdm.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "extensions": { 8 | "recommendations": [ 9 | "alefragnani.project-manager", // 项目管理 10 | "GuodongSun.vscode-git-cruise", // git 历史管理 11 | "maciejdems.add-to-gitignore", // 添加到忽略 12 | "donjayamanne.githistory", // 文件历史记录 13 | "JanBn.git-last-commit-message", // 快速提交 14 | "k--kato.intellij-idea-keybindings", // idea 快捷键 15 | "anweber.vscode-httpyac", // http 接口测试 16 | "saber2pr.file-git-history", // 文件历史记录 17 | "mk12.better-git-line-blame", // git blame 18 | "alefragnani.Bookmarks", // 书签 19 | "MS-CEINTL.vscode-language-pack-zh-hans", // 中文 20 | "streetsidesoftware.code-spell-checker", // 拼写检查 21 | "jgclark.vscode-todo-highlight", // 高亮todo 22 | "be5invis.vscode-icontheme-nomo-dark", // icon图标 23 | "eenaree.webstorm-new-dark", // 主题 24 | "golang.go", // golang 25 | "ohanedan.lowlight-go-errors", // 错误低亮 26 | "whosydd.go-tags", // 为结构体快捷增加标签 27 | "farnetani.ddl-to-go-struct", // 将DDL转换为结构体 28 | "usernamehw.errorlens", // 错误提示 29 | "wero.go-struct-tag", // 为结构体增加标签 30 | "maracko.json-to-go", // 将json转换为结构体 31 | "chadalen.vscode-jetbrains-icon-theme", 32 | "zerefdev.todo-highlighter", 33 | "whosydd.go-impl", 34 | ] 35 | }, 36 | "settings": { 37 | "editor.quickSuggestions": { 38 | "strings": true 39 | }, 40 | "editor.suggest.showWords": true, 41 | "editor.lineHeight": 1.5, 42 | "gopls": { 43 | "analyses": { 44 | "composites": false // 忽略指定优化警告 45 | }, 46 | "ui.semanticTokens": true, 47 | }, 48 | "files.exclude": { 49 | "**/.git": true, 50 | "**/.svn": true, 51 | "**/.hg": true, 52 | "**/CVS": true, 53 | "**/.DS_Store": true, 54 | "**/Thumbs.db": true, 55 | "docs": true, 56 | "deployments": true, 57 | "configs/recom_cc.yml": true, 58 | "scripts": true, 59 | "Makefile": true, 60 | "Jenkinsfile": true, 61 | ".golangci.yml": true, 62 | "go.sum": true, 63 | "**/__debug_bin*": true 64 | }, 65 | "errorLens.enabledDiagnosticLevels": [ 66 | "error", 67 | "warning", 68 | ], 69 | "go.lintTool": "revive", // 选择lint 70 | "cSpell.ignoreWords": [ 71 | "ifnull" 72 | ], 73 | "editor.fontSize": 12, 74 | "go.testTimeout": "120s", 75 | }, 76 | "launch": { 77 | "version": "0.2.0", 78 | "configurations": [ 79 | { 80 | "name": "调试项目", 81 | "type": "go", 82 | "request": "launch", 83 | "mode": "auto", 84 | "program": "${workspaceFolder}/server/main.go" 85 | }, 86 | ] 87 | } 88 | } -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiusin/rdm/server 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/Tim-Paik/webview2 v0.1.9 7 | github.com/gomodule/redigo v1.9.2 8 | github.com/gorilla/websocket v1.4.2 9 | github.com/kataras/basicauth v0.0.1 10 | github.com/rs/cors v1.7.0 11 | github.com/xiusin/logger v0.0.10-0.20220103084022-9cb8378d9b03 12 | golang.org/x/sys v0.28.0 // indirect 13 | ) 14 | 15 | require github.com/webview/webview_go v0.0.0-20240220051247-56f456ca3a43 16 | 17 | require ( 18 | github.com/fatih/color v1.13.0 // indirect 19 | github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d // indirect 20 | github.com/gopherjs/gopherjs v1.17.2 // indirect 21 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 22 | github.com/mattn/go-colorable v0.1.12 // indirect 23 | github.com/mattn/go-isatty v0.0.14 // indirect 24 | golang.org/x/crypto v0.31.0 // indirect 25 | golang.org/x/term v0.27.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /server/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/Tim-Paik/webview2 v0.1.9 h1:f5oAz3ho/Uyv9wpHZxuaxKjGFnNyrH+za0Tt3+JxbNY= 18 | github.com/Tim-Paik/webview2 v0.1.9/go.mod h1:sjxONP45g3420kYedNpWDAE/ggSb1Xhz7ZxZ4lIJfxM= 19 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 20 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 21 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 22 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 23 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 24 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 25 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 26 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 27 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 28 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 29 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 30 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 31 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 32 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 33 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 34 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 35 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 36 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 38 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 40 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 41 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 42 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 43 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 44 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 45 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 46 | github.com/gen2brain/dlgs v0.0.0-20210609125024-bf6c92aaa984/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU= 47 | github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d h1:dHYKX8CBAs1zSGXm3q3M15CLAEwPEkwrK1ed8FCo+Xo= 48 | github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU= 49 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 50 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 51 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 52 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 53 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 54 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 55 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 56 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 57 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 58 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 59 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 60 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 61 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 62 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= 66 | github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= 67 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 68 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 69 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 70 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 71 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 72 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 73 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 74 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 75 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 76 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 77 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 78 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 79 | github.com/gopherjs/gopherjs v0.0.0-20210621113107-84c6004145de/go.mod h1:MtKwTfDNYAP5EtbQSMYjTSqvj1aXJKQRASWq3bwaP+g= 80 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 81 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 82 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 83 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 84 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 85 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 86 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 87 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 88 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 89 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 90 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 91 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 92 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 93 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 94 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 95 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 96 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 97 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 98 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 99 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 100 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 101 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 102 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 103 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 104 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 105 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 106 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 107 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 108 | github.com/jchv/go-winloader v0.0.0-20210323001710-152514a7f070/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 109 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= 110 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 111 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 112 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 113 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 114 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 115 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 116 | github.com/kataras/basicauth v0.0.1 h1:6y/CgLMEJ37yC7tTkqqOZGAnPoSobJ/0sNXFg93p3xc= 117 | github.com/kataras/basicauth v0.0.1/go.mod h1:YvvBoEL0rPO1qPWefeTMUM+UDU5pemmTTf5AfTvc2PI= 118 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 119 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 120 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 121 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 122 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 123 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 124 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 125 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 126 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 127 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 128 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 129 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 130 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 131 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 132 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 133 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 134 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 135 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 136 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 137 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 138 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 139 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 140 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 141 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 142 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 143 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 144 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 145 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 146 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 147 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 148 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 149 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 150 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 151 | github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 152 | github.com/nsf/bin2go v0.0.0-20111230051807-37ed6a4f29b0/go.mod h1:Xr1R4SlgUz7N40dMSVMy+BjpiUhS1zP3/9dkKKLVq/A= 153 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 154 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 155 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 156 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 157 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 158 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 159 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 160 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 161 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 162 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 163 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 164 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 165 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 166 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 167 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 168 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 169 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 170 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 171 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 172 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 173 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 174 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 175 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 176 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 177 | github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 178 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 179 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 180 | github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 181 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 182 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 183 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 184 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 185 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 186 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 187 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 188 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 189 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 190 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 191 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 192 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 193 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 194 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 195 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 196 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 197 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 198 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 199 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 200 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 201 | github.com/webview/webview_go v0.0.0-20240220051247-56f456ca3a43 h1:PwbNdNumoKba+ZgrE6ZpSluJfNJfyMuqwVyUB5+iLDI= 202 | github.com/webview/webview_go v0.0.0-20240220051247-56f456ca3a43/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk= 203 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 204 | github.com/xiusin/logger v0.0.10-0.20220103084022-9cb8378d9b03 h1:o3vENhiqDP4DlqK2AEf4Qr7nCZhvfT9UWdDfhaApPPs= 205 | github.com/xiusin/logger v0.0.10-0.20220103084022-9cb8378d9b03/go.mod h1:fEzFJfh865KMxYsx3maW7dRFEubGNcZy6wbzAqVMfF8= 206 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 207 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 208 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 209 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 210 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 211 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 212 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 213 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 214 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 215 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 216 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 217 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 218 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 219 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 220 | golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 221 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 222 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 223 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 224 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 225 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 226 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 227 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 228 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 229 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 230 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 231 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 232 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 233 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 234 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 235 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 236 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 237 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 238 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 239 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 240 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 241 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 242 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 243 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 244 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 246 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 247 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 248 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 249 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 250 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 251 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 252 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 253 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 254 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 255 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 256 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 257 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 258 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 259 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 260 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 261 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 263 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 264 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 265 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 267 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 268 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 269 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 270 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 271 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 272 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 273 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 274 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 275 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 277 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 291 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 292 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 293 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 294 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 295 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 296 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 297 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 298 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 299 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 300 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 301 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 302 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 303 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 304 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 305 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 306 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 307 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 308 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 309 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 310 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 311 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 312 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 313 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 314 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 315 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 316 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 317 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 318 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 319 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 320 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 321 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 322 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 323 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 324 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 325 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 326 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 327 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 328 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 329 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 330 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 331 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 332 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 333 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 334 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 335 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 336 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 337 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 338 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 339 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 340 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 341 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 342 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 343 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 344 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 345 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 346 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 347 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 348 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 349 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 350 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 351 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 352 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 353 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 354 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 355 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 356 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 357 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 358 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 359 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 360 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 361 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 362 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 363 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 364 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 365 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 366 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 367 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 368 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 369 | -------------------------------------------------------------------------------- /server/handler/actions.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/gob" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "reflect" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/gorilla/websocket" 17 | "github.com/xiusin/logger" 18 | 19 | "github.com/gomodule/redigo/redis" 20 | ) 21 | 22 | var pubSubs = map[string]bool{} 23 | var cliClients = cliConns{conns: map[string]redis.Conn{}} 24 | 25 | type slowLog struct { 26 | UsedTime string `json:"used_time"` 27 | Command string `json:"command"` 28 | Time string `json:"time"` 29 | } 30 | 31 | type ConnectionListItem struct { 32 | ID int64 `json:"id"` 33 | Title string `json:"title"` 34 | } 35 | 36 | type cliConns struct { 37 | sync.Mutex 38 | conns map[string]redis.Conn 39 | } 40 | 41 | var loc, _ = time.LoadLocation("PRC") 42 | 43 | func RedisManagerGetInfo(data RequestData) string { 44 | client, _ := getRedisClient(data, false, false) 45 | defer client.Close() 46 | d, err := redis.String(client.Do("INFO")) 47 | ThrowIf(err) 48 | 49 | c, err := redis.Strings(client.Do("CONFIG", "GET", "*")) 50 | ThrowIf(err) 51 | 52 | logs, err := redis.Values(client.Do("SLOWLOG", "GET", 50)) 53 | ThrowIf(err) 54 | 55 | structLogs := []slowLog{} 56 | for _, log := range logs { 57 | var sl slowLog 58 | for k, val := range log.([]interface{}) { 59 | if k == 1 { 60 | sl.Time = time.Unix(val.(int64), 0).In(loc).Format("2006-01-02 15:04:05") 61 | } else if k == 2 { 62 | sl.UsedTime = strconv.Itoa(int(val.(int64))) 63 | } else if k == 3 { 64 | sl.Command = strings.TrimRight(strings.TrimLeft(fmt.Sprintf("%s", val), "["), "]") 65 | } 66 | } 67 | structLogs = append(structLogs, sl) 68 | } 69 | 70 | return JSON(ResponseData{SuccessCode, "保存成功", RequestData{"data": d, "config": c, "slowLogs": structLogs}}) 71 | } 72 | 73 | func RedisManagerConnectionTest(data RequestData) string { 74 | var config connection 75 | config.Ip = data["ip"].(string) 76 | config.Username = data["username"].(string) 77 | config.Port = data["port"].(string) 78 | config.Auth = data["auth"].(string) 79 | 80 | client, err := redis.Dial("tcp", fmt.Sprintf("%s:%s", config.Ip, config.Port)) 81 | ThrowIf(err) 82 | defer client.Close() 83 | if config.Auth != "" { 84 | if config.Username != "" { 85 | _, err = client.Do("AUTH", config.Username, config.Auth) 86 | } else { 87 | _, err = client.Do("AUTH", config.Auth) 88 | } 89 | ThrowIf(err) 90 | } 91 | 92 | _, err = client.Do("PING") 93 | ThrowIf(err) 94 | return JSON(ResponseData{SuccessCode, "连接成功", nil}) 95 | } 96 | 97 | func RedisManagerConfigSave(data RequestData) string { 98 | var config connection 99 | config.Ip = data["ip"].(string) 100 | config.Title = data["title"].(string) 101 | config.Port = data["port"].(string) 102 | config.Auth = data["auth"].(string) 103 | config.Username = data["username"].(string) 104 | config.Readonly, _ = strconv.ParseBool(data["readonly"].(string)) 105 | totalConnection = totalConnection + 1 106 | config.ID = int64(totalConnection) 107 | ThrowIf(config.Title == "", "名称不能为空") 108 | 109 | for _, conn := range connectionList { 110 | if conn.Ip == config.Ip && conn.Port == config.Port { 111 | return JSON(ResponseData{FailedCode, "已经存在相同的连接, 名称为: " + conn.Title, nil}) 112 | } 113 | } 114 | connectionList = append(connectionList, config) 115 | ThrowIf(writeConfigJSON()) 116 | return JSON(ResponseData{SuccessCode, "保存成功", config}) 117 | } 118 | 119 | func RedisManagerConnectionList(_ RequestData) string { 120 | ThrowIf(readConfigJSON()) 121 | var conns = []ConnectionListItem{} 122 | for _, conn := range connectionList { 123 | conns = append(conns, ConnectionListItem{ID: conn.ID, Title: conn.Title}) 124 | } 125 | return JSON(ResponseData{SuccessCode, "获取列表成功", conns}) 126 | } 127 | 128 | func getFromInterfaceOrFloat64ToInt(id interface{}) int { 129 | switch id := id.(type) { 130 | case float64: 131 | return int(id) 132 | case string: 133 | idInfo, _ := strconv.Atoi(id) 134 | return idInfo 135 | default: 136 | panic(fmt.Errorf("invalid type: %T", id)) 137 | } 138 | } 139 | 140 | func RedisManagerRenameKey(data RequestData) string { 141 | client, key := getRedisClient(data, true, true) 142 | defer client.Close() 143 | 144 | newKey := data["newKey"].(string) 145 | ThrowIf(len(newKey) == 0, "new key is empty") 146 | 147 | resp, _ := client.Do("EXISTS", newKey) 148 | ThrowIf(resp.(int64) != 0, "new key already exists") 149 | 150 | _, err := client.Do("RENAME", key, newKey) 151 | ThrowIf(err) 152 | return JSON(ResponseData{SuccessCode, "重命名成功", nil}) 153 | } 154 | 155 | func RedisManagerMoveKey(data RequestData) string { 156 | client, key := getRedisClient(data, true, true) 157 | defer client.Close() 158 | 159 | _, err := client.Do("MOVE", key, data["todb"].(string)) 160 | ThrowIf(err) 161 | 162 | return JSON(ResponseData{SuccessCode, "移动成功", nil}) 163 | } 164 | 165 | func RedisPubSub(data RequestData) string { 166 | wsIntf := data["ws"] 167 | channelPrefix := "channel-" 168 | var ws *websocket.Conn 169 | if wsIntf != nil { 170 | channelPrefix = "ws-channel-" 171 | ws = wsIntf.(*websocket.Conn) 172 | } else { 173 | ws = nil 174 | } 175 | client, _ := getRedisClient(data, false, false) 176 | id := getFromInterfaceOrFloat64ToInt(data["id"]) 177 | channels, err := redis.Strings(client.Do("PUBSUB", "channels")) 178 | ThrowIf(err) 179 | ok := pubSubs[fmt.Sprintf("%s%d", channelPrefix, id)] 180 | 181 | // 检查订阅所有通道 182 | if (ws != nil) && !ok { 183 | pubSubs[fmt.Sprintf("%s%d", channelPrefix, id)] = true 184 | go func() { 185 | defer func(id int) { 186 | pubSubs[fmt.Sprintf("%s%d", channelPrefix, id)] = false 187 | }(id) 188 | pubsub := redis.PubSubConn{Conn: client} 189 | if err := pubsub.PSubscribe("*"); err != nil { 190 | logger.Warning(err) 191 | return 192 | } 193 | for { 194 | message, err := client.Receive() 195 | if err == nil { 196 | fmt.Println(string(message.([]byte)), err) 197 | switch v := message.(type) { 198 | case redis.Message: //单个订阅subscribe 199 | retData := map[string]string{ 200 | "data": string(v.Data), 201 | "id": strconv.Itoa(id), 202 | "channel": v.Channel, 203 | "time": time.Now().In(loc).Format("15:04:05"), 204 | } 205 | if ws != nil { 206 | resultValue, _ := json.Marshal(&retData) 207 | if err := ws.WriteMessage(websocket.TextMessage, resultValue); err != nil { 208 | logger.Warning(err) 209 | return 210 | } 211 | } 212 | case error: 213 | logger.Warning(v) 214 | return 215 | default: 216 | } 217 | } else { 218 | fmt.Println(err) 219 | } 220 | 221 | } 222 | }() 223 | } 224 | 225 | // 获取所有通道列表, 如果通道还没订阅那么就开启订阅协程 226 | if channel, ok := data["channel"]; ok { 227 | msg := data["msg"] 228 | ThrowIf(msg == "" || channel == "", "发布内容失败") 229 | var flag bool // 先查看是否有消费者订阅频道 230 | for _, ch := range channels { 231 | if ch == channel { 232 | flag = true 233 | break 234 | } 235 | } 236 | if !flag { 237 | go func() { 238 | client, _ := getRedisClient(data, false, false) 239 | defer client.Close() 240 | pubsub := redis.PubSubConn{Conn: client} 241 | if err := pubsub.Subscribe(channel); err != nil { 242 | panic(err) 243 | } 244 | for range time.Tick(time.Second * 10) { 245 | pubsub.ReceiveWithTimeout(time.Second) 246 | } 247 | }() 248 | } 249 | _, err := client.Do("PUBLISH", channel, msg) 250 | ThrowIf(err) 251 | return JSON(ResponseData{SuccessCode, "发布内容成功", nil}) 252 | } 253 | 254 | return JSON(ResponseData{SuccessCode, SuccessMsg, channels}) 255 | } 256 | 257 | func RedisManagerCommand(data RequestData) string { 258 | cliClients.Lock() 259 | defer cliClients.Unlock() 260 | id := strconv.Itoa(getFromInterfaceOrFloat64ToInt(data["id"])) 261 | conn, ok := cliClients.conns[id] 262 | if !ok || conn == nil || conn.Err() != nil { 263 | conn, _ = getRedisClient(data, false, false) 264 | cliClients.conns[id] = conn 265 | } 266 | command, ok := data["command"] 267 | ThrowIf(!ok, "command empty!") 268 | 269 | var commands []interface{} 270 | 271 | err := json.Unmarshal([]byte(command.(string)), &commands) 272 | ThrowIf(err) 273 | 274 | var flags []interface{} 275 | for _, v := range commands[1:] { 276 | rightfulParam := strings.Replace(v.(string), "\"", "\\\"", -1) 277 | rightfulParam = strings.Replace(rightfulParam, "'", "\\'", -1) 278 | flags = append(flags, rightfulParam) 279 | } 280 | val, err := conn.Do(commands[0].(string), flags...) 281 | if err != nil { 282 | return JSON(ResponseData{SuccessCode, SuccessMsg, fmt.Sprintf("(error) %s", err)}) 283 | } 284 | if val == nil { 285 | return JSON(ResponseData{SuccessCode, SuccessMsg, `(nil)`}) 286 | } 287 | switch val := val.(type) { 288 | case []byte, string: 289 | res, _ := redis.String(val, nil) 290 | return JSON(ResponseData{SuccessCode, SuccessMsg, res}) 291 | 292 | case int64, int, int32: 293 | res, _ := redis.Int64(val, nil) 294 | return JSON(ResponseData{SuccessCode, SuccessMsg, fmt.Sprintf("(integer) %d", res)}) 295 | 296 | case []interface{}: 297 | var ret = "" 298 | parseInterfaces(val, &ret) 299 | return JSON(ResponseData{SuccessCode, SuccessMsg, ret}) 300 | default: 301 | return JSON(ResponseData{SuccessCode, SuccessMsg, val}) 302 | } 303 | } 304 | 305 | func parseInterfaces(val []interface{}, target *string) { 306 | var strs = []string{} 307 | if len(val) == 0 { 308 | *target = "(empty list or set)" 309 | return 310 | } 311 | var i int 312 | strList, err := redis.ByteSlices(val, nil) 313 | if err == nil { 314 | for _, v := range strList { 315 | fmt.Println(reflect.TypeOf(v)) 316 | i++ 317 | strs = append(strs, fmt.Sprintf("%d) \"%s\"", i, string(v))) 318 | } 319 | *target = strings.Join(strs, "
") 320 | return 321 | } 322 | res, err := redis.StringMap(val, nil) 323 | if err == nil { 324 | for k, v := range res { 325 | i++ 326 | if ok, _ := regexp.MatchString("^d+$", k); ok { 327 | strs = append(strs, strconv.Itoa(i)+") "+k) 328 | } else { 329 | strs = append(strs, strconv.Itoa(i)+`) "`+k+`"`) 330 | } 331 | i++ 332 | strs = append(strs, strconv.Itoa(i)+") \""+v+"\"") 333 | } 334 | *target = strings.Join(strs, "
") 335 | return 336 | } 337 | deepLoopInterfaces(val, &strs, 0, "") 338 | *target = strings.Join(strs, "
") 339 | } 340 | 341 | func deepLoopInterfaces(val []interface{}, strs *[]string, level int, prefix string) { 342 | if level == 10 { 343 | return 344 | } 345 | for k, v := range val { 346 | item := "" 347 | if k == 0 && prefix != "" { 348 | item += prefix 349 | } 350 | item += strings.Repeat(" ", level) 351 | switch v := v.(type) { 352 | case []byte: 353 | *strs = append(*strs, fmt.Sprintf("%s%d) %s", item, k+1, string(v))) 354 | case string: 355 | *strs = append(*strs, fmt.Sprintf("%s%d) %s", item, k+1, v)) 356 | case int, int8, int32, int64: 357 | *strs = append(*strs, fmt.Sprintf("%s%d) %d", item, k+1, v)) 358 | case []interface{}: 359 | exp, _ := regexp.Compile(" +") 360 | item = exp.ReplaceAllString(item, " ") 361 | deepLoopInterfaces(v, strs, level+1, fmt.Sprintf("%s%d) ", item, k+1)) 362 | default: 363 | *strs = append(*strs, fmt.Sprintf("%s,%+v", reflect.TypeOf(v), v)) 364 | } 365 | } 366 | } 367 | 368 | func RedisManagerRemoveConnection(data RequestData) string { 369 | var configs []connection 370 | id := int64(getFromInterfaceOrFloat64ToInt(data["id"])) 371 | if id == 0 { 372 | return JSON(ResponseData{SuccessCode, FailedMsg, nil}) 373 | } 374 | for _, v := range connectionList { 375 | if v.ID != id { 376 | configs = append(configs, v) 377 | } 378 | } 379 | connectionList = configs 380 | ThrowIf(writeConfigJSON()) 381 | return JSON(ResponseData{SuccessCode, SuccessMsg, nil}) 382 | } 383 | 384 | var redisPools = map[int64]*redis.Pool{} 385 | 386 | func GetServerCfg(data RequestData) (connection, error) { 387 | var config = connection{} 388 | if len(connectionList) == 0 { 389 | _ = readConfigJSON() 390 | } 391 | id := getFromInterfaceOrFloat64ToInt(data["id"]) 392 | config.ID = int64(id) 393 | for _, v := range connectionList { 394 | if v.ID == config.ID { 395 | config = v 396 | break 397 | } 398 | } 399 | ThrowIf(config.Title == "", "no connection") 400 | return config, nil 401 | } 402 | 403 | func getRedisClient(data RequestData, getSelectedIndexClient bool, getKey bool) (redis.Conn, string) { 404 | var pool *redis.Pool 405 | var ok bool 406 | config, err := GetServerCfg(data) 407 | ThrowIf(err) 408 | 409 | if pool, ok = redisPools[config.ID]; !ok { 410 | pool = &redis.Pool{ 411 | Dial: func() (conn redis.Conn, err error) { 412 | conn, err = redis.Dial("tcp", config.Ip+":"+config.Port) 413 | ThrowIf(err) 414 | if config.Auth != "" { 415 | if config.Username != "" { 416 | _, err = conn.Do("AUTH", config.Username, config.Auth) 417 | } else { 418 | _, err = conn.Do("AUTH", config.Auth) 419 | } 420 | ThrowIf(err) 421 | } 422 | conn.Do("CLIENT", "SETNAME", fmt.Sprintf("RDM:(%d):CLIENT(%d)", config.ID, rand.Intn(19999))) 423 | return conn, nil 424 | }, 425 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 426 | if time.Since(t) >= 3*time.Minute { 427 | c.Do("PING") 428 | } 429 | return nil 430 | }, 431 | MaxIdle: 3, MaxActive: 0, IdleTimeout: time.Minute * 10, Wait: true, 432 | } 433 | redisPools[config.ID] = pool 434 | } 435 | client := pool.Get() 436 | var index = 0 437 | if getSelectedIndexClient { 438 | index = getFromInterfaceOrFloat64ToInt(data["index"]) 439 | } 440 | _, _ = client.Do("SELECT", index) 441 | var key string 442 | if getKey { 443 | key = data["key"].(string) 444 | ThrowIf(key == "", "please select the key to operate") 445 | } else { 446 | key = "" 447 | } 448 | return client, key 449 | } 450 | 451 | func RedisManagerConnectionServer(data RequestData) string { 452 | client, _ := getRedisClient(data, false, false) 453 | defer client.Close() 454 | var err error 455 | action := strings.Trim(data["action"].(string), " ") 456 | switch action { 457 | case "get_value": 458 | index := getFromInterfaceOrFloat64ToInt(data["index"]) 459 | _, err = client.Do("SELECT", index) 460 | ThrowIf(err) 461 | key := data["key"].(string) 462 | ThrowIf(key == "", FailedMsg) 463 | typeStr, _ := redis.String(client.Do("TYPE", key)) 464 | if typeStr == "none" { 465 | return JSON(ResponseData{5001, FailedMsg, nil}) 466 | } 467 | ttl, _ := redis.Int64(client.Do("TTL", key)) 468 | size := 1000 469 | switch typeStr { 470 | case "list": // 读取总长度 471 | llen, _ := redis.Int64(client.Do("LLEN", key)) 472 | val, err := redis.Strings(client.Do("LRANGE", key, 0, size)) 473 | ThrowIf(err) 474 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": val, "ttl": ttl, "count": llen, "size": 50, "current": 1}}) 475 | case "set": 476 | llen, _ := redis.Int64(client.Do("SCARD", key)) 477 | repl, err := client.Do("SSCAN", key, 0, "COUNT", size) 478 | ThrowIf(err) 479 | keys, err := redis.Strings(repl.([]interface{})[1], nil) 480 | ThrowIf(err) 481 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": keys, "ttl": ttl, "count": llen, "size": 50, "current": 1}}) 482 | case "stream": 483 | llen, _ := redis.Int64(client.Do("XLEN", key)) 484 | val, err := client.Do("XRANGE", key, "-", "+", "COUNT", size) 485 | ThrowIf(err) 486 | vds := val.([]interface{}) 487 | var retData []map[string][]string 488 | for _, v := range vds { 489 | item := map[string][]string{} 490 | v := v.([]interface{}) 491 | xid := string(v[0].([]byte)) 492 | item[xid] = []string{} 493 | fv := v[1].([]interface{}) 494 | for _, v := range fv { 495 | item[xid] = append(item[xid], string(v.([]byte))) 496 | } 497 | retData = append(retData, item) 498 | } 499 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": retData, "ttl": ttl, "count": llen, "size": 50, "current": 1}}) 500 | case "zset": 501 | llen, _ := redis.Int64(client.Do("ZCARD", key)) 502 | repl, err := client.Do("ZSCAN", key, 0, "COUNT", size) 503 | ThrowIf(err) 504 | values, err := redis.Strings(repl.([]interface{})[1], nil) 505 | ThrowIf(err) 506 | 507 | var retData []map[string]string 508 | for i, v := range values { 509 | if i%2 == 1 { 510 | retData = append(retData, map[string]string{"value": values[i-1], "score": v}) 511 | } 512 | } 513 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": retData, "ttl": ttl, "count": llen, "size": 50, "current": 1}}) 514 | case "string": 515 | val, err := redis.String(client.Do("GET", key)) 516 | ThrowIf(err) 517 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": val, "ttl": ttl}}) 518 | case "hash": 519 | llen, _ := redis.Int64(client.Do("HLEN", key)) 520 | repl, err := client.Do("HSCAN", key, 0, "COUNT", size) 521 | ThrowIf(err) 522 | keys, err := redis.StringMap(repl.([]interface{})[1], nil) 523 | ThrowIf(err) 524 | return JSON(ResponseData{SuccessCode, "读取所有key成功", RequestData{"type": typeStr, "data": keys, "ttl": ttl, "count": llen, "size": 50, "current": 1}}) 525 | } 526 | case "dblist": 527 | var dbs []int 528 | for i := 0; i < 20; i++ { 529 | if _, err := client.Do("SELECT", i); err != nil { 530 | break 531 | } 532 | total, _ := redis.Int(client.Do("DBSIZE")) 533 | dbs = append(dbs, total) 534 | } 535 | return JSON(ResponseData{SuccessCode, "连接数据库成功", dbs}) 536 | case "select_db": 537 | index := getFromInterfaceOrFloat64ToInt(data["index"]) 538 | _, _ = client.Do("SELECT", index) //选择数据库 539 | var nextCur = "0" 540 | var resKeys = map[string][]string{} 541 | 542 | filter := data["filter"].(string) 543 | if filter == "" { 544 | filter = "*" 545 | } 546 | 547 | repl, err := client.Do("SCAN", nextCur, "MATCH", filter, "COUNT", 3000) 548 | if err != nil { 549 | return JSON(ResponseData{FailedCode, err.Error(), nil}) 550 | } 551 | nextCur = string(repl.([]interface{})[0].([]byte)) 552 | keys, err := redis.Strings(repl.([]interface{})[1], nil) 553 | if err != nil { 554 | return JSON(ResponseData{FailedCode, "错误,无法解析SCAN返回值", nil}) 555 | } 556 | for _, v := range keys { 557 | resKeys[v] = append(resKeys[v], v) 558 | } 559 | 560 | return JSON(ResponseData{SuccessCode, "读取所有key成功:" + string(nextCur), resKeys}) 561 | } 562 | return JSON(ResponseData{FailedCode, "错误,无法解析到动作:" + action, nil}) 563 | } 564 | 565 | func RedisManagerRemoveKey(data RequestData) string { 566 | client, key := getRedisClient(data, true, true) 567 | defer client.Close() 568 | _, err := client.Do("UNLINK", key) // UNLINK (异步) 替代 DEL (同步) 569 | ThrowIf(err) 570 | return JSON(ResponseData{SuccessCode, "删除成功", nil}) 571 | } 572 | 573 | func RedisManagerFlushDB(data RequestData) string { 574 | client, _ := getRedisClient(data, true, false) 575 | defer client.Close() 576 | _, err := client.Do("FLUSHDB") 577 | ThrowIf(err) 578 | return JSON(ResponseData{SuccessCode, "Successfully cleared the database", nil}) 579 | } 580 | 581 | func RedisManagerRemoveRow(data RequestData) string { 582 | client, key := getRedisClient(data, true, true) 583 | defer client.Close() 584 | var err error 585 | valType := data["type"].(string) 586 | ThrowIf(valType == "", "Unable to parse data type") 587 | switch valType { 588 | case "list": 589 | _, err = client.Do("LREM", key, 1, data["data"]) 590 | case "set": 591 | _, err = client.Do("SREM", key, data["data"]) 592 | case "zset": 593 | _, err = client.Do("ZREM", key, data["data"].(string)) 594 | case "hash": 595 | _, err = client.Do("HDEL", key, data["data"]) 596 | case "stream": 597 | _, err = client.Do("XDEL", key, data["data"]) 598 | } 599 | ThrowIf(err) 600 | return JSON(ResponseData{SuccessCode, "Successfully deleted", nil}) 601 | } 602 | 603 | func RedisManagerUpdateKey(data RequestData) string { 604 | client, key := getRedisClient(data, true, true) 605 | defer client.Close() 606 | var err error 607 | action := data["action"].(string) 608 | var extraData = map[string]interface{}{} 609 | switch action { 610 | case "ttl": //更新ttl时间 611 | ttl := getFromInterfaceOrFloat64ToInt(data["ttl"]) 612 | if ttl < -1 { 613 | _, err = client.Do("PERSIST", key) 614 | } else { 615 | _, err = client.Do("EXPIRE", key, ttl) 616 | } 617 | ThrowIf(err) 618 | case "value": //更新value 619 | valType := data["type"].(string) 620 | ThrowIf(valType == "", "Unable to parse data type") 621 | switch valType { 622 | case "list": 623 | _, err = client.Do("LPUSH", key, data["data"]) 624 | case "set": 625 | _, err = client.Do("SADD", key, data["data"]) 626 | case "zset": 627 | _, err = client.Do("ZADD", key, data["rowkey"], data["data"]) 628 | case "string": 629 | _, err = client.Do("SET", key, data["data"]) 630 | case "hash": 631 | rowkey := data["rowkey"].(string) 632 | ThrowIf(rowkey == "", "parameter error") 633 | _, err = client.Do("HSET", key, rowkey, data["data"]) 634 | } 635 | case "addrow": // 添加新的列 636 | valType := data["type"].(string) 637 | ThrowIf(valType == "", "Unable to parse data type") 638 | switch valType { 639 | case "list": 640 | _, err = client.Do("RPUSH", key, data["data"]) 641 | case "set": 642 | var ok int 643 | ok, err = redis.Int(client.Do("SADD", key, data["data"])) 644 | ThrowIf(ok == 0, "Adding failed, data already exists") 645 | case "stream": 646 | newId := data["score"].(string) 647 | if len(newId) == 0 { 648 | newId = "*" 649 | } 650 | params := []interface{}{key, newId} 651 | fvs := strings.Split(data["data"].(string), "\n") 652 | if len(fvs)%2 == 1 { 653 | return JSON(ResponseData{FailedCode, "stream值必须成对设置", nil}) 654 | } 655 | for _, fv := range fvs { 656 | params = append(params, fv) 657 | } 658 | newId, err = redis.String(client.Do("XADD", params...)) 659 | extraData["id"] = newId 660 | case "zset": 661 | score := getFromInterfaceOrFloat64ToInt(data["rowkey"]) 662 | _, err = client.Do("ZADD", key, score, data["data"]) 663 | case "hash": 664 | rowkey := data["rowkey"].(string) 665 | if rowkey == "" { 666 | return JSON(ResponseData{FailedCode, "参数错误", nil}) 667 | } 668 | _, err = client.Do("HSET", key, rowkey, data["data"]) 669 | } 670 | case "updateRowValue": 671 | valType := data["type"].(string) 672 | ThrowIf(valType == "", "Unable to parse data type") 673 | switch strings.ToLower(valType) { 674 | case "list": 675 | _, ok := data["rowkey"] 676 | if !ok { 677 | return JSON(ResponseData{FailedCode, "请选择要编辑的数据", nil}) 678 | } 679 | rowkey := getFromInterfaceOrFloat64ToInt(data["rowkey"]) 680 | _, err = client.Do("LSET", key, rowkey, data["data"]) 681 | case "set": 682 | rowkey, ok := data["rowkey"].(string) 683 | if !ok { 684 | return JSON(ResponseData{FailedCode, "请选择要编辑的数据", nil}) 685 | } 686 | _, _ = client.Do("SREM", key, rowkey) 687 | 688 | _, err = client.Do("SADD", key, data["data"]) 689 | case "zset": 690 | rowkey := data["rowkey"].(string) 691 | score := getFromInterfaceOrFloat64ToInt(data["score"]) 692 | _, _ = client.Do("ZREM", key, rowkey) 693 | _, err = client.Do("ZADD", key, score, data["data"]) 694 | case "stream": 695 | return JSON(ResponseData{FailedCode, "不支持修改Steam内容", nil}) 696 | case "hash": 697 | hashKey := data["rowkey"].(string) 698 | _, err = client.Do("HSET", key, hashKey, data["data"]) 699 | } 700 | default: 701 | return JSON(ResponseData{FailedCode, "Unable to parse action", nil}) 702 | } 703 | ThrowIf(err) 704 | return JSON(ResponseData{SuccessCode, "Operation successful", extraData}) 705 | } 706 | 707 | func RedisManagerAddKey(data RequestData) string { 708 | client, key := getRedisClient(data, true, true) 709 | var err error 710 | defer client.Close() 711 | valType := data["type"].(string) 712 | ThrowIf(valType == "", "Unable to parse data type") 713 | switch valType { 714 | case "list": 715 | _, err = client.Do("LPUSH", key, data["data"].(string)) 716 | case "set": 717 | _, err = client.Do("SADD", key, data["data"].(string)) 718 | case "zset": 719 | score := getFromInterfaceOrFloat64ToInt(data["rowKey"]) 720 | _, err = client.Do("ZADD", key, score, data["data"].(string)) 721 | case "string": 722 | _, err = client.Do("SET", key, data["data"].(string)) 723 | case "stream": 724 | newId := data["rowKey"].(string) 725 | if len(newId) == 0 { 726 | newId = "*" 727 | } 728 | params := []interface{}{key, newId} 729 | fvs := strings.Split(data["data"].(string), "\n") 730 | if len(fvs)%2 == 1 { 731 | return JSON(ResponseData{FailedCode, "stream值必须成对设置", nil}) 732 | } 733 | for _, fv := range fvs { 734 | params = append(params, fv) 735 | } 736 | _, err = redis.String(client.Do("XADD", params...)) 737 | case "hash": 738 | rowkey := data["rowKey"].(string) 739 | ThrowIf(rowkey == "", "参数错误") 740 | _, err = client.Do("HSET", key, rowkey, data["data"].(string)) 741 | } 742 | ThrowIf(err) 743 | return JSON(ResponseData{SuccessCode, "操作成功", nil}) 744 | } 745 | 746 | func JSON(data ResponseData) string { 747 | b, _ := json.Marshal(data) 748 | return string(b) 749 | } 750 | 751 | func readConfigJSON() error { 752 | f, err := os.OpenFile(ConnectionFile, os.O_RDWR, os.ModePerm) 753 | if err != nil && os.IsNotExist(err) { 754 | return nil 755 | } 756 | defer f.Close() 757 | decoder := gob.NewDecoder(f) 758 | err = decoder.Decode(&connectionList) 759 | if err != nil { 760 | return err 761 | } 762 | if len(connectionList) > 0 { 763 | last := connectionList[len(connectionList)-1] 764 | totalConnection = int(last.ID) + 1 765 | } 766 | return nil 767 | } 768 | 769 | func RedisManagerGetCommandList(_ RequestData) string { 770 | return JSON(ResponseData{SuccessCode, "成功", map[string]interface{}{ 771 | "helpers": FromRedisSourceCommandHelper(), 772 | }}) 773 | } 774 | 775 | func writeConfigJSON() error { 776 | //os.O_TRUNC 清空内容 777 | f, err := os.OpenFile(ConnectionFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.ModePerm) 778 | if err != nil { 779 | return err 780 | } 781 | defer f.Close() 782 | encoder := gob.NewEncoder(f) 783 | err = encoder.Encode(&connectionList) 784 | if err != nil { 785 | return err 786 | } 787 | totalConnection = len(connectionList) 788 | return nil 789 | } 790 | -------------------------------------------------------------------------------- /server/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/xiusin/logger" 5 | "runtime/debug" 6 | "sync" 7 | ) 8 | 9 | type Handler struct { 10 | sync.Mutex 11 | routes map[string]HandleFunc 12 | } 13 | type HandleFunc func(data RequestData) string 14 | 15 | func (h *Handler) Handle(route string, data RequestData) string { 16 | h.Lock() 17 | defer h.Unlock() 18 | defer func() { 19 | if err := recover(); err != nil { 20 | s := debug.Stack() 21 | logger.Errorf("Recovered Error: %s, ErrorStack: \n%s\n\n", err, string(s)) 22 | } 23 | }() 24 | 25 | if handle, ok := h.routes[route]; !ok { 26 | return JSON(ResponseData{Status: FailedCode, Data: nil, Msg: "not found"}) 27 | } else { 28 | res := handle(data) 29 | return res 30 | } 31 | } 32 | 33 | func (h *Handler) Add(route string, handle HandleFunc) { 34 | h.routes[route] = handle 35 | } 36 | -------------------------------------------------------------------------------- /server/handler/helper.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | type ResponseData struct { 9 | Status int64 `json:"status"` 10 | Msg string `json:"msg"` 11 | Data interface{} `json:"data"` 12 | } 13 | 14 | type connection struct { 15 | ID int64 `json:"id"` 16 | Title string `json:"title"` 17 | Ip string `json:"ip"` 18 | Port string `json:"port"` 19 | Auth string `json:"auth"` 20 | Username string `json:"username"` 21 | Readonly bool `json:"readonly,omitempty"` 22 | } 23 | 24 | type RequestData map[string]interface{} 25 | 26 | var ( 27 | totalConnection = 0 28 | connectionList []connection 29 | ConnectionFile string 30 | SecretKey string 31 | ) 32 | 33 | const SuccessMsg = "success" 34 | const FailedMsg = "failed" 35 | const SuccessCode = 200 36 | const FailedCode = 5000 37 | 38 | type commandHelp struct { 39 | Name string `json:"name"` 40 | Params string `json:"params"` 41 | Summary string `json:"summary"` 42 | Group int `json:"group"` 43 | Since string `json:"since"` 44 | } 45 | 46 | func CheckReadonly(readonly bool, modifyKey string) { 47 | if readonly { 48 | for _, key := range []string{"removekey", "removerow", "updatekey", "addkey", "flushDB", "renameKey", "command"} { 49 | ThrowIf(modifyKey == key, "Cannot make modifications or add operations in read-only mode") 50 | } 51 | } 52 | } 53 | 54 | func ThrowIf(cond interface{}, msg ...string) { 55 | if cond == nil { 56 | return 57 | } 58 | switch val := cond.(type) { 59 | case bool: 60 | if val { 61 | if len(msg) == 0 { 62 | msg = append(msg, "unknown error") 63 | } 64 | panic(errors.New(msg[0])) 65 | } 66 | case error: 67 | panic(cond) 68 | default: 69 | panic(errors.New("ThrowIf only support types: error,bool: " + reflect.TypeOf(cond).String())) 70 | } 71 | } 72 | 73 | // @see https://raw.githubusercontent.com/antirez/redis/ad78b50f62c88b6396c5ee86cda89fc2313f77af/src/help.h 74 | // 辅助提醒命令参数 75 | func FromRedisSourceCommandHelper() map[string]commandHelp { 76 | var helpers = []commandHelp{ 77 | {"APPEND", 78 | "key value", 79 | "Append a value to a key", 80 | 1, 81 | "2.0.0"}, 82 | {"AUTH", 83 | "password", 84 | "Authenticate to the server", 85 | 8, 86 | "1.0.0"}, 87 | {"BGREWRITEAOF", 88 | "-", 89 | "Asynchronously rewrite the append-only file", 90 | 9, 91 | "1.0.0"}, 92 | {"BGSAVE", 93 | "-", 94 | "Asynchronously save the dataset to disk", 95 | 9, 96 | "1.0.0"}, 97 | {"BITCOUNT", 98 | "key [start end]", 99 | "Count set bits in a string", 100 | 1, 101 | "2.6.0"}, 102 | {"BITFIELD", 103 | "key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]", 104 | "Perform arbitrary bitfield integer operations on strings", 105 | 1, 106 | "3.2.0"}, 107 | {"BITOP", 108 | "operation destkey key [key ...]", 109 | "Perform bitwise operations between strings", 110 | 1, 111 | "2.6.0"}, 112 | {"BITPOS", 113 | "key bit [start] [end]", 114 | "Find first bit set or clear in a string", 115 | 1, 116 | "2.8.7"}, 117 | {"BLPOP", 118 | "key [key ...] timeout", 119 | "Remove and get the first element in a list, or block until one is available", 120 | 2, 121 | "2.0.0"}, 122 | {"BRPOP", 123 | "key [key ...] timeout", 124 | "Remove and get the last element in a list, or block until one is available", 125 | 2, 126 | "2.0.0"}, 127 | {"BRPOPLPUSH", 128 | "source destination timeout", 129 | "Pop a value from a list, push it to another list and return it; or block until one is available", 130 | 2, 131 | "2.2.0"}, 132 | {"BZPOPMAX", 133 | "key [key ...] timeout", 134 | "Remove and return the member with the highest score from one or more sorted sets, or block until one is available", 135 | 4, 136 | "5.0.0"}, 137 | {"BZPOPMIN", 138 | "key [key ...] timeout", 139 | "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available", 140 | 4, 141 | "5.0.0"}, 142 | {"CLIENT GETNAME", 143 | "-", 144 | "Get the current connection name", 145 | 9, 146 | "2.6.9"}, 147 | {"CLIENT ID", 148 | "-", 149 | "Returns the client ID for the current connection", 150 | 9, 151 | "5.0.0"}, 152 | {"CLIENT KILL", 153 | "[ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [ADDR ip:port] [SKIPME yes/no]", 154 | "Kill the connection of a client", 155 | 9, 156 | "2.4.0"}, 157 | {"CLIENT LIST", 158 | "-", 159 | "Get the list of client connections", 160 | 9, 161 | "2.4.0"}, 162 | {"CLIENT PAUSE", 163 | "timeout", 164 | "Stop processing commands from clients for some time", 165 | 9, 166 | "2.9.50"}, 167 | {"CLIENT REPLY", 168 | "ON|OFF|SKIP", 169 | "Instruct the server whether to reply to commands", 170 | 9, 171 | "3.2"}, 172 | {"CLIENT SETNAME", 173 | "connection-name", 174 | "Set the current connection name", 175 | 9, 176 | "2.6.9"}, 177 | {"CLIENT UNBLOCK", 178 | "client-id [TIMEOUT|ERROR]", 179 | "Unblock a client blocked in a blocking command from a different connection", 180 | 9, 181 | "5.0.0"}, 182 | {"CLUSTER ADDSLOTS", 183 | "slot [slot ...]", 184 | "Assign new hash slots to receiving node", 185 | 12, 186 | "3.0.0"}, 187 | {"CLUSTER COUNT-FAILURE-REPORTS", 188 | "node-id", 189 | "Return the number of failure reports active for a given node", 190 | 12, 191 | "3.0.0"}, 192 | {"CLUSTER COUNTKEYSINSLOT", 193 | "slot", 194 | "Return the number of local keys in the specified hash slot", 195 | 12, 196 | "3.0.0"}, 197 | {"CLUSTER DELSLOTS", 198 | "slot [slot ...]", 199 | "Set hash slots as unbound in receiving node", 200 | 12, 201 | "3.0.0"}, 202 | {"CLUSTER FAILOVER", 203 | "[FORCE|TAKEOVER]", 204 | "Forces a replica to perform a manual failover of its master.", 205 | 12, 206 | "3.0.0"}, 207 | {"CLUSTER FORGET", 208 | "node-id", 209 | "Remove a node from the nodes table", 210 | 12, 211 | "3.0.0"}, 212 | {"CLUSTER GETKEYSINSLOT", 213 | "slot count", 214 | "Return local key names in the specified hash slot", 215 | 12, 216 | "3.0.0"}, 217 | {"CLUSTER INFO", 218 | "-", 219 | "Provides info about Redis Cluster node state", 220 | 12, 221 | "3.0.0"}, 222 | {"CLUSTER KEYSLOT", 223 | "key", 224 | "Returns the hash slot of the specified key", 225 | 12, 226 | "3.0.0"}, 227 | {"CLUSTER MEET", 228 | "ip port", 229 | "Force a node cluster to handshake with another node", 230 | 12, 231 | "3.0.0"}, 232 | {"CLUSTER NODES", 233 | "-", 234 | "Get Cluster config for the node", 235 | 12, 236 | "3.0.0"}, 237 | {"CLUSTER REPLICAS", 238 | "node-id", 239 | "List replica nodes of the specified master node", 240 | 12, 241 | "5.0.0"}, 242 | {"CLUSTER REPLICATE", 243 | "node-id", 244 | "Reconfigure a node as a replica of the specified master node", 245 | 12, 246 | "3.0.0"}, 247 | {"CLUSTER RESET", 248 | "[HARD|SOFT]", 249 | "Reset a Redis Cluster node", 250 | 12, 251 | "3.0.0"}, 252 | {"CLUSTER SAVECONFIG", 253 | "-", 254 | "Forces the node to save cluster state on disk", 255 | 12, 256 | "3.0.0"}, 257 | {"CLUSTER SET-CONFIG-EPOCH", 258 | "config-epoch", 259 | "Set the configuration epoch in a new node", 260 | 12, 261 | "3.0.0"}, 262 | {"CLUSTER SETSLOT", 263 | "slot IMPORTING|MIGRATING|STABLE|NODE [node-id]", 264 | "Bind a hash slot to a specific node", 265 | 12, 266 | "3.0.0"}, 267 | {"CLUSTER SLAVES", 268 | "node-id", 269 | "List replica nodes of the specified master node", 270 | 12, 271 | "3.0.0"}, 272 | {"CLUSTER SLOTS", 273 | "-", 274 | "Get array of Cluster slot to node mappings", 275 | 12, 276 | "3.0.0"}, 277 | {"COMMAND", 278 | "-", 279 | "Get array of Redis command details", 280 | 9, 281 | "2.8.13"}, 282 | {"COMMAND COUNT", 283 | "-", 284 | "Get total number of Redis commands", 285 | 9, 286 | "2.8.13"}, 287 | {"COMMAND GETKEYS", 288 | "-", 289 | "Extract keys given a full Redis command", 290 | 9, 291 | "2.8.13"}, 292 | {"COMMAND INFO", 293 | "command-name [command-name ...]", 294 | "Get array of specific Redis command details", 295 | 9, 296 | "2.8.13"}, 297 | {"CONFIG GET", 298 | "parameter", 299 | "Get the value of a configuration parameter", 300 | 9, 301 | "2.0.0"}, 302 | {"CONFIG RESETSTAT", 303 | "-", 304 | "Reset the stats returned by INFO", 305 | 9, 306 | "2.0.0"}, 307 | {"CONFIG REWRITE", 308 | "-", 309 | "Rewrite the configuration file with the in memory configuration", 310 | 9, 311 | "2.8.0"}, 312 | {"CONFIG SET", 313 | "parameter value", 314 | "Set a configuration parameter to the given value", 315 | 9, 316 | "2.0.0"}, 317 | {"DBSIZE", 318 | "-", 319 | "Return the number of keys in the selected database", 320 | 9, 321 | "1.0.0"}, 322 | {"DEBUG OBJECT", 323 | "key", 324 | "Get debugging information about a key", 325 | 9, 326 | "1.0.0"}, 327 | {"DEBUG SEGFAULT", 328 | "-", 329 | "Make the server crash", 330 | 9, 331 | "1.0.0"}, 332 | {"DECR", 333 | "key", 334 | "Decrement the integer value of a key by one", 335 | 1, 336 | "1.0.0"}, 337 | {"DECRBY", 338 | "key decrement", 339 | "Decrement the integer value of a key by the given number", 340 | 1, 341 | "1.0.0"}, 342 | {"DEL", 343 | "key [key ...]", 344 | "Delete a key", 345 | 0, 346 | "1.0.0"}, 347 | {"DISCARD", 348 | "-", 349 | "Discard all commands issued after MULTI", 350 | 7, 351 | "2.0.0"}, 352 | {"DUMP", 353 | "key", 354 | "Return a serialized version of the value stored at the specified key.", 355 | 0, 356 | "2.6.0"}, 357 | {"ECHO", 358 | "message", 359 | "Echo the given string", 360 | 8, 361 | "1.0.0"}, 362 | {"EVAL", 363 | "script numkeys key [key ...] arg [arg ...]", 364 | "Execute a Lua script server side", 365 | 10, 366 | "2.6.0"}, 367 | {"EVALSHA", 368 | "sha1 numkeys key [key ...] arg [arg ...]", 369 | "Execute a Lua script server side", 370 | 10, 371 | "2.6.0"}, 372 | {"EXEC", 373 | "-", 374 | "Execute all commands issued after MULTI", 375 | 7, 376 | "1.2.0"}, 377 | {"EXISTS", 378 | "key [key ...]", 379 | "Determine if a key exists", 380 | 0, 381 | "1.0.0"}, 382 | {"EXPIRE", 383 | "key seconds", 384 | "Set a key's time to live in seconds", 385 | 0, 386 | "1.0.0"}, 387 | {"EXPIREAT", 388 | "key timestamp", 389 | "Set the expiration for a key as a UNIX timestamp", 390 | 0, 391 | "1.2.0"}, 392 | {"FLUSHALL", 393 | "[ASYNC]", 394 | "Remove all keys from all databases", 395 | 9, 396 | "1.0.0"}, 397 | {"FLUSHDB", 398 | "[ASYNC]", 399 | "Remove all keys from the current database", 400 | 9, 401 | "1.0.0"}, 402 | {"GEOADD", 403 | "key longitude latitude member [longitude latitude member ...]", 404 | "Add one or more geospatial items in the geospatial index represented using a sorted set", 405 | 13, 406 | "3.2.0"}, 407 | {"GEODIST", 408 | "key member1 member2 [unit]", 409 | "Returns the distance between two members of a geospatial index", 410 | 13, 411 | "3.2.0"}, 412 | {"GEOHASH", 413 | "key member [member ...]", 414 | "Returns members of a geospatial index as standard geohash strings", 415 | 13, 416 | "3.2.0"}, 417 | {"GEOPOS", 418 | "key member [member ...]", 419 | "Returns longitude and latitude of members of a geospatial index", 420 | 13, 421 | "3.2.0"}, 422 | {"GEORADIUS", 423 | "key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]", 424 | "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", 425 | 13, 426 | "3.2.0"}, 427 | {"GEORADIUSBYMEMBER", 428 | "key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]", 429 | "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", 430 | 13, 431 | "3.2.0"}, 432 | {"GET", 433 | "key", 434 | "Get the value of a key", 435 | 1, 436 | "1.0.0"}, 437 | {"GETBIT", 438 | "key offset", 439 | "Returns the bit value at offset in the string value stored at key", 440 | 1, 441 | "2.2.0"}, 442 | {"GETRANGE", 443 | "key start end", 444 | "Get a substring of the string stored at a key", 445 | 1, 446 | "2.4.0"}, 447 | {"GETSET", 448 | "key value", 449 | "Set the string value of a key and return its old value", 450 | 1, 451 | "1.0.0"}, 452 | {"HDEL", 453 | "key field [field ...]", 454 | "Delete one or more hash fields", 455 | 5, 456 | "2.0.0"}, 457 | {"HEXISTS", 458 | "key field", 459 | "Determine if a hash field exists", 460 | 5, 461 | "2.0.0"}, 462 | {"HGET", 463 | "key field", 464 | "Get the value of a hash field", 465 | 5, 466 | "2.0.0"}, 467 | {"HGETALL", 468 | "key", 469 | "Get all the fields and values in a hash", 470 | 5, 471 | "2.0.0"}, 472 | {"HINCRBY", 473 | "key field increment", 474 | "Increment the integer value of a hash field by the given number", 475 | 5, 476 | "2.0.0"}, 477 | {"HINCRBYFLOAT", 478 | "key field increment", 479 | "Increment the float value of a hash field by the given amount", 480 | 5, 481 | "2.6.0"}, 482 | {"HKEYS", 483 | "key", 484 | "Get all the fields in a hash", 485 | 5, 486 | "2.0.0"}, 487 | {"HLEN", 488 | "key", 489 | "Get the number of fields in a hash", 490 | 5, 491 | "2.0.0"}, 492 | {"HMGET", 493 | "key field [field ...]", 494 | "Get the values of all the given hash fields", 495 | 5, 496 | "2.0.0"}, 497 | {"HMSET", 498 | "key field value [field value ...]", 499 | "Set multiple hash fields to multiple values", 500 | 5, 501 | "2.0.0"}, 502 | {"HSCAN", 503 | "key cursor [MATCH pattern] [COUNT count]", 504 | "Incrementally iterate hash fields and associated values", 505 | 5, 506 | "2.8.0"}, 507 | {"HSET", 508 | "key field value", 509 | "Set the string value of a hash field", 510 | 5, 511 | "2.0.0"}, 512 | {"HSETNX", 513 | "key field value", 514 | "Set the value of a hash field, only if the field does not exist", 515 | 5, 516 | "2.0.0"}, 517 | {"HSTRLEN", 518 | "key field", 519 | "Get the length of the value of a hash field", 520 | 5, 521 | "3.2.0"}, 522 | {"HVALS", 523 | "key", 524 | "Get all the values in a hash", 525 | 5, 526 | "2.0.0"}, 527 | {"INCR", 528 | "key", 529 | "Increment the integer value of a key by one", 530 | 1, 531 | "1.0.0"}, 532 | {"INCRBY", 533 | "key increment", 534 | "Increment the integer value of a key by the given amount", 535 | 1, 536 | "1.0.0"}, 537 | {"INCRBYFLOAT", 538 | "key increment", 539 | "Increment the float value of a key by the given amount", 540 | 1, 541 | "2.6.0"}, 542 | {"INFO", 543 | "[section]", 544 | "Get information and statistics about the server", 545 | 9, 546 | "1.0.0"}, 547 | {"KEYS", 548 | "pattern", 549 | "Find all keys matching the given pattern", 550 | 0, 551 | "1.0.0"}, 552 | {"LASTSAVE", 553 | "-", 554 | "Get the UNIX time stamp of the last successful save to disk", 555 | 9, 556 | "1.0.0"}, 557 | {"LINDEX", 558 | "key index", 559 | "Get an element from a list by its index", 560 | 2, 561 | "1.0.0"}, 562 | {"LINSERT", 563 | "key BEFORE|AFTER pivot value", 564 | "Insert an element before or after another element in a list", 565 | 2, 566 | "2.2.0"}, 567 | {"LLEN", 568 | "key", 569 | "Get the length of a list", 570 | 2, 571 | "1.0.0"}, 572 | {"LPOP", 573 | "key", 574 | "Remove and get the first element in a list", 575 | 2, 576 | "1.0.0"}, 577 | {"LPUSH", 578 | "key value [value ...]", 579 | "Prepend one or multiple values to a list", 580 | 2, 581 | "1.0.0"}, 582 | {"LPUSHX", 583 | "key value", 584 | "Prepend a value to a list, only if the list exists", 585 | 2, 586 | "2.2.0"}, 587 | {"LRANGE", 588 | "key start stop", 589 | "Get a range of elements from a list", 590 | 2, 591 | "1.0.0"}, 592 | {"LREM", 593 | "key count value", 594 | "Remove elements from a list", 595 | 2, 596 | "1.0.0"}, 597 | {"LSET", 598 | "key index value", 599 | "Set the value of an element in a list by its index", 600 | 2, 601 | "1.0.0"}, 602 | {"LTRIM", 603 | "key start stop", 604 | "Trim a list to the specified range", 605 | 2, 606 | "1.0.0"}, 607 | {"MEMORY DOCTOR", 608 | "-", 609 | "Outputs memory problems report", 610 | 9, 611 | "4.0.0"}, 612 | {"MEMORY HELP", 613 | "-", 614 | "Show helpful text about the different subcommands", 615 | 9, 616 | "4.0.0"}, 617 | {"MEMORY MALLOC-STATS", 618 | "-", 619 | "Show allocator internal stats", 620 | 9, 621 | "4.0.0"}, 622 | {"MEMORY PURGE", 623 | "-", 624 | "Ask the allocator to release memory", 625 | 9, 626 | "4.0.0"}, 627 | {"MEMORY STATS", 628 | "-", 629 | "Show memory usage details", 630 | 9, 631 | "4.0.0"}, 632 | {"MEMORY USAGE", 633 | "key [SAMPLES count]", 634 | "Estimate the memory usage of a key", 635 | 9, 636 | "4.0.0"}, 637 | {"MGET", 638 | "key [key ...]", 639 | "Get the values of all the given keys", 640 | 1, 641 | "1.0.0"}, 642 | {"MIGRATE", 643 | "host port key|\"\" destination-db timeout [COPY] [REPLACE] [KEYS key]", 644 | "Atomically transfer a key from a Redis instance to another one.", 645 | 0, 646 | "2.6.0"}, 647 | {"MONITOR", 648 | "-", 649 | "Listen for all requests received by the server in real time", 650 | 9, 651 | "1.0.0"}, 652 | {"MOVE", 653 | "key db", 654 | "Move a key to another database", 655 | 0, 656 | "1.0.0"}, 657 | {"MSET", 658 | "key value [key value ...]", 659 | "Set multiple keys to multiple values", 660 | 1, 661 | "1.0.1"}, 662 | {"MSETNX", 663 | "key value [key value ...]", 664 | "Set multiple keys to multiple values, only if none of the keys exist", 665 | 1, 666 | "1.0.1"}, 667 | {"MULTI", 668 | "-", 669 | "Mark the start of a transaction block", 670 | 7, 671 | "1.2.0"}, 672 | {"OBJECT", 673 | "subcommand [arguments [arguments ...]]", 674 | "Inspect the internals of Redis objects", 675 | 0, 676 | "2.2.3"}, 677 | {"PERSIST", 678 | "key", 679 | "Remove the expiration from a key", 680 | 0, 681 | "2.2.0"}, 682 | {"PEXPIRE", 683 | "key milliseconds", 684 | "Set a key's time to live in milliseconds", 685 | 0, 686 | "2.6.0"}, 687 | {"PEXPIREAT", 688 | "key milliseconds-timestamp", 689 | "Set the expiration for a key as a UNIX timestamp specified in milliseconds", 690 | 0, 691 | "2.6.0"}, 692 | {"PFADD", 693 | "key element [element ...]", 694 | "Adds the specified elements to the specified HyperLogLog.", 695 | 11, 696 | "2.8.9"}, 697 | {"PFCOUNT", 698 | "key [key ...]", 699 | "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).", 700 | 11, 701 | "2.8.9"}, 702 | {"PFMERGE", 703 | "destkey sourcekey [sourcekey ...]", 704 | "Merge N different HyperLogLogs into a single one.", 705 | 11, 706 | "2.8.9"}, 707 | {"PING", 708 | "[message]", 709 | "Ping the server", 710 | 8, 711 | "1.0.0"}, 712 | {"PSETEX", 713 | "key milliseconds value", 714 | "Set the value and expiration in milliseconds of a key", 715 | 1, 716 | "2.6.0"}, 717 | {"PSUBSCRIBE", 718 | "pattern [pattern ...]", 719 | "Listen for messages published to channels matching the given patterns", 720 | 6, 721 | "2.0.0"}, 722 | {"PTTL", 723 | "key", 724 | "Get the time to live for a key in milliseconds", 725 | 0, 726 | "2.6.0"}, 727 | {"PUBLISH", 728 | "channel message", 729 | "Post a message to a channel", 730 | 6, 731 | "2.0.0"}, 732 | {"PUBSUB", 733 | "subcommand [argument [argument ...]]", 734 | "Inspect the state of the Pub/Sub subsystem", 735 | 6, 736 | "2.8.0"}, 737 | {"PUNSUBSCRIBE", 738 | "[pattern [pattern ...]]", 739 | "Stop listening for messages posted to channels matching the given patterns", 740 | 6, 741 | "2.0.0"}, 742 | {"QUIT", 743 | "-", 744 | "Close the connection", 745 | 8, 746 | "1.0.0"}, 747 | {"RANDOMKEY", 748 | "-", 749 | "Return a random key from the keyspace", 750 | 0, 751 | "1.0.0"}, 752 | {"READONLY", 753 | "-", 754 | "Enables read queries for a connection to a cluster replica node", 755 | 12, 756 | "3.0.0"}, 757 | {"READWRITE", 758 | "-", 759 | "Disables read queries for a connection to a cluster replica node", 760 | 12, 761 | "3.0.0"}, 762 | {"RENAME", 763 | "key newkey", 764 | "Rename a key", 765 | 0, 766 | "1.0.0"}, 767 | {"RENAMENX", 768 | "key newkey", 769 | "Rename a key, only if the new key does not exist", 770 | 0, 771 | "1.0.0"}, 772 | {"REPLICAOF", 773 | "host port", 774 | "Make the server a replica of another instance, or promote it as master.", 775 | 9, 776 | "5.0.0"}, 777 | {"RESTORE", 778 | "key ttl serialized-value [REPLACE]", 779 | "Create a key using the provided serialized value, previously obtained using DUMP.", 780 | 0, 781 | "2.6.0"}, 782 | {"ROLE", 783 | "-", 784 | "Return the role of the instance in the context of replication", 785 | 9, 786 | "2.8.12"}, 787 | {"RPOP", 788 | "key", 789 | "Remove and get the last element in a list", 790 | 2, 791 | "1.0.0"}, 792 | {"RPOPLPUSH", 793 | "source destination", 794 | "Remove the last element in a list, prepend it to another list and return it", 795 | 2, 796 | "1.2.0"}, 797 | {"RPUSH", 798 | "key value [value ...]", 799 | "Append one or multiple values to a list", 800 | 2, 801 | "1.0.0"}, 802 | {"RPUSHX", 803 | "key value", 804 | "Append a value to a list, only if the list exists", 805 | 2, 806 | "2.2.0"}, 807 | {"SADD", 808 | "key member [member ...]", 809 | "Add one or more members to a set", 810 | 3, 811 | "1.0.0"}, 812 | {"SAVE", 813 | "-", 814 | "Synchronously save the dataset to disk", 815 | 9, 816 | "1.0.0"}, 817 | {"SCAN", 818 | "cursor [MATCH pattern] [COUNT count]", 819 | "Incrementally iterate the keys space", 820 | 0, 821 | "2.8.0"}, 822 | {"SCARD", 823 | "key", 824 | "Get the number of members in a set", 825 | 3, 826 | "1.0.0"}, 827 | {"SCRIPT DEBUG", 828 | "YES|SYNC|NO", 829 | "Set the debug mode for executed scripts.", 830 | 10, 831 | "3.2.0"}, 832 | {"SCRIPT EXISTS", 833 | "sha1 [sha1 ...]", 834 | "Check existence of scripts in the script cache.", 835 | 10, 836 | "2.6.0"}, 837 | {"SCRIPT FLUSH", 838 | "-", 839 | "Remove all the scripts from the script cache.", 840 | 10, 841 | "2.6.0"}, 842 | {"SCRIPT KILL", 843 | "-", 844 | "Kill the script currently in execution.", 845 | 10, 846 | "2.6.0"}, 847 | {"SCRIPT LOAD", 848 | "script", 849 | "Load the specified Lua script into the script cache.", 850 | 10, 851 | "2.6.0"}, 852 | {"SDIFF", 853 | "key [key ...]", 854 | "Subtract multiple sets", 855 | 3, 856 | "1.0.0"}, 857 | {"SDIFFSTORE", 858 | "destination key [key ...]", 859 | "Subtract multiple sets and store the resulting set in a key", 860 | 3, 861 | "1.0.0"}, 862 | {"SELECT", 863 | "index", 864 | "Change the selected database for the current connection", 865 | 8, 866 | "1.0.0"}, 867 | {"SET", 868 | "key value [expiration EX seconds|PX milliseconds] [NX|XX]", 869 | "Set the string value of a key", 870 | 1, 871 | "1.0.0"}, 872 | {"SETBIT", 873 | "key offset value", 874 | "Sets or clears the bit at offset in the string value stored at key", 875 | 1, 876 | "2.2.0"}, 877 | {"SETEX", 878 | "key seconds value", 879 | "Set the value and expiration of a key", 880 | 1, 881 | "2.0.0"}, 882 | {"SETNX", 883 | "key value", 884 | "Set the value of a key, only if the key does not exist", 885 | 1, 886 | "1.0.0"}, 887 | {"SETRANGE", 888 | "key offset value", 889 | "Overwrite part of a string at key starting at the specified offset", 890 | 1, 891 | "2.2.0"}, 892 | {"SHUTDOWN", 893 | "[NOSAVE|SAVE]", 894 | "Synchronously save the dataset to disk and then shut down the server", 895 | 9, 896 | "1.0.0"}, 897 | {"SINTER", 898 | "key [key ...]", 899 | "Intersect multiple sets", 900 | 3, 901 | "1.0.0"}, 902 | {"SINTERSTORE", 903 | "destination key [key ...]", 904 | "Intersect multiple sets and store the resulting set in a key", 905 | 3, 906 | "1.0.0"}, 907 | {"SISMEMBER", 908 | "key member", 909 | "Determine if a given value is a member of a set", 910 | 3, 911 | "1.0.0"}, 912 | {"SLAVEOF", 913 | "host port", 914 | "Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.", 915 | 9, 916 | "1.0.0"}, 917 | {"SLOWLOG", 918 | "subcommand [argument]", 919 | "Manages the Redis slow queries log", 920 | 9, 921 | "2.2.12"}, 922 | {"SMEMBERS", 923 | "key", 924 | "Get all the members in a set", 925 | 3, 926 | "1.0.0"}, 927 | {"SMOVE", 928 | "source destination member", 929 | "Move a member from one set to another", 930 | 3, 931 | "1.0.0"}, 932 | {"SORT", 933 | "key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]", 934 | "Sort the elements in a list, set or sorted set", 935 | 0, 936 | "1.0.0"}, 937 | {"SPOP", 938 | "key [count]", 939 | "Remove and return one or multiple random members from a set", 940 | 3, 941 | "1.0.0"}, 942 | {"SRANDMEMBER", 943 | "key [count]", 944 | "Get one or multiple random members from a set", 945 | 3, 946 | "1.0.0"}, 947 | {"SREM", 948 | "key member [member ...]", 949 | "Remove one or more members from a set", 950 | 3, 951 | "1.0.0"}, 952 | {"SSCAN", 953 | "key cursor [MATCH pattern] [COUNT count]", 954 | "Incrementally iterate Set elements", 955 | 3, 956 | "2.8.0"}, 957 | {"STRLEN", 958 | "key", 959 | "Get the length of the value stored in a key", 960 | 1, 961 | "2.2.0"}, 962 | {"SUBSCRIBE", 963 | "channel [channel ...]", 964 | "Listen for messages published to the given channels", 965 | 6, 966 | "2.0.0"}, 967 | {"SUNION", 968 | "key [key ...]", 969 | "Add multiple sets", 970 | 3, 971 | "1.0.0"}, 972 | {"SUNIONSTORE", 973 | "destination key [key ...]", 974 | "Add multiple sets and store the resulting set in a key", 975 | 3, 976 | "1.0.0"}, 977 | {"SWAPDB", 978 | "index index", 979 | "Swaps two Redis databases", 980 | 8, 981 | "4.0.0"}, 982 | {"SYNC", 983 | "-", 984 | "Internal command used for replication", 985 | 9, 986 | "1.0.0"}, 987 | {"TIME", 988 | "-", 989 | "Return the current server time", 990 | 9, 991 | "2.6.0"}, 992 | {"TOUCH", 993 | "key [key ...]", 994 | "Alters the last access time of a key(s). Returns the number of existing keys specified.", 995 | 0, 996 | "3.2.1"}, 997 | {"TTL", 998 | "key", 999 | "Get the time to live for a key", 1000 | 0, 1001 | "1.0.0"}, 1002 | {"TYPE", 1003 | "key", 1004 | "Determine the type stored at key", 1005 | 0, 1006 | "1.0.0"}, 1007 | {"UNLINK", 1008 | "key [key ...]", 1009 | "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.", 1010 | 0, 1011 | "4.0.0"}, 1012 | {"UNSUBSCRIBE", 1013 | "[channel [channel ...]]", 1014 | "Stop listening for messages posted to the given channels", 1015 | 6, 1016 | "2.0.0"}, 1017 | {"UNWATCH", 1018 | "-", 1019 | "Forget about all watched keys", 1020 | 7, 1021 | "2.2.0"}, 1022 | {"WAIT", 1023 | "numreplicas timeout", 1024 | "Wait for the synchronous replication of all the write commands sent in the context of the current connection", 1025 | 0, 1026 | "3.0.0"}, 1027 | {"WATCH", 1028 | "key [key ...]", 1029 | "Watch the given keys to determine execution of the MULTI/EXEC block", 1030 | 7, 1031 | "2.2.0"}, 1032 | {"XACK", 1033 | "key group ID [ID ...]", 1034 | "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.", 1035 | 14, 1036 | "5.0.0"}, 1037 | {"XADD", 1038 | "key ID field string [field string ...]", 1039 | "Appends a new entry to a stream", 1040 | 14, 1041 | "5.0.0"}, 1042 | {"XCLAIM", 1043 | "key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [force] [justid]", 1044 | "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.", 1045 | 14, 1046 | "5.0.0"}, 1047 | {"XDEL", 1048 | "key ID [ID ...]", 1049 | "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.", 1050 | 14, 1051 | "5.0.0"}, 1052 | {"XGROUP", 1053 | "[CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]", 1054 | "Create, destroy, and manage consumer groups.", 1055 | 14, 1056 | "5.0.0"}, 1057 | {"XINFO", 1058 | "[CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]", 1059 | "Get information on streams and consumer groups", 1060 | 14, 1061 | "5.0.0"}, 1062 | {"XLEN", 1063 | "key", 1064 | "Return the number of entires in a stream", 1065 | 14, 1066 | "5.0.0"}, 1067 | {"XPENDING", 1068 | "key group [start end count] [consumer]", 1069 | "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.", 1070 | 14, 1071 | "5.0.0"}, 1072 | {"XRANGE", 1073 | "key start end [COUNT count]", 1074 | "Return a range of elements in a stream, with IDs matching the specified IDs interval", 1075 | 14, 1076 | "5.0.0"}, 1077 | {"XREAD", 1078 | "[COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]", 1079 | "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.", 1080 | 14, 1081 | "5.0.0"}, 1082 | {"XREADGROUP", 1083 | "GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]", 1084 | "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.", 1085 | 14, 1086 | "5.0.0"}, 1087 | {"XREVRANGE", 1088 | "key end start [COUNT count]", 1089 | "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE", 1090 | 14, 1091 | "5.0.0"}, 1092 | {"XTRIM", 1093 | "key MAXLEN [~] count", 1094 | "Trims the stream to (approximately if '~' is passed) a certain size", 1095 | 14, 1096 | "5.0.0"}, 1097 | {"ZADD", 1098 | "key [NX|XX] [CH] [INCR] score member [score member ...]", 1099 | "Add one or more members to a sorted set, or update its score if it already exists", 1100 | 4, 1101 | "1.2.0"}, 1102 | {"ZCARD", 1103 | "key", 1104 | "Get the number of members in a sorted set", 1105 | 4, 1106 | "1.2.0"}, 1107 | {"ZCOUNT", 1108 | "key min max", 1109 | "Count the members in a sorted set with scores within the given values", 1110 | 4, 1111 | "2.0.0"}, 1112 | {"ZINCRBY", 1113 | "key increment member", 1114 | "Increment the score of a member in a sorted set", 1115 | 4, 1116 | "1.2.0"}, 1117 | {"ZINTERSTORE", 1118 | "destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]", 1119 | "Intersect multiple sorted sets and store the resulting sorted set in a new key", 1120 | 4, 1121 | "2.0.0"}, 1122 | {"ZLEXCOUNT", 1123 | "key min max", 1124 | "Count the number of members in a sorted set between a given lexicographical range", 1125 | 4, 1126 | "2.8.9"}, 1127 | {"ZPOPMAX", 1128 | "key [count]", 1129 | "Remove and return members with the highest scores in a sorted set", 1130 | 4, 1131 | "5.0.0"}, 1132 | {"ZPOPMIN", 1133 | "key [count]", 1134 | "Remove and return members with the lowest scores in a sorted set", 1135 | 4, 1136 | "5.0.0"}, 1137 | {"ZRANGE", 1138 | "key start stop [WITHSCORES]", 1139 | "Return a range of members in a sorted set, by index", 1140 | 4, 1141 | "1.2.0"}, 1142 | {"ZRANGEBYLEX", 1143 | "key min max [LIMIT offset count]", 1144 | "Return a range of members in a sorted set, by lexicographical range", 1145 | 4, 1146 | "2.8.9"}, 1147 | {"ZRANGEBYSCORE", 1148 | "key min max [WITHSCORES] [LIMIT offset count]", 1149 | "Return a range of members in a sorted set, by score", 1150 | 4, 1151 | "1.0.5"}, 1152 | {"ZRANK", 1153 | "key member", 1154 | "Determine the index of a member in a sorted set", 1155 | 4, 1156 | "2.0.0"}, 1157 | {"ZREM", 1158 | "key member [member ...]", 1159 | "Remove one or more members from a sorted set", 1160 | 4, 1161 | "1.2.0"}, 1162 | {"ZREMRANGEBYLEX", 1163 | "key min max", 1164 | "Remove all members in a sorted set between the given lexicographical range", 1165 | 4, 1166 | "2.8.9"}, 1167 | {"ZREMRANGEBYRANK", 1168 | "key start stop", 1169 | "Remove all members in a sorted set within the given indexes", 1170 | 4, 1171 | "2.0.0"}, 1172 | {"ZREMRANGEBYSCORE", 1173 | "key min max", 1174 | "Remove all members in a sorted set within the given scores", 1175 | 4, 1176 | "1.2.0"}, 1177 | {"ZREVRANGE", 1178 | "key start stop [WITHSCORES]", 1179 | "Return a range of members in a sorted set, by index, with scores ordered from high to low", 1180 | 4, 1181 | "1.2.0"}, 1182 | {"ZREVRANGEBYLEX", 1183 | "key max min [LIMIT offset count]", 1184 | "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.", 1185 | 4, 1186 | "2.8.9"}, 1187 | {"ZREVRANGEBYSCORE", 1188 | "key max min [WITHSCORES] [LIMIT offset count]", 1189 | "Return a range of members in a sorted set, by score, with scores ordered from high to low", 1190 | 4, 1191 | "2.2.0"}, 1192 | {"ZREVRANK", 1193 | "key member", 1194 | "Determine the index of a member in a sorted set, with scores ordered from high to low", 1195 | 4, 1196 | "2.0.0"}, 1197 | {"ZSCAN", 1198 | "key cursor [MATCH pattern] [COUNT count]", 1199 | "Incrementally iterate sorted sets elements and associated scores", 1200 | 4, 1201 | "2.8.0"}, 1202 | {"ZSCORE", 1203 | "key member", 1204 | "Get the score associated with the given member in a sorted set", 1205 | 4, 1206 | "1.2.0"}, 1207 | {"ZUNIONSTORE", 1208 | "destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]", 1209 | "Add multiple sorted sets and store the resulting sorted set in a new key", 1210 | 4, 1211 | "2.0.0"}, 1212 | } 1213 | ret := map[string]commandHelp{} 1214 | for _, help := range helpers { 1215 | ret[help.Name] = help 1216 | } 1217 | return ret 1218 | } 1219 | -------------------------------------------------------------------------------- /server/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/server/icon.ico -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "io/fs" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/kataras/basicauth" 15 | "github.com/rs/cors" 16 | "github.com/xiusin/rdm/server/handler" 17 | "github.com/xiusin/rdm/server/router" 18 | "github.com/xiusin/rdm/server/windows" 19 | ) 20 | 21 | var ( 22 | mux = http.NewServeMux() 23 | basicAuthName string 24 | basicAuthPass string 25 | port = ":8787" 26 | ) 27 | 28 | //go:embed resources 29 | var embedFiles embed.FS 30 | 31 | func init() { 32 | handler.ConnectionFile = windows.GetStorePath("rdm.db") 33 | router.RegisterRouter(mux) 34 | } 35 | 36 | func main() { 37 | isDebug := strings.Contains(os.Args[0], "build") 38 | flag.StringVar(&basicAuthName, "username", "admin", "basicAuth 名称") 39 | flag.StringVar(&basicAuthPass, "password", "", "basicAuth 验证密码") 40 | flag.Parse() 41 | 42 | appAssets, err := fs.Sub(embedFiles, "resources/app") 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | mux.Handle("/", http.FileServer(http.FS(appAssets))) 48 | 49 | _handler := cors.Default().Handler(mux) 50 | var hasAuth = len(basicAuthName) > 0 && len(basicAuthPass) > 0 51 | if hasAuth { 52 | _handler = basicauth.Default(map[string]string{basicAuthName: basicAuthPass})(_handler) 53 | } 54 | 55 | if !isDebug { 56 | go func() { _ = http.ListenAndServe(port, _handler) }() 57 | time.Sleep(time.Millisecond * 100) 58 | portInt, _ := strconv.Atoi(strings.Trim(port, ":")) 59 | windows.InitWebview(fmt.Sprintf("http://localhost:%d/#/", portInt)) 60 | } else { 61 | fmt.Printf("> service listening on: \033[32mhttp://0.0.0.0:%s/ \033[0m\n", strings.Trim(port, ":")) 62 | fmt.Println("> \033[43;35missue: https://github.com/xiusin/web-redis-manager/issues\033[0m") 63 | if hasAuth { 64 | fmt.Println(`> 账号: ` + basicAuthName + ` 密码: ` + basicAuthPass) 65 | } 66 | _ = http.ListenAndServe(port, _handler) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /server/main.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/note.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/server/note.syso -------------------------------------------------------------------------------- /server/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/server/resources/icon.icns -------------------------------------------------------------------------------- /server/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/server/resources/icon.ico -------------------------------------------------------------------------------- /server/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/web-redis-manager/baf97aaf4df3cb859259cdc7910edb2cb772c2d6/server/resources/icon.png -------------------------------------------------------------------------------- /server/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/xiusin/logger" 11 | "github.com/xiusin/rdm/server/handler" 12 | ) 13 | 14 | var upgrader = websocket.Upgrader{ 15 | ReadBufferSize: 102400, 16 | WriteBufferSize: 102400, 17 | CheckOrigin: func(r *http.Request) bool { 18 | return true 19 | }, 20 | EnableCompression: true, 21 | } 22 | 23 | func RegisterRouter(mux *http.ServeMux) { 24 | var routes = map[string]handler.HandleFunc{ 25 | "/redis/connection/test": handler.RedisManagerConnectionTest, 26 | "/redis/connection/save": handler.RedisManagerConfigSave, 27 | "/redis/connection/list": handler.RedisManagerConnectionList, 28 | "/redis/connection/server": handler.RedisManagerConnectionServer, 29 | "/redis/connection/removekey": handler.RedisManagerRemoveKey, 30 | "/redis/connection/removerow": handler.RedisManagerRemoveRow, 31 | "/redis/connection/updatekey": handler.RedisManagerUpdateKey, 32 | "/redis/connection/addkey": handler.RedisManagerAddKey, 33 | "/redis/connection/flushDB": handler.RedisManagerFlushDB, 34 | "/redis/connection/remove": handler.RedisManagerRemoveConnection, 35 | "/redis/connection/renameKey": handler.RedisManagerRenameKey, 36 | "/redis/connection/moveKey": handler.RedisManagerMoveKey, 37 | "/redis/connection/command": handler.RedisManagerCommand, 38 | "/redis/connection/info": handler.RedisManagerGetInfo, 39 | "/redis/connection/get-command": handler.RedisManagerGetCommandList, 40 | } 41 | 42 | for route, handle := range routes { 43 | mux.HandleFunc(route, func(handle handler.HandleFunc) func(writer http.ResponseWriter, request *http.Request) { 44 | return func(writer http.ResponseWriter, request *http.Request) { 45 | writer.Header().Set("Content-Type", "application/json") 46 | defer func() { 47 | if err := recover(); err != nil { 48 | _, _ = writer.Write([]byte(handler.JSON(handler.ResponseData{Status: handler.FailedCode, Msg: err.(error).Error()}))) 49 | } 50 | }() 51 | 52 | var params url.Values 53 | data := make(map[string]interface{}) 54 | if request.Method == http.MethodPost { 55 | _ = request.ParseForm() 56 | params = request.PostForm 57 | } else { 58 | params = request.URL.Query() 59 | } 60 | for param, values := range params { 61 | if len(values) > 0 { 62 | data[param] = values[0] 63 | } else { 64 | data[param] = nil 65 | } 66 | } 67 | if data["id"] != nil { 68 | cfg, err := handler.GetServerCfg(data) 69 | handler.ThrowIf(err) 70 | handler.CheckReadonly(cfg.Readonly, strings.Replace(request.URL.Path, "/redis/connection/", "", 1)) 71 | } 72 | _, _ = writer.Write([]byte(handle(data))) 73 | } 74 | }(handle)) 75 | } 76 | 77 | mux.HandleFunc("/redis/connection/pubsub", func(writer http.ResponseWriter, request *http.Request) { 78 | defer func() { 79 | if err := recover(); err != nil { 80 | logger.Error(err) 81 | } 82 | }() 83 | if request.Method == http.MethodPost { 84 | data := make(map[string]interface{}) 85 | _ = request.ParseForm() 86 | params := request.PostForm 87 | for param, values := range params { 88 | if len(values) > 0 { 89 | data[param] = values[0] 90 | } else { 91 | data[param] = nil 92 | } 93 | } 94 | _, _ = writer.Write([]byte(handler.RedisPubSub(data))) 95 | return 96 | } 97 | 98 | ws, _ := upgrader.Upgrade(writer, request, nil) 99 | for { 100 | _, msg, err := ws.ReadMessage() 101 | if err != nil { 102 | break 103 | } 104 | data := make(map[string]interface{}) 105 | if err := json.Unmarshal(msg, &data); err != nil { 106 | continue 107 | } 108 | data["ws"] = ws 109 | handler.RedisPubSub(data) 110 | } 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /server/windows/path.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package windows 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func GetStorePath(path ...string) string { 12 | home, _ := os.UserHomeDir() 13 | dir := filepath.Join(home, "icloud", "个人保管库") 14 | if _, err := os.Stat(dir); err != nil { 15 | dir = filepath.Join(home, ".rdm") 16 | _ = os.MkdirAll(dir, os.ModePerm) 17 | } 18 | if len(path) > 0 { 19 | dir = filepath.Join(dir, strings.Join(path, "/")) 20 | } 21 | return dir 22 | } 23 | -------------------------------------------------------------------------------- /server/windows/path_windows.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | func GetStorePath(path ...string) string { 10 | userhome, _ := os.UserHomeDir() 11 | dir := filepath.Join(userhome, "OneDrive") 12 | if _, err := os.Stat(dir); err != nil { 13 | dir = filepath.Join(userhome, ".rdm") 14 | os.MkdirAll(dir, os.ModePerm) 15 | } 16 | if len(path) > 0 { 17 | dir = filepath.Join(dir, strings.Join(path, "/")) 18 | } 19 | return dir 20 | } 21 | -------------------------------------------------------------------------------- /server/windows/webview_darwin.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "github.com/webview/webview_go" 5 | ) 6 | 7 | func InitWebview(url string) { 8 | w := webview.New(false) 9 | defer w.Destroy() 10 | w.SetSize(1200, 800, webview.HintNone) 11 | w.SetTitle("Redis Manager") 12 | w.Navigate(url) 13 | w.Run() 14 | } 15 | -------------------------------------------------------------------------------- /server/windows/webview_linux.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | func InitWebview(url string) { 4 | var block = make(chan struct{}) 5 | 6 | <-block 7 | } 8 | -------------------------------------------------------------------------------- /server/windows/webview_windows.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "syscall" 5 | 6 | "github.com/Tim-Paik/webview2" 7 | ) 8 | 9 | var ( 10 | SmCxScreen = 0 11 | SmCyScreen = 1 12 | dll = syscall.MustLoadDLL("user32") 13 | getSystemMetrics, _ = dll.FindProc("GetSystemMetrics") 14 | ) 15 | 16 | func InitWebview(url string) { 17 | if proc, err := dll.FindProc("SetProcessDpiAwarenessContext"); err == nil { 18 | aware := -4 // 支持HIDPI 其实是根据缩放比处理 19 | _, _, _ = proc.Call(uintptr(aware)) 20 | } 21 | w := webview2.New(false) 22 | defer w.Destroy() 23 | w.Navigate(url) 24 | w.SetTitle("Redis Manager") 25 | 26 | width, height := int(float64(GetSystemMetrics(SmCxScreen))*0.7), int(float64(GetSystemMetrics(SmCyScreen))*0.7) 27 | w.SetSize(width, height, webview2.HintNone) 28 | w.Run() 29 | } 30 | 31 | func GetSystemMetrics(nIndex int) int { 32 | index := uintptr(nIndex) 33 | ret, _, _ := getSystemMetrics.Call(index) 34 | return int(ret) 35 | } 36 | -------------------------------------------------------------------------------- /vbuild.vsh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/v run 2 | import term 3 | 4 | term.clear() 5 | system("cd frontend && bun run build") 6 | system("cd server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w' -o rdm && upx -9 rdm && mv rdm ..") 7 | 8 | println("build done!") 9 | --------------------------------------------------------------------------------