├── .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 | 
77 |
78 | ### connection ###
79 |
80 | 
81 |
82 | ### key list ###
83 |
84 | 
85 |
86 | ### value ###
87 |
88 | 
89 |
90 | ### configure ###
91 |
92 | 
93 |
94 | ### server info ###
95 |
96 | 
97 |
98 | ### slow log ###
99 |
100 | 
101 |
102 | ### cli mode ###
103 |
104 | 
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 |
24 |
25 |
26 |
27 |
32 |
--------------------------------------------------------------------------------
/frontend/src/RedisManager/Index.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
2 |
5 |
6 |
7 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/RedisManager/pages/components/infoTabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Version
10 | {{ info.version }}
11 |
12 |
13 |
14 |
15 |
16 |
17 | Memory
18 | {{ info.memory }}
19 |
20 |
21 |
22 |
23 |
24 |
25 | Client
26 | {{ info.clientNum }}
27 |
28 |
29 |
30 |
31 |
32 |
33 | KEY
34 | {{ info.keyNum }}
35 |
36 |
37 |
38 |
39 |
40 |
41 | Cpu Rate
42 | {{ info.cpuRate }}
43 |
44 |
45 |
46 |
47 |
48 |
49 | Hit Ratio
50 | {{ info.ratio }}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
78 |
79 |
80 |
81 |
82 |
83 |
CPU
84 |
85 |
86 |
87 |
88 |
89 |
Key数
90 |
91 |
92 |
93 |
94 |
95 |
内存使用
96 |
97 |
98 |
99 |
100 |
101 |
连接数
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
511 |
512 |
538 |
--------------------------------------------------------------------------------
/frontend/src/RedisManager/pages/components/modals/addRowModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ rowValue.key }} 添加行操作
6 |
7 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
50 |
51 |
54 |
--------------------------------------------------------------------------------
/frontend/src/RedisManager/pages/components/serverInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ key }}
6 | {{ value }}
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
27 |
--------------------------------------------------------------------------------
/frontend/src/RedisManager/pages/components/slowLog.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 |
2 |
12 |
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 |
--------------------------------------------------------------------------------