├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── SSDBAdmin ├── __init__.py ├── apps │ ├── __init__.py │ ├── hash.py │ ├── index.py │ ├── kv.py │ ├── queue.py │ └── zset.py ├── model │ ├── SSDBClient.py │ └── __init__.py ├── setting.py ├── static │ ├── css │ │ └── style.css │ ├── img │ │ ├── index.png │ │ └── queue.png │ └── js │ │ └── function.js ├── templates │ ├── 500.html │ ├── base.html │ ├── hash │ │ ├── hash.html │ │ ├── hash_clear.html │ │ ├── hash_del.html │ │ ├── hash_get.html │ │ ├── hash_scan.html │ │ └── hash_set.html │ ├── index.html │ ├── kv │ │ ├── kv_del.html │ │ ├── kv_get.html │ │ ├── kv_scan.html │ │ └── kv_set.html │ ├── pager_tabs.html │ ├── paging_tabs.html │ ├── query_part.html │ ├── queue │ │ ├── queue.html │ │ ├── queueGet.html │ │ ├── queue_clear.html │ │ ├── queue_pop.html │ │ ├── queue_push.html │ │ └── queue_range.html │ ├── select_size.html │ └── zset │ │ ├── zset.html │ │ ├── zset_clear.html │ │ ├── zset_del.html │ │ ├── zset_get.html │ │ ├── zset_range.html │ │ └── zset_set.html ├── util.py └── utils │ └── paginator.py ├── Test ├── __init__.py └── test.py ├── requirements.txt └── run.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | # - nightly 5 | os: 6 | - linux 7 | install: 8 | - pip install -r requirements.txt 9 | 10 | script: python ./Test/test.py -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | MAINTAINER jhao104 "j_hao104@163.com" 4 | 5 | ENV TZ Asia/Shanghai 6 | 7 | COPY ./requirements.txt /requirements.txt 8 | 9 | WORKDIR / 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | COPY . / 14 | 15 | 16 | ENTRYPOINT [ "python", "run.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SSDBAdmin 2 | ======= 3 | [](https://travis-ci.org/jhao104/SSDBAdmin) 4 |  5 | [](https://requires.io/github/jhao104/SSDBAdmin/requirements/?branch=master) 6 | [](http://www.spiderpy.cn/blog/) 7 | 8 | SSDB数据库的可视化界面管理工具 9 | 10 | 11 | ## 功能 12 | 提供SSDB数据的hash/zset/kv/queue等数据结构的增删改查等功能 13 | 14 | ## 依赖 15 | 16 | * Python 3.x (Python2请移步[py2](https://github.com/jhao104/SSDBAdmin/tree/dev_py2)) 17 | 18 | * Flask 19 | 20 | ## 安装 21 | 22 | 下载项目到本地`git clone https://github.com/jhao104/SSDBAdmin.git` 23 | 24 | 编辑配置文件`SSDBAdmin/setting.py`: 25 | ``` 26 | # SSDB config 27 | DB_CONFIG = [ 28 | { 29 | "host": "127.0.0.1", 30 | "port": 8888 31 | }, 32 | { 33 | "host": "127.0.0.1", 34 | "port": 8899 35 | } 36 | ] 37 | 38 | # service config 39 | SERVICE_CONFIG = { 40 | "host": "0.0.0.0", 41 | "port": 5000, 42 | "debug": True 43 | } 44 | ``` 45 | 将`host`和`port`修改成正确值。 46 | 47 | 安装依赖包: 48 | ```pip install -r requirements.txt``` 49 | 50 | 启动: 51 | ```python run.py``` 52 | 53 | 访问:http://127.0.0.1:5000/ssdbadmin 54 | 55 | ## Docker 56 | 57 | ```bash 58 | $ docker pull jhao104/ssdb-admin 59 | 60 | $ docker run --env DB_CONFIG=127.0.0.1:8888 -p 5000:5000 jhao104/ssdb-admin 61 | 62 | ``` 63 | 64 | ## Release notes 65 | 66 | * 3.0.0 67 | 68 | * 使用python3版本,不再支持python2。 python2用户请使用2.0.0及以下版本。 69 | 70 | * 2.0.0 71 | 72 | * python2版本分割。 73 | 74 | * 1.1 75 | 76 | * 使用redis.py模块代替ssdb-py模块,ssdb-py模块长期没有维护更新,许多新功能不支持。 77 | 78 | * 1.0 79 | 80 | * First release of SSDBAdmin; 81 | * `List`/`Hashmap`/`Set`/`KeyValue` 操作; 82 | 83 | ## Screenshots 84 | 85 |  86 | 87 |  88 | -------------------------------------------------------------------------------- /SSDBAdmin/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | app.config.from_object('SSDBAdmin.setting') 5 | 6 | from SSDBAdmin.apps import index 7 | from SSDBAdmin.apps import kv 8 | from SSDBAdmin.apps import hash 9 | from SSDBAdmin.apps import zset 10 | from SSDBAdmin.apps import queue 11 | -------------------------------------------------------------------------------- /SSDBAdmin/apps/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -------------------------------------------------------------------------------- /SSDBAdmin/apps/hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: hash 5 | Description : hash view 6 | Author : JHao 7 | date: 2018/9/3 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/9/3: hash view 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin import app 16 | from SSDBAdmin.model.SSDBClient import SSDBClient 17 | from flask import render_template, request, make_response, redirect, url_for 18 | 19 | 20 | @app.route('/ssdbadmin/hash/') 21 | def hashLists(): 22 | """ 23 | show the list of hash 24 | :return: 25 | """ 26 | page_num = int(request.args.get('page_num', 1)) 27 | page_size = request.args.get('page_size') 28 | if not page_size: 29 | page_size = request.cookies.get('SIZE', 10) 30 | start = request.args.get('start', '') 31 | end = request.args.get('end', '') 32 | 33 | db_client = SSDBClient(request) 34 | hash_list, has_next = db_client.hashList(name_start=start, name_end=end, page_num=page_num, 35 | page_size=int(page_size)) 36 | select_arg = {'start': start, 'end': end, 'page_size': int(page_size)} 37 | resp = make_response(render_template('hash/hash.html', hash_list=hash_list, has_next=has_next, 38 | has_prev=page_num > 1, 39 | page_num=page_num, select_arg=select_arg, active='hash')) 40 | resp.set_cookie("SIZE", str(page_size), httponly=True, samesite='Lax') 41 | return resp 42 | 43 | 44 | @app.route('/ssdbadmin/hash/scan') 45 | def hashScan(): 46 | """ 47 | show the list of hash item 48 | :return: 49 | """ 50 | name = request.args.get('name') 51 | key_start = request.args.get('start', '') 52 | page_size = request.args.get('page_size') 53 | page_number = int(request.args.get('page_num', 1)) 54 | if not page_size: 55 | page_size = request.cookies.get('SIZE', 10) 56 | db_client = SSDBClient(request) 57 | limit = int(page_size) * page_number 58 | item_list = db_client.hashScan(name, key_start, "", limit=limit + 1) 59 | if len(item_list) > limit: 60 | has_next = True 61 | item_list = item_list[-int(page_size) - 1:-1] 62 | else: 63 | has_next = False 64 | item_list = item_list[-int(page_size):] 65 | select_arg = {'page_size': int(page_size), 'start': key_start} 66 | resp = make_response(render_template('hash/hash_scan.html', 67 | item_list=item_list, 68 | page_num=page_number, 69 | key_start=key_start, 70 | name=name, 71 | has_next=has_next, 72 | has_prev=page_number > 1, 73 | select_arg=select_arg, 74 | active='hash')) 75 | resp.set_cookie('SIZE', str(page_size), httponly=True, samesite='Lax') 76 | return resp 77 | 78 | 79 | @app.route('/ssdbadmin/hash/set', methods=['GET', 'POST']) 80 | def hashSet(): 81 | """ 82 | Set the value of key within the hash_name 83 | :return: 84 | """ 85 | if request.method == 'GET': 86 | name = request.args.get('name') 87 | key = request.args.get('key', '') 88 | value = request.args.get('value', '') 89 | return render_template('hash/hash_set.html', name=name, key=key, value=value, active='hash') 90 | else: 91 | name = request.form.get('name') 92 | key = request.form.get('key') 93 | value = request.form.get('value') 94 | SSDBClient(request).hashSet(name, key, value) 95 | return redirect(url_for('hashScan', name=name)) 96 | pass 97 | 98 | 99 | @app.route('/ssdbadmin/hash/del/', methods=['GET', 'POST']) 100 | def hashDel(): 101 | """ 102 | remove keys from zset_name 103 | :return: 104 | """ 105 | if request.method == 'GET': 106 | name = request.args.get('name') 107 | key = request.args.get('key') 108 | keys = request.args.getlist('keys') 109 | if key: 110 | keys.append(key) 111 | return render_template('hash/hash_del.html', keys=keys, name=name, active='hash') 112 | else: 113 | keys = request.form.getlist('key') 114 | name = request.form.get('name') 115 | SSDBClient(request).hashDel(name, *keys) 116 | return redirect(url_for('hashScan', name=name)) 117 | 118 | 119 | @app.route('/ssdbadmin/hash/clear/', methods=['GET', 'POST']) 120 | def hashClear(): 121 | """ 122 | delete the specified hash data 123 | :return: 124 | """ 125 | if request.method == 'POST': 126 | name = request.form.get('name') 127 | SSDBClient(request).hashClear(name) 128 | return redirect(url_for('hashLists')) 129 | else: 130 | name = request.args.get('name') 131 | return render_template('hash/hash_clear.html', name=name, active='hash') 132 | pass 133 | 134 | 135 | @app.route('/ssdbadmin/hash/get/') 136 | def hashGet(): 137 | """ 138 | show an item info from hash 139 | :return: 140 | """ 141 | name = request.args.get('name') 142 | key = request.args.get('key') 143 | value = SSDBClient(request).hashGet(name, key) 144 | return render_template('hash/hash_get.html', name=name, value=value, key=key, active='hash') 145 | -------------------------------------------------------------------------------- /SSDBAdmin/apps/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: index 5 | Description : index view 6 | Author : JHao 7 | date: 2018/8/24 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/8/24: index view 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin import app 16 | from SSDBAdmin.setting import DB_CONFIG, VERSION 17 | from SSDBAdmin.model.SSDBClient import getSAServer, SSDBClient 18 | from flask import render_template, request, make_response, redirect, url_for 19 | 20 | 21 | @app.errorhandler(500) 22 | def internal_error(error): 23 | return render_template('500.html', error=error) 24 | 25 | 26 | # @app.errorhandler(404) 27 | # def not_found_error(error): 28 | # return redirect(url_for('index')) 29 | 30 | 31 | @app.route('/ssdbadmin/') 32 | def index(): 33 | host, port = getSAServer(request) 34 | server_info = SSDBClient(request).serverInfo() 35 | resp = make_response(render_template('index.html', server_info=server_info)) 36 | resp.set_cookie('SSDBADMINSERVER', '{host}:{port}'.format(host=host, port=port), httponly=True, samesite='Lax') 37 | return resp 38 | 39 | 40 | @app.context_processor 41 | def commonParam(): 42 | host, port = getSAServer(request) 43 | server_list = ['{}:{}'.format(server.get('host'), server.get('port')) for server in DB_CONFIG] 44 | current_server = '{}:{}'.format(host, port) 45 | return dict(server_list=server_list, current_server=current_server, version=VERSION) 46 | -------------------------------------------------------------------------------- /SSDBAdmin/apps/kv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: kv 5 | Description : kv view 6 | Author : JHao 7 | date: 2018/9/4 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/9/4: kv view 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin import app 16 | from SSDBAdmin.model.SSDBClient import SSDBClient 17 | from flask import render_template, request, make_response, redirect, url_for 18 | 19 | 20 | @app.route('/ssdbadmin/kv/scan') 21 | def kvScan(): 22 | """ 23 | show the list of kv item 24 | """ 25 | key_start = request.args.get('start', '') 26 | page_size = request.args.get('page_size') 27 | page_number = int(request.args.get('page_num', 1)) 28 | if not page_size: 29 | page_size = request.cookies.get('SIZE', 10) 30 | db_client = SSDBClient(request) 31 | limit = int(page_size) * page_number 32 | item_list = db_client.kvScan(key_start, "", limit=limit + 1) 33 | if len(item_list) > limit: 34 | has_next = True 35 | item_list = item_list[-int(page_size) - 1:-1] 36 | else: 37 | has_next = False 38 | item_list = item_list[-int(page_size):] 39 | select_arg = {'page_size': int(page_size), 'start': key_start} 40 | resp = make_response(render_template('kv/kv_scan.html', 41 | item_list=item_list, 42 | page_num=page_number, 43 | key_start=key_start, 44 | has_next=has_next, 45 | has_prev=page_number > 1, 46 | select_arg=select_arg, 47 | active='kv')) 48 | resp.set_cookie('SIZE', str(page_size), httponly=True, samesite='Lax') 49 | return resp 50 | 51 | 52 | @app.route('/ssdbadmin/kv/get/') 53 | def kvGet(): 54 | """ 55 | show a kv info 56 | """ 57 | key = request.args.get('key') 58 | value, ttl = SSDBClient(request).kvGet(key) 59 | return render_template('kv/kv_get.html', key=key, value=value, ttl=ttl, active='kv') 60 | pass 61 | 62 | 63 | @app.route('/ssdbadmin/kv/set/', methods=['GET', 'POST']) 64 | def kvSet(): 65 | """ 66 | add item to kv 67 | """ 68 | if request.method == 'GET': 69 | key = request.args.get('key', '') 70 | value = request.args.get('value', '') 71 | return render_template('kv/kv_set.html', key=key, value=value, active='kv') 72 | else: 73 | key = request.form.get('key') 74 | value = request.form.get('value') 75 | SSDBClient(request).kvSet(key, value) 76 | return redirect(url_for('kvScan')) 77 | 78 | 79 | @app.route('/ssdbadmin/kv/del/', methods=['GET', 'POST']) 80 | def kvDel(): 81 | """ 82 | remove keys from kv 83 | :return: 84 | """ 85 | if request.method == 'GET': 86 | key = request.args.get('key') 87 | keys = request.args.getlist('keys') 88 | if key: 89 | keys.append(key) 90 | return render_template('kv/kv_del.html', keys=keys, active='kv') 91 | else: 92 | keys = request.form.getlist('key') 93 | if keys: 94 | SSDBClient(request).kvDel(*keys) 95 | return redirect(url_for('kvScan')) 96 | -------------------------------------------------------------------------------- /SSDBAdmin/apps/queue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: queue 5 | Description : queue view 6 | Author : JHao 7 | date: 2018/8/27 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/8/27: queue view 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin import app 16 | from SSDBAdmin.model.SSDBClient import SSDBClient 17 | from SSDBAdmin.utils.paginator import getPagingTabsInfo, getPageNumberInfo 18 | from flask import render_template, request, make_response, redirect, url_for 19 | 20 | 21 | @app.route('/ssdbadmin/queue/') 22 | def queueLists(): 23 | """ 24 | show the items of queue/list 25 | :return: 26 | """ 27 | page_num = int(request.args.get('page_num', 1)) 28 | page_size = request.args.get('page_size') 29 | if not page_size: 30 | page_size = request.cookies.get('SIZE', 10) 31 | start = request.args.get('start', '') 32 | end = request.args.get('end', '') 33 | 34 | db_client = SSDBClient(request) 35 | queue_list, has_next = db_client.queueList(name_start=start, name_end=end, page_num=page_num, 36 | page_size=int(page_size)) 37 | select_arg = {'start': start, 'end': end, 'page_size': int(page_size)} 38 | resp = make_response(render_template('queue/queue.html', queue_list=queue_list, has_next=has_next, 39 | has_prev=page_num > 1, 40 | page_num=page_num, select_arg=select_arg, active='queue')) 41 | resp.set_cookie("SIZE", str(page_size), httponly=True, samesite='Lax') 42 | return resp 43 | 44 | 45 | @app.route('/ssdbadmin/queue/push/', methods=['GET', 'POST']) 46 | def queuePush(): 47 | """ 48 | add item to queue(support back/front type) 49 | Returns: 50 | """ 51 | if request.method == 'GET': 52 | queue_name = request.args.get('name') 53 | return render_template('queue/queue_push.html', queue_name=queue_name, active='queue') 54 | elif request.method == "POST": 55 | queue_name = request.form.get('queue_name') 56 | push_type = request.form.get('type') 57 | item = request.form.get('item') 58 | SSDBClient(request).queuePush(queue_name, item, push_type) 59 | return redirect(url_for('queueRange', name=queue_name)) 60 | 61 | 62 | @app.route('/ssdbadmin/queue/pop/', methods=['GET', 'POST']) 63 | def queuePop(): 64 | """ 65 | pop item(s) from queue (support back/front type) 66 | :return: 67 | """ 68 | if request.method == 'GET': 69 | queue_name = request.args.get('name') 70 | return render_template('queue/queue_pop.html', queue_name=queue_name, active='queue') 71 | else: 72 | queue_name = request.form.get('name') 73 | pop_type = request.form.get('type') 74 | number = request.form.get('number') 75 | SSDBClient(request).queuePop(queue_name, number, pop_type) 76 | return redirect(url_for('queueRange', name=queue_name)) 77 | 78 | 79 | @app.route('/ssdbadmin/queue/range/') 80 | def queueRange(): 81 | """ 82 | show the list of item from queue 83 | :return: 84 | """ 85 | queue_name = request.args.get('name') 86 | start = request.args.get('start') 87 | page_num = request.args.get('page_num', 1) 88 | page_size = request.args.get('page_size') 89 | if not page_size: 90 | page_size = request.cookies.get('SIZE', "10") 91 | 92 | db_object = SSDBClient(request) 93 | item_total = db_object.queueSize(queue_name) 94 | page_count, page_num = getPagingTabsInfo(item_total, page_num, page_size) 95 | offset = (page_num - 1) * int(page_size) 96 | if start and start.isdigit(): 97 | page_num = getPageNumberInfo(int(start), page_count, page_size) 98 | offset = (page_num - 1) * int(page_size) 99 | else: 100 | start = offset 101 | 102 | item_list = db_object.queueRange(queue_name, offset=offset, limit=page_size) 103 | select_arg = {'page_size': int(page_size)} 104 | resp = make_response(render_template('queue/queue_range.html', 105 | item_list=item_list, 106 | name=queue_name, 107 | page_num=int(page_num), 108 | page_count=page_count, 109 | select_arg=select_arg, 110 | start_index=offset, 111 | start=start, 112 | data_total=item_total, 113 | active='queue')) 114 | resp.set_cookie('SIZE', page_size, httponly=True, samesite='Lax') 115 | return resp 116 | 117 | 118 | @app.route('/ssdbadmin/queue/get/') 119 | def queueGet(): 120 | """ 121 | show an item info from queue 122 | :return: 123 | """ 124 | queue_name = request.args.get('name') 125 | index = request.args.get('index') 126 | item = SSDBClient(request).queueGet(queue_name, index) 127 | return render_template('queue/queueGet.html', name=queue_name, item=item, index=index, active='queue') 128 | 129 | 130 | @app.route('/ssdbadmin/queue/clear/', methods=['GET', 'POST']) 131 | def queueClear(): 132 | """ 133 | delete the specified queue 134 | :return: 135 | """ 136 | if request.method == 'POST': 137 | queue_name = request.form.get('name') 138 | SSDBClient(request).queueClear(queue_name) 139 | return redirect(url_for('queueLists')) 140 | else: 141 | queue_name = request.args.get('name') 142 | return render_template('queue/queue_clear.html', name=queue_name, active='queue') 143 | -------------------------------------------------------------------------------- /SSDBAdmin/apps/zset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: zset 5 | Description : zset view 6 | Author : JHao 7 | date: 2018/8/30 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/8/30: zset view 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin import app 16 | from SSDBAdmin.model.SSDBClient import SSDBClient 17 | from SSDBAdmin.utils.paginator import getPagingTabsInfo, getPageNumberInfo 18 | from flask import render_template, request, make_response, redirect, url_for 19 | 20 | 21 | @app.route('/ssdbadmin/zset/') 22 | def zsetLists(): 23 | """ 24 | show the items of zset 25 | :return: 26 | """ 27 | page_num = int(request.args.get('page_num', 1)) 28 | page_size = request.args.get('page_size') 29 | if not page_size: 30 | page_size = request.cookies.get('SIZE', 20) 31 | start = request.args.get('start', '') 32 | db_client = SSDBClient(request) 33 | zset_list, has_next = db_client.zsetList(start=start, page_num=page_num, page_size=int(page_size)) 34 | select_arg = {'start': start, 'page_size': int(page_size)} 35 | resp = make_response(render_template('zset/zset.html', zset_list=zset_list, has_next=has_next, 36 | has_prev=page_num > 1, 37 | page_num=page_num, select_arg=select_arg, active='zset')) 38 | resp.set_cookie('SIZE', str(page_size), httponly=True, samesite='Lax') 39 | return resp 40 | 41 | 42 | @app.route('/ssdbadmin/zset/set/', methods=['GET', 'POST']) 43 | def zsetSet(): 44 | """ 45 | add item to zset 46 | :return: 47 | """ 48 | if request.method == 'GET': 49 | name = request.args.get('name') 50 | key = request.args.get('key', '') 51 | score = request.args.get('score', '') 52 | return render_template('zset/zset_set.html', name=name, key=key, score=score, active='zset') 53 | else: 54 | name = request.form.get('name') 55 | key = request.form.get('key') 56 | score = request.form.get('score') 57 | try: 58 | score = int(score) 59 | except ValueError: 60 | score = 0 61 | SSDBClient(request).zsetSet(name, key, score) 62 | return redirect(url_for('zsetRange', name=name)) 63 | 64 | 65 | @app.route('/ssdbadmin/zset/range/') 66 | def zsetRange(): 67 | """ 68 | show the list of item from zst 69 | :return: 70 | """ 71 | set_name = request.args.get('name') 72 | start = request.args.get('start', "") 73 | page_num = request.args.get('page_num', 1) 74 | page_size = request.args.get('page_size') 75 | if not page_size: 76 | page_size = request.cookies.get('SIZE', 10) 77 | 78 | db_object = SSDBClient(request) 79 | item_total = db_object.zsetSize(set_name) 80 | page_count, page_num = getPagingTabsInfo(item_total, page_num, page_size) 81 | offset = (page_num - 1) * int(page_size) 82 | if start: 83 | rank = db_object.zsetRank(set_name, start) 84 | if rank: 85 | page_num = getPageNumberInfo(rank, page_count, page_size) 86 | offset = (page_num - 1) * int(page_size) 87 | 88 | item_list = db_object.zsetRange(set_name, offset=offset, limit=page_size) 89 | select_arg = {'page_size': int(page_size)} 90 | resp = make_response(render_template('zset/zset_range.html', 91 | item_list=item_list, 92 | name=set_name, 93 | page_num=int(page_num), 94 | page_count=page_count, 95 | select_arg=select_arg, 96 | start=start, 97 | active='zset')) 98 | resp.set_cookie('SIZE', str(page_size), httponly=True, samesite='Lax') 99 | return resp 100 | 101 | 102 | @app.route('/ssdbadmin/zset/del/', methods=['GET', 'POST']) 103 | def zsetDel(): 104 | """ 105 | remove keys from zset 106 | :return: 107 | """ 108 | if request.method == 'GET': 109 | name = request.args.get('name') 110 | key = request.args.get('key') 111 | keys = request.args.getlist('keys') 112 | if key: 113 | keys.append(key) 114 | return render_template('zset/zset_del.html', keys=keys, name=name, active='zset') 115 | else: 116 | keys = request.form.getlist('key') 117 | name = request.form.get('name') 118 | SSDBClient(request).zsetDel(name, *keys) 119 | return redirect(url_for('zsetRange', name=name)) 120 | 121 | 122 | @app.route('/ssdbadmin/zset/zclear/', methods=['GET', 'POST']) 123 | def zsetClear(): 124 | """ 125 | delete the specified zset data 126 | :return: 127 | """ 128 | if request.method == 'POST': 129 | name = request.form.get('name') 130 | SSDBClient(request).zsetClear(name) 131 | return redirect(url_for('zsetLists')) 132 | else: 133 | queue_name = request.args.get('name') 134 | return render_template('zset/zset_clear.html', name=queue_name, active='zset') 135 | 136 | 137 | @app.route('/ssdbadmin/zset/zget/') 138 | def zset_zget(): 139 | """ 140 | show item info from zset 141 | :return: 142 | """ 143 | name = request.args.get('name') 144 | key = request.args.get('key') 145 | score = SSDBClient(request).zsetGet(name, key) 146 | return render_template('zset/zset_get.html', name=name, score=score, key=key, active='zset') 147 | -------------------------------------------------------------------------------- /SSDBAdmin/model/SSDBClient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: SSDBClient 5 | Description : SSDBClient 6 | Author : JHao 7 | date: 2018/8/24 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2018/8/24: SSDBClient 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | from SSDBAdmin.utils.paginator import getPagingTabsInfo 16 | from redis.connection import BlockingConnectionPool 17 | from SSDBAdmin.setting import DB_CONFIG 18 | from redis import Redis 19 | 20 | 21 | def getSAServer(request): 22 | """ 23 | Get key `SSDBADMIN_SERVER` from request’s cookie 24 | Args: 25 | request: flask request 26 | Returns: 27 | current ssdb's host and post 28 | """ 29 | if 'SSDBADMIN_SERVER' in request.args: 30 | host, port = request.args.get('SSDBADMIN_SERVER').split(':') 31 | elif 'SSDBADMINSERVER' in request.cookies: 32 | host, port = request.cookies.get('SSDBADMINSERVER').split(':') 33 | else: 34 | server = DB_CONFIG[0] 35 | host = server.get('host', 'localhost') 36 | port = server.get('port', 8888) 37 | return host, port 38 | 39 | 40 | class SSDBClient(object): 41 | def __init__(self, request): 42 | host, port = getSAServer(request) 43 | self.__conn = Redis(connection_pool=BlockingConnectionPool(host=host, port=int(port))) 44 | 45 | def serverInfo(self): 46 | """ 47 | DB service info(version/links/size/stats ...) 48 | Returns: 49 | info message as dict 50 | """ 51 | info_list = [_.decode() if isinstance(_, bytes) else _ for _ in self.__conn.execute_command('info')] 52 | version = info_list[2] 53 | links = info_list[4] 54 | total_calls = info_list[6] 55 | db_size = info_list[8] 56 | bin_logs = info_list[10] 57 | service_key_range = info_list[12] 58 | data_key_range = info_list[14] 59 | stats = info_list[16] 60 | 61 | def _parseDiskUsage(_stats): 62 | return sum([int(each.split()[2]) for each in _stats.split('\n')[3:-1]]) 63 | 64 | return {'info_list': info_list, 'version': version, 'links': links, 'total_calls': total_calls, 65 | 'db_size': db_size, 'bin_logs': bin_logs, 'service_key_range': service_key_range, 66 | 'data_key_range': data_key_range, 67 | 'stats': stats, 'disk_usage': _parseDiskUsage(stats)} 68 | 69 | # region queue operate 70 | def queueList(self, name_start, name_end, page_num, page_size): 71 | """ 72 | return queue/list items in range(`name_start`, `name_end`) 73 | Args: 74 | name_start: The lower bound(not included) of keys to be returned, empty string ``''`` means -inf 75 | name_end: The upper bound(included) of keys to be returned, empty string ``''`` means +inf 76 | page_num: page number 77 | page_size: items size per page 78 | Returns: 79 | items list 80 | """ 81 | limit = (page_num + 1) * page_size 82 | items_list = [_.decode() for _ in self.__conn.execute_command('qlist', name_start, name_end, limit)] 83 | page_count, page_num = getPagingTabsInfo(data_count=len(items_list), page_no=page_num, page_row_num=page_size) 84 | has_next = True if page_count > page_num else False 85 | queue_list = map(lambda queue_name: {'name': queue_name, 'size': self.__conn.llen(queue_name)}, 86 | items_list[(page_num - 1) * page_size: page_num * page_size]) 87 | return queue_list, has_next 88 | 89 | def queuePush(self, queue_name, item, push_type): 90 | """ 91 | Add `item` to the back/front(`push_type`) of queue which named `queue_name` 92 | Args: 93 | queue_name: queue's name 94 | item: the push item 95 | push_type: push type (back/front) 96 | Returns: 97 | None 98 | """ 99 | if push_type == 'front': 100 | self.__conn.lpush(queue_name, item) 101 | else: 102 | # push_type = 'back' 103 | self.__conn.rpush(queue_name, item) 104 | 105 | def queuePop(self, queue_name, number, pop_type): 106 | """ 107 | Pop `number` items from head/end(`pop_type`) of the queue which named `queue_name` 108 | Args: 109 | queue_name: queue name 110 | number: the number of pop items 111 | pop_type: back/front 112 | Returns: 113 | None 114 | """ 115 | if pop_type == 'front': 116 | self.__conn.execute_command('qpop_front', queue_name, int(number)) 117 | else: 118 | # pop_type = 'back 119 | self.__conn.execute_command('qpop_back', queue_name, int(number)) 120 | 121 | def queueRange(self, queue_name, offset, limit): 122 | """ 123 | Return a portion of item from the queue which named `queue_name` 124 | at the specified range[`offset`, `offset` + `limit`] 125 | Args: 126 | queue_name: queue_name: queue name 127 | offset: offset 128 | limit: limit 129 | 130 | Returns: 131 | items as List 132 | """ 133 | start, end = int(offset), int(offset) + int(limit) - 1 134 | items_list = self.__conn.lrange(queue_name, start, end) 135 | return [_.decode('utf8') for _ in items_list if isinstance(_, bytes)] 136 | 137 | def queueSize(self, queue_name): 138 | """ 139 | the size of queue 140 | Args: 141 | queue_name: queue name 142 | Returns: 143 | size(int) 144 | """ 145 | return self.__conn.llen(queue_name) 146 | 147 | def queueGet(self, queue_name, index): 148 | """ 149 | Get the item at `index` position from the queue which named `queue_name` 150 | Args: 151 | queue_name: queue name 152 | index: index (int) 153 | 154 | Returns: 155 | item info (str) 156 | """ 157 | return self.__conn.lindex(queue_name, int(index)).decode() 158 | 159 | def queueClear(self, queue_name): 160 | """ 161 | **Clear&Delete** the queue specified by `queue_name` 162 | Args: 163 | queue_name: queue name 164 | Returns: 165 | None 166 | """ 167 | return self.__conn.execute_command('qclear', queue_name) 168 | 169 | # endregion queue operate 170 | 171 | # region zset operate 172 | def zsetList(self, start, page_num, page_size): 173 | """ 174 | return zset items in range(`name_start`, `name_end`) 175 | Args: 176 | start: The lower bound(not included) of keys to be returned, empty string ``''`` means -inf 177 | page_num: page number 178 | page_size: page size 179 | 180 | Returns: 181 | items list 182 | """ 183 | limit = (page_num + 1) * page_size 184 | name_list = [_.decode("utf8", "ignore") for _ in self.__conn.execute_command('zlist', start, '', limit)] 185 | page_count, page_num = getPagingTabsInfo(data_count=len(name_list), page_no=page_num, page_row_num=page_size) 186 | has_next = True if page_count > page_num else False 187 | zset_list = map(lambda zset_name: {'name': zset_name, 'size': self.__conn.zcard(zset_name)}, 188 | name_list[(page_num - 1) * page_size: page_num * page_size - 1]) 189 | return zset_list, has_next 190 | 191 | def zsetSet(self, name, key, score): 192 | """ 193 | Set `key` with `score` into zset which named `name` 194 | Args: 195 | name: zset name 196 | key: key 197 | score: score 198 | Returns: 199 | None 200 | """ 201 | return self.__conn.execute_command('zset', name, key, score) 202 | 203 | def zsetGet(self, zset_name, key): 204 | """ 205 | Return the score of element ``key`` in zset_name 206 | :param zset_name: 207 | :param key: 208 | :return: 209 | """ 210 | return self.__conn.zscore(zset_name, key) 211 | 212 | def zsetRange(self, zset_name, offset, limit): 213 | """ 214 | Return a portion of item from the zset which named `zset_name` 215 | at the specified range[`offset`, `offset` + `limit`] 216 | Args: 217 | zset_name: zset name 218 | offset: offset 219 | limit: limit 220 | Returns: 221 | 222 | """ 223 | start, end = int(offset), int(offset) + int(limit) - 1 224 | key_list = self.__conn.zrange(zset_name, start, end) 225 | key_list = [_.decode() for _ in key_list if isinstance(_, bytes)] 226 | return [{"key": _, "score": int(self.__conn.zscore(zset_name, _))} for _ in key_list] 227 | 228 | def zsetRank(self, zset_name, key): 229 | """ 230 | Returns a 0-based value indicating the rank of ``value`` in sorted set 231 | Args: 232 | zset_name: zset name 233 | key: key 234 | Returns: 235 | 236 | """ 237 | return self.__conn.zrank(zset_name, key) 238 | 239 | def zsetDel(self, zset_name, *keys): 240 | """ 241 | Delete specified multiple keys of zset which named `zset_name`. 242 | Args: 243 | zset_name: zset name 244 | *keys: will del keys 245 | Returns: 246 | None 247 | """ 248 | return self.__conn.execute_command('multi_zdel', zset_name, *keys) 249 | 250 | def zsetClear(self, zset_name): 251 | """ 252 | **Clear&Delete** the zset named `zset_name` specified by `zset_name` 253 | :param zset_name: zset name 254 | :return: 255 | """ 256 | return self.__conn.execute_command('zclear', zset_name) 257 | 258 | def zsetSize(self, zset_name): 259 | """ 260 | the size of zst 261 | Args: 262 | zset_name: zset name 263 | Returns: 264 | size(int) 265 | """ 266 | return self.__conn.zcard(zset_name) 267 | 268 | # endregion zset operate 269 | 270 | # region hash operate 271 | def hashList(self, name_start, name_end, page_num, page_size): 272 | """ 273 | return hash_map items in range(`name_start`, `name_end`) 274 | Args: 275 | name_start: The lower bound(not included) of keys to be returned, empty string ``''`` means -inf 276 | name_end: The upper bound(included) of keys to be returned, empty string ``''`` means +inf 277 | page_num: page number 278 | page_size: items size per page 279 | Returns: 280 | items list 281 | """ 282 | limit = (page_num + 1) * page_size 283 | items_list = [_.decode() for _ in self.__conn.execute_command('hlist', name_start, name_end, limit)] 284 | page_count, page_num = getPagingTabsInfo(data_count=len(items_list), page_no=page_num, page_row_num=page_size) 285 | has_next = True if page_count > page_num else False 286 | hash_list = map(lambda hash_name: {'name': hash_name, 'size': self.__conn.hlen(hash_name)}, 287 | items_list[(page_num - 1) * page_size: page_num * page_size]) 288 | return hash_list, has_next 289 | 290 | def hashScan(self, hash_name, key_start, key_end, limit): 291 | """ 292 | List key-value pairs of a hashmap with keys in range (key_start, key_end]. 293 | Args: 294 | hash_name: hash name 295 | key_start: start key 296 | key_end: end key 297 | limit: limit 298 | Returns: 299 | list of hash item 300 | """ 301 | item_list = self.__conn.execute_command('hscan', hash_name, key_start, key_end, limit) 302 | item_list = [_.decode() for _ in item_list if isinstance(_, bytes)] 303 | hash_list = [{'key': item_list[index], 'value': item_list[index + 1]} for index in range(0, len(item_list), 2)] 304 | return hash_list 305 | 306 | def hashSet(self, hash_name, key, value): 307 | """ 308 | Set ``key`` to ``value`` within hash ``hash_name`` 309 | Args: 310 | hash_name: hash name 311 | key: key 312 | value: value 313 | Returns: 314 | None 315 | """ 316 | self.__conn.hset(hash_name, key, value) 317 | 318 | def hashGet(self, hash_name, key): 319 | """ 320 | Return the value of ``key`` within the hash ``hash_name`` 321 | Args: 322 | hash_name: hash name 323 | key: key 324 | Returns: 325 | hash value(str) 326 | """ 327 | value = self.__conn.hget(hash_name, key) 328 | return value.decode() if isinstance(value, bytes) else value 329 | 330 | def hashDel(self, hash_name, *keys): 331 | """ 332 | "Delete ``keys`` from hash ``hash_name``" 333 | Args: 334 | hash_name: hash name 335 | *keys: keys 336 | Returns: 337 | None 338 | """ 339 | return self.__conn.execute_command('multi_hdel', hash_name, *keys) 340 | 341 | def hashClear(self, hash_name): 342 | """ 343 | **Clear&Delete** the hash specified by `hash_name` 344 | Args: 345 | hash_name: hash name 346 | Returns: 347 | None 348 | """ 349 | self.__conn.execute_command('hclear', hash_name) 350 | 351 | def hashSize(self, hash_name): 352 | """ 353 | the hash of zst 354 | Args: 355 | hash_name: hash name 356 | Returns: 357 | 358 | size(int) 359 | """ 360 | return self.__conn.hlen(hash_name) 361 | 362 | # endregion hash operate 363 | 364 | # region kv operate 365 | def kvScan(self, key_start, key_end, limit): 366 | """ 367 | List key-value pairs of a kv with keys in range (key_start, key_end]. 368 | Args: 369 | key_start: start key 370 | key_end: end key 371 | limit: limit 372 | Returns: 373 | list of hash item 374 | """ 375 | item_list = self.__conn.execute_command('scan', key_start, key_end, limit) 376 | item_list = [_.decode() for _ in item_list if isinstance(_, bytes)] 377 | hash_list = [{'key': item_list[index], 'value': item_list[index + 1]} for index in range(0, len(item_list), 2)] 378 | return hash_list 379 | 380 | def kvGet(self, key): 381 | """ 382 | Return the value at key ``key``, or ``None`` if the key doesn't exist 383 | and the number of seconds until the key ``name`` will expire 384 | Args: 385 | key: key 386 | Returns: 387 | key's value and ttl sec 388 | """ 389 | value = self.__conn.get(key) 390 | value = value.decode() if isinstance(value, bytes) else value 391 | ttl = self.__conn.execute_command('ttl', key) 392 | return value, ttl 393 | 394 | def kvSet(self, key, value): 395 | """ 396 | Set the value of the key 397 | Args: 398 | key: key 399 | value: value 400 | Returns: 401 | None 402 | """ 403 | self.__conn.execute_command('set', key, value) 404 | 405 | def kvDel(self, *keys): 406 | """ 407 | Delete one or more keys specified by ``keys`` 408 | Args: 409 | *keys: keys 410 | Returns: 411 | None 412 | """ 413 | self.__conn.execute_command('multi_del', *keys) 414 | 415 | # endregion kv operate 416 | 417 | 418 | if __name__ == '__main__': 419 | class R(object): 420 | @property 421 | def args(self): 422 | return {"SSDBADMIN_SERVER": "127.0.0.1:8080"} 423 | 424 | @property 425 | def cookies(self): 426 | return {"SSDBADMIN_SERVER": "127.0.0.1:8080"} 427 | 428 | 429 | request = R() 430 | db = SSDBClient(request) 431 | -------------------------------------------------------------------------------- /SSDBAdmin/model/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -------------------------------------------------------------------------------- /SSDBAdmin/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DB_CONFIG = [ 4 | { 5 | "host": "127.0.0.1", 6 | "port": 8888 7 | }, 8 | { 9 | "host": "127.0.0.1", # 可配置多个db, 自由切换 10 | "port": 8080 11 | } 12 | ] 13 | 14 | SERVICE_CONFIG = { 15 | "host": "0.0.0.0", 16 | "port": 5000, 17 | "debug": False 18 | } 19 | 20 | # ENSURE_ASCII = False 21 | 22 | VERSION = '3.0.0' 23 | 24 | # ------ get config by env(in Docker) ---- 25 | 26 | db_config = os.getenv("DB_CONFIG", "") # 127.0.0.1:8888,127.0.0.1:8080 27 | if db_config: 28 | try: 29 | DB_CONFIG = [{"host": _.split(":")[0], 30 | "port": _.split(":")[1]} for _ in db_config.split(',')] 31 | except Exception: 32 | pass 33 | 34 | server_host = os.getenv("SERVER_HOST", "") 35 | if server_host: 36 | SERVICE_CONFIG["host"] = server_host 37 | 38 | server_port = os.getenv("SERVER_PORT", "") 39 | if server_port: 40 | SERVICE_CONFIG["port"] = server_port 41 | -------------------------------------------------------------------------------- /SSDBAdmin/static/css/style.css: -------------------------------------------------------------------------------- 1 | .foot{ 2 | text-align: center; 3 | padding: 15px 0; 4 | color: #999; 5 | font-size: 90% 6 | } 7 | 8 | .clear{ 9 | clear: both; 10 | height: 10px 11 | } 12 | 13 | .center{ 14 | text-align: center; 15 | } 16 | 17 | .page_info{ 18 | height:30px; 19 | line-height:30px; 20 | width:100px; 21 | overflow:hidden; 22 | } -------------------------------------------------------------------------------- /SSDBAdmin/static/img/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhao104/SSDBAdmin/fbefa85e7343500c3ebabf373bd33bb2f40c28a0/SSDBAdmin/static/img/index.png -------------------------------------------------------------------------------- /SSDBAdmin/static/img/queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhao104/SSDBAdmin/fbefa85e7343500c3ebabf373bd33bb2f40c28a0/SSDBAdmin/static/img/queue.png -------------------------------------------------------------------------------- /SSDBAdmin/static/js/function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jh on 2017/6/1. 3 | */ 4 | 5 | /* change SSDB server */ 6 | $(function () { 7 | $('select[name=SSDBADMIN_SERVER]').change(function(){ 8 | var url = '/ssdbadmin?SSDBADMIN_SERVER=' + $(this).val(); 9 | location.href = url; 10 | return false; 11 | }) 12 | 13 | }) 14 | 15 | function check_all(cb){ 16 | $('#data_list input.cb').each(function(i, e){ 17 | e.checked = cb.checked; 18 | }); 19 | } -------------------------------------------------------------------------------- /SSDBAdmin/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {{ error }} 4 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 || Hash9 | | Size10 | | Action11 | | 
|---|---|---|
| {{ hash.name }}17 | | {{ hash.size }}18 | | 19 | 20 | 21 |22 | | 
Hash:
{{ name }} 4 |Key:
{{ key }} 5 |Value({{ value|count }} bytes):
{{ value }} 6 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/hash/hash_scan.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |{{ name }}| version10 | | {{ server_info.version }}11 | | 
| links14 | | {{ server_info.links }}15 | | 
| total_calls18 | | {{ server_info.total_calls }}19 | | 
| dbsize22 | | {{ server_info.db_size }}23 | | 
| binlogs26 | | {{ server_info.bin_logs }}27 | | 
| serv_key_range30 | | {{ server_info.service_key_range }}31 | | 
| data_key_range34 | | {{ server_info.data_key_range }}35 | | 
| leveldb.stats38 | | 39 | 42 |{{ server_info.stats }}
40 |                 41 | | 
| disk_usage45 | | {{ server_info.disk_usage }} MB46 | | 
Key:
{{ key }} 4 |Value({{ value|count }} bytes):
{{ value }} 5 |TTL:
{{ ttl }} 6 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/kv/kv_scan.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% block content %} 3 | {% import 'query_part.html' as query %} 4 | {{ query.query_part(add=url_for('kvSet'), select_arg=select_arg) }} 5 | 6 | 39 | {% include 'pager_tabs.html' %} 40 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/kv/kv_set.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 || Queue10 | | Size11 | | Action12 | | 
|---|---|---|
| {{ queue.name }}18 | | {{ queue.size }}19 | | 20 | 21 | 22 | 23 | 24 | 25 | 26 |27 | | 
List:
{{ name }} 4 |Index:
{{ index }} 5 |Item({{ item|count }} bytes):
{{ item }} 6 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/queue/queue_clear.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/queue/queue_pop.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% block content %} 3 |{{ queue_name }}{{ name }}| Index30 | | Item31 | | Item Length32 | | Action33 | | 
|---|---|---|---|
| {{ start_index + loop.index0 }}39 | | {{ item|safe|truncate(100, True) }}40 | | {{ item|count }}41 | | 42 | | 
| Zset9 | | Size10 | | Action11 | | 
|---|---|---|
| {{ zset.name }}17 | | {{ zset.size }}18 | | 19 | 20 | 21 |22 | | 
Zset:
{{ name }} 4 |Key:
{{ key }} 5 |Score:
{{ score }} 6 | {% endblock %} -------------------------------------------------------------------------------- /SSDBAdmin/templates/zset/zset_range.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |{{ name }}