1176 lines
33 KiB
Go
1176 lines
33 KiB
Go
package db
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
func redisDBIndex(c ConnReq) int {
|
||
db := 0
|
||
if c.Database != "" {
|
||
if n, err := strconv.Atoi(c.Database); err == nil {
|
||
db = n
|
||
}
|
||
}
|
||
return db
|
||
}
|
||
|
||
func normalizeRedisMode(c ConnReq) string {
|
||
m := strings.ToLower(strings.TrimSpace(c.RedisMode))
|
||
if m == "" {
|
||
return "standalone"
|
||
}
|
||
return m
|
||
}
|
||
|
||
// parseRedisAddrs 解析 host:port 列表(换行 / 逗号 / 分号 / 空白分隔)。
|
||
func parseRedisAddrs(raw string) []string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||
raw = strings.ReplaceAll(raw, "\r", "\n")
|
||
raw = strings.ReplaceAll(raw, ",", " ")
|
||
raw = strings.ReplaceAll(raw, ";", " ")
|
||
raw = strings.ReplaceAll(raw, "\n", " ")
|
||
parts := strings.Fields(raw)
|
||
out := make([]string, 0, len(parts))
|
||
seen := map[string]struct{}{}
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[p]; ok {
|
||
continue
|
||
}
|
||
seen[p] = struct{}{}
|
||
out = append(out, p)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func redisStandaloneOpts(c ConnReq) (*redis.Options, error) {
|
||
host := c.Host
|
||
if host == "" {
|
||
host = "127.0.0.1"
|
||
}
|
||
port := c.Port
|
||
if port == 0 {
|
||
port = 6379
|
||
}
|
||
opts := &redis.Options{
|
||
Addr: fmt.Sprintf("%s:%d", host, port),
|
||
Password: c.Password,
|
||
Username: c.User,
|
||
DB: redisDBIndex(c),
|
||
DialTimeout: 5 * time.Second,
|
||
ReadTimeout: 10 * time.Second,
|
||
WriteTimeout: 10 * time.Second,
|
||
}
|
||
if SSLEnabled(c) {
|
||
tlsCfg, err := tlsConfigFrom(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
opts.TLSConfig = tlsCfg
|
||
}
|
||
return opts, nil
|
||
}
|
||
|
||
// openRedis 按 redisMode 建 UniversalClient(单机 / Sentinel / Cluster)。
|
||
func openRedis(c ConnReq) (redis.UniversalClient, error) {
|
||
mode := normalizeRedisMode(c)
|
||
switch mode {
|
||
case "sentinel":
|
||
addrs := parseRedisAddrs(c.RedisNodes)
|
||
if len(addrs) == 0 {
|
||
return nil, fmt.Errorf("Sentinel 模式请填写至少一个 Sentinel 节点(host:port)")
|
||
}
|
||
master := strings.TrimSpace(c.MasterName)
|
||
if master == "" {
|
||
return nil, fmt.Errorf("Sentinel 模式请填写主节点名 masterName")
|
||
}
|
||
opts := &redis.FailoverOptions{
|
||
MasterName: master,
|
||
SentinelAddrs: addrs,
|
||
Password: c.Password,
|
||
Username: c.User,
|
||
SentinelPassword: c.SentinelPassword,
|
||
DB: redisDBIndex(c),
|
||
DialTimeout: 5 * time.Second,
|
||
ReadTimeout: 10 * time.Second,
|
||
WriteTimeout: 10 * time.Second,
|
||
}
|
||
if SSLEnabled(c) {
|
||
tlsCfg, err := tlsConfigFrom(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
opts.TLSConfig = tlsCfg
|
||
}
|
||
return redis.NewFailoverClient(opts), nil
|
||
case "cluster":
|
||
addrs := parseRedisAddrs(c.RedisNodes)
|
||
if len(addrs) == 0 {
|
||
return nil, fmt.Errorf("Cluster 模式请填写至少一个种子节点(host:port)")
|
||
}
|
||
opts := &redis.ClusterOptions{
|
||
Addrs: addrs,
|
||
Password: c.Password,
|
||
Username: c.User,
|
||
DialTimeout: 5 * time.Second,
|
||
ReadTimeout: 10 * time.Second,
|
||
WriteTimeout: 10 * time.Second,
|
||
}
|
||
if SSLEnabled(c) {
|
||
tlsCfg, err := tlsConfigFrom(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
opts.TLSConfig = tlsCfg
|
||
}
|
||
return redis.NewClusterClient(opts), nil
|
||
default:
|
||
opts, err := redisStandaloneOpts(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return redis.NewClient(opts), nil
|
||
}
|
||
}
|
||
|
||
// redisClientFor:会话请求复用会话级单例,非会话请求新建并随请求关闭。
|
||
func redisClientFor(c ConnReq) (cli redis.UniversalClient, done func(), err error) {
|
||
if c.SessionID != "" {
|
||
s, err := GetSession(c.SessionID)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
// 请求携带的 Database 决定用哪个 DB 的客户端(对象树切库时逐请求变化)
|
||
cli, err := s.redisClientFor(c)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return cli, func() {}, nil
|
||
}
|
||
cli, err = openRedis(c)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return cli, func() { _ = cli.Close() }, nil
|
||
}
|
||
|
||
func reqCtxRedis(parent context.Context, d time.Duration) (context.Context, context.CancelFunc) {
|
||
if parent == nil {
|
||
parent = context.Background()
|
||
}
|
||
return context.WithTimeout(parent, d)
|
||
}
|
||
|
||
func testRedis(c ConnReq) error {
|
||
cli, err := openRedis(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer cli.Close()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
return cli.Ping(ctx).Err()
|
||
}
|
||
|
||
func listDatabasesRedis(parent context.Context, c ConnReq) ([]string, error) {
|
||
// INFO keyspace 可拿已用 DB;统一暴露 0-15 便于切换,先 Ping 探活
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
|
||
defer cancel()
|
||
if err := cli.Ping(ctx).Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
// Cluster 不支持 SELECT 多库,只暴露 0
|
||
if normalizeRedisMode(c) == "cluster" {
|
||
return []string{"0"}, nil
|
||
}
|
||
out := make([]string, 16)
|
||
for i := 0; i < 16; i++ {
|
||
out[i] = strconv.Itoa(i)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func listTablesRedis(parent context.Context, c ConnReq) ([]string, error) {
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 8*time.Second)
|
||
defer cancel()
|
||
var keys []string
|
||
var cursor uint64
|
||
for {
|
||
batch, next, err := cli.Scan(ctx, cursor, "*", 200).Result()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
keys = append(keys, batch...)
|
||
cursor = next
|
||
if cursor == 0 || len(keys) >= 500 {
|
||
break
|
||
}
|
||
}
|
||
if keys == nil {
|
||
keys = []string{}
|
||
}
|
||
return keys, nil
|
||
}
|
||
|
||
// redisDangerCmds 需要 confirmDanger 确认后才执行的命令组:
|
||
// FLUSHALL/FLUSHDB/CONFIG 破坏性;KEYS 大库阻塞。
|
||
var redisDangerCmds = map[string]bool{
|
||
"FLUSHALL": true,
|
||
"FLUSHDB": true,
|
||
"CONFIG": true,
|
||
"KEYS": true,
|
||
}
|
||
|
||
func redisOK(start time.Time) ExecResult {
|
||
return ExecResult{
|
||
Columns: []string{"affected_rows"},
|
||
Rows: []map[string]interface{}{{"affected_rows": 1}},
|
||
RowCount: 1,
|
||
TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
|
||
func redisAffected(start time.Time, n int64) ExecResult {
|
||
return ExecResult{
|
||
Columns: []string{"affected_rows"},
|
||
Rows: []map[string]interface{}{{"affected_rows": n}},
|
||
RowCount: 1,
|
||
TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
|
||
func redisErr(start time.Time, err error) ExecResult {
|
||
return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
|
||
func redisUsage(start time.Time, usage string) ExecResult {
|
||
return ExecResult{Error: usage, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
|
||
func execRedis(parent context.Context, c ConnReq) ExecResult {
|
||
start := time.Now()
|
||
sql := strings.TrimSpace(c.SQL)
|
||
if sql == "" {
|
||
return ExecResult{Error: "empty sql", TimeMs: 0}
|
||
}
|
||
fields := splitRedisCmd(sql)
|
||
if len(fields) == 0 {
|
||
return ExecResult{Error: "empty command", TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
cmd := strings.ToUpper(fields[0])
|
||
args := fields[1:]
|
||
|
||
// 危险命令二段确认:未确认不执行
|
||
if redisDangerCmds[cmd] && !c.ConfirmDanger {
|
||
return ExecResult{
|
||
Error: "检测到危险命令: " + cmd,
|
||
NeedConfirm: true,
|
||
TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 10*time.Second)
|
||
defer cancel()
|
||
|
||
switch cmd {
|
||
case "PING":
|
||
val, err := cli.Ping(ctx).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
case "GET":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "GET key")
|
||
}
|
||
val, err := cli.Get(ctx, args[0]).Result()
|
||
if err == redis.Nil {
|
||
return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{}, RowCount: 0, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{{"key": args[0], "value": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
case "SET":
|
||
// 参数已按 shell 词法切分,value 即 args[1](不再 Join 破坏引号分组);
|
||
// 支持可选 EX 秒数:SET key value [EX n]
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "SET key value [EX seconds]")
|
||
}
|
||
exp := time.Duration(0)
|
||
if len(args) >= 4 && strings.ToUpper(args[2]) == "EX" {
|
||
n, err := strconv.Atoi(args[3])
|
||
if err != nil || n <= 0 {
|
||
return redisUsage(start, "SET key value EX seconds(秒数需为正整数)")
|
||
}
|
||
exp = time.Duration(n) * time.Second
|
||
}
|
||
if err := cli.Set(ctx, args[0], args[1], exp).Err(); err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisOK(start)
|
||
case "DEL":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "DEL key [key...]")
|
||
}
|
||
n, err := cli.Del(ctx, args...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "KEYS":
|
||
// 危险组已在上面对 confirmDanger 把关,此处直接执行
|
||
pat := "*"
|
||
if len(args) > 0 {
|
||
pat = args[0]
|
||
}
|
||
keys, err := cli.Keys(ctx, pat).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
rows := make([]map[string]interface{}, 0, len(keys))
|
||
for _, k := range keys {
|
||
rows = append(rows, map[string]interface{}{"key": k})
|
||
}
|
||
return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()}
|
||
case "FLUSHALL", "FLUSHDB":
|
||
// 危险组,已经过 confirmDanger 把关
|
||
var err error
|
||
if cmd == "FLUSHALL" {
|
||
err = cli.FlushAll(ctx).Err()
|
||
} else {
|
||
err = cli.FlushDB(ctx).Err()
|
||
}
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisOK(start)
|
||
case "CONFIG":
|
||
// 危险组,已经过 confirmDanger 把关;通用透传(CONFIG GET/SET ...)
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "CONFIG GET|SET parameter [value]")
|
||
}
|
||
doArgs := append([]interface{}{"CONFIG"}, strsToAnys(args)...)
|
||
val, err := cli.Do(ctx, doArgs...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": normalize(fmt.Sprintf("%v", val))}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
case "SCAN":
|
||
pat := "*"
|
||
count := int64(200)
|
||
i := 0
|
||
if len(args) > 0 {
|
||
// SCAN cursor [MATCH pattern] [COUNT n] —— 忽略 cursor,内部自扫
|
||
i = 1
|
||
}
|
||
for i < len(args) {
|
||
up := strings.ToUpper(args[i])
|
||
if up == "MATCH" && i+1 < len(args) {
|
||
pat = args[i+1]
|
||
i += 2
|
||
continue
|
||
}
|
||
if up == "COUNT" && i+1 < len(args) {
|
||
if n, err := strconv.ParseInt(args[i+1], 10, 64); err == nil && n > 0 {
|
||
count = n
|
||
}
|
||
i += 2
|
||
continue
|
||
}
|
||
i++
|
||
}
|
||
names, truncated, err := scanAll(ctx, cli, pat, count, redisKeyListCap)
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
if names == nil {
|
||
names = []string{}
|
||
}
|
||
rows := make([]map[string]interface{}, 0, len(names))
|
||
for _, k := range names {
|
||
rows = append(rows, map[string]interface{}{"key": k})
|
||
}
|
||
return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds(), Truncated: truncated}
|
||
case "EXPIRE":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "EXPIRE key seconds")
|
||
}
|
||
sec, err := strconv.Atoi(args[1])
|
||
if err != nil {
|
||
return redisUsage(start, "EXPIRE key seconds(秒数需为整数)")
|
||
}
|
||
ok, err := cli.Expire(ctx, args[0], time.Duration(sec)*time.Second).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
if !ok {
|
||
return redisAffected(start, 0)
|
||
}
|
||
return redisAffected(start, 1)
|
||
case "PERSIST":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "PERSIST key")
|
||
}
|
||
ok, err := cli.Persist(ctx, args[0]).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
if !ok {
|
||
return redisAffected(start, 0)
|
||
}
|
||
return redisAffected(start, 1)
|
||
case "HSET":
|
||
if len(args) < 3 || len(args)%2 == 0 {
|
||
return redisUsage(start, "HSET key field value [field value...]")
|
||
}
|
||
n, err := cli.HSet(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "HDEL":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "HDEL key field [field...]")
|
||
}
|
||
n, err := cli.HDel(ctx, args[0], args[1:]...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "SADD":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "SADD key member [member...]")
|
||
}
|
||
n, err := cli.SAdd(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "SREM":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "SREM key member [member...]")
|
||
}
|
||
n, err := cli.SRem(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "ZADD":
|
||
// ZADD key score member [score member...]
|
||
if len(args) < 3 || len(args)%2 == 0 {
|
||
return redisUsage(start, "ZADD key score member [score member...]")
|
||
}
|
||
var zs []redis.Z
|
||
for i := 1; i+1 < len(args); i += 2 {
|
||
score, err := strconv.ParseFloat(args[i], 64)
|
||
if err != nil {
|
||
return redisUsage(start, "ZADD score 需为数字: "+args[i])
|
||
}
|
||
zs = append(zs, redis.Z{Score: score, Member: args[i+1]})
|
||
}
|
||
n, err := cli.ZAdd(ctx, args[0], zs...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "ZREM":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "ZREM key member [member...]")
|
||
}
|
||
n, err := cli.ZRem(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "LPUSH", "RPUSH":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, cmd+" key value [value...]")
|
||
}
|
||
var n int64
|
||
var err error
|
||
if cmd == "LPUSH" {
|
||
n, err = cli.LPush(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
} else {
|
||
n, err = cli.RPush(ctx, args[0], strsToAnys(args[1:])...).Result()
|
||
}
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
case "LSET":
|
||
if len(args) < 3 {
|
||
return redisUsage(start, "LSET key index value")
|
||
}
|
||
idx, err := strconv.ParseInt(args[1], 10, 64)
|
||
if err != nil {
|
||
return redisUsage(start, "LSET key index value(index 需为整数)")
|
||
}
|
||
if err := cli.LSet(ctx, args[0], idx, args[2]).Err(); err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisOK(start)
|
||
case "INCR", "DECR":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, cmd+" key")
|
||
}
|
||
var n int64
|
||
var err error
|
||
if cmd == "INCR" {
|
||
n, err = cli.Incr(ctx, args[0]).Result()
|
||
} else {
|
||
n, err = cli.Decr(ctx, args[0]).Result()
|
||
}
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"value"}, Rows: []map[string]interface{}{{"value": n}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
case "MGET":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "MGET key [key...]")
|
||
}
|
||
vals, err := cli.MGet(ctx, args...).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
rows := make([]map[string]interface{}, 0, len(args))
|
||
for i, v := range vals {
|
||
rows = append(rows, map[string]interface{}{"key": args[i], "value": normalize(v)})
|
||
}
|
||
return ExecResult{Columns: []string{"key", "value"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()}
|
||
case "MSET":
|
||
if len(args) < 2 || len(args)%2 != 0 {
|
||
return redisUsage(start, "MSET key value [key value...]")
|
||
}
|
||
if err := cli.MSet(ctx, strsToAnys(args)...).Err(); err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisOK(start)
|
||
case "RENAME":
|
||
if len(args) < 2 {
|
||
return redisUsage(start, "RENAME key newkey")
|
||
}
|
||
if err := cli.Rename(ctx, args[0], args[1]).Err(); err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisOK(start)
|
||
case "HGETALL":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "HGETALL key")
|
||
}
|
||
c2 := c
|
||
c2.Key = args[0]
|
||
c2.Limit = redisValueRowCap
|
||
v := GetRedisKey(ctx, c2)
|
||
if v.Error != "" && v.Type == "" {
|
||
return ExecResult{Error: v.Error, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
|
||
case "LRANGE":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "LRANGE key [start stop]")
|
||
}
|
||
c2 := c
|
||
c2.Key = args[0]
|
||
c2.Limit = redisValueRowCap
|
||
v := GetRedisKey(ctx, c2)
|
||
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
|
||
case "SMEMBERS":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "SMEMBERS key")
|
||
}
|
||
c2 := c
|
||
c2.Key = args[0]
|
||
v := GetRedisKey(ctx, c2)
|
||
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
|
||
case "ZRANGE":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "ZRANGE key [start stop]")
|
||
}
|
||
c2 := c
|
||
c2.Key = args[0]
|
||
v := GetRedisKey(ctx, c2)
|
||
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
|
||
case "TYPE":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "TYPE key")
|
||
}
|
||
t, err := cli.Type(ctx, args[0]).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"key", "type"}, Rows: []map[string]interface{}{{"key": args[0], "type": t}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
case "TTL":
|
||
if len(args) < 1 {
|
||
return redisUsage(start, "TTL key")
|
||
}
|
||
n, err := cli.TTL(ctx, args[0]).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return ExecResult{Columns: []string{"key", "ttl_sec"}, Rows: []map[string]interface{}{{"key": args[0], "ttl_sec": int64(n.Seconds())}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
|
||
default:
|
||
// 未点名的命令走通用 Do,避免 CLI 只能跑白名单。
|
||
doArgs := append([]interface{}{cmd}, strsToAnys(args)...)
|
||
val, err := cli.Do(ctx, doArgs...).Result()
|
||
if err == redis.Nil {
|
||
return ExecResult{
|
||
Columns: []string{"result"},
|
||
Rows: []map[string]interface{}{{"result": "(nil)"}},
|
||
RowCount: 1,
|
||
TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return formatRedisReply(start, val)
|
||
}
|
||
}
|
||
|
||
func formatRedisReply(start time.Time, val interface{}) ExecResult {
|
||
ms := time.Since(start).Milliseconds()
|
||
switch v := val.(type) {
|
||
case nil:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": "(nil)"}}, RowCount: 1, TimeMs: ms}
|
||
case string:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
|
||
case []byte:
|
||
return formatRedisReply(start, string(v))
|
||
case int64:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
|
||
case float64:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
|
||
case bool:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
|
||
case error:
|
||
return ExecResult{Error: v.Error(), TimeMs: ms}
|
||
case []interface{}:
|
||
if len(v) == 0 {
|
||
return ExecResult{Columns: []string{"value"}, Rows: []map[string]interface{}{}, RowCount: 0, TimeMs: ms}
|
||
}
|
||
if kvRows := redisPairRows(v); kvRows != nil {
|
||
return ExecResult{Columns: []string{"field", "value"}, Rows: kvRows, RowCount: len(kvRows), TimeMs: ms}
|
||
}
|
||
rows := make([]map[string]interface{}, 0, len(v))
|
||
for i, item := range v {
|
||
rows = append(rows, map[string]interface{}{"index": i, "value": normalizeRedisReply(item)})
|
||
}
|
||
return ExecResult{Columns: []string{"index", "value"}, Rows: rows, RowCount: len(rows), TimeMs: ms}
|
||
case map[string]interface{}:
|
||
keys := make([]string, 0, len(v))
|
||
for k := range v {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
rows := make([]map[string]interface{}, 0, len(keys))
|
||
for _, k := range keys {
|
||
rows = append(rows, map[string]interface{}{"field": k, "value": normalizeRedisReply(v[k])})
|
||
}
|
||
return ExecResult{Columns: []string{"field", "value"}, Rows: rows, RowCount: len(rows), TimeMs: ms}
|
||
default:
|
||
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": normalizeRedisReply(v)}}, RowCount: 1, TimeMs: ms}
|
||
}
|
||
}
|
||
|
||
func redisPairRows(v []interface{}) []map[string]interface{} {
|
||
if len(v) < 2 || len(v)%2 != 0 {
|
||
return nil
|
||
}
|
||
for _, item := range v {
|
||
switch item.(type) {
|
||
case string, []byte:
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
rows := make([]map[string]interface{}, 0, len(v)/2)
|
||
for i := 0; i+1 < len(v); i += 2 {
|
||
rows = append(rows, map[string]interface{}{
|
||
"field": normalizeRedisReply(v[i]),
|
||
"value": normalizeRedisReply(v[i+1]),
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func normalizeRedisReply(v interface{}) interface{} {
|
||
switch t := v.(type) {
|
||
case nil:
|
||
return "(nil)"
|
||
case []byte:
|
||
return string(t)
|
||
case []interface{}:
|
||
parts := make([]string, len(t))
|
||
for i, x := range t {
|
||
parts[i] = fmt.Sprintf("%v", normalizeRedisReply(x))
|
||
}
|
||
return strings.Join(parts, " ")
|
||
default:
|
||
return normalize(t)
|
||
}
|
||
}
|
||
|
||
func strsToAnys(ss []string) []interface{} {
|
||
out := make([]interface{}, len(ss))
|
||
for i, s := range ss {
|
||
out[i] = s
|
||
}
|
||
return out
|
||
}
|
||
|
||
// splitRedisCmd 按 shell 词法切分:空格分隔,单/双引号分组,反斜杠转义。
|
||
func splitRedisCmd(s string) []string {
|
||
var out []string
|
||
var cur strings.Builder
|
||
inS, inD := false, false
|
||
esc := false
|
||
for _, r := range s {
|
||
switch {
|
||
case esc:
|
||
cur.WriteRune(r)
|
||
esc = false
|
||
case r == '\\':
|
||
esc = true
|
||
case r == '"' && !inS:
|
||
inD = !inD
|
||
case r == '\'' && !inD:
|
||
inS = !inS
|
||
case !inS && !inD && (r == ' ' || r == '\t' || r == '\n'):
|
||
if cur.Len() > 0 {
|
||
out = append(out, cur.String())
|
||
cur.Reset()
|
||
}
|
||
default:
|
||
cur.WriteRune(r)
|
||
}
|
||
}
|
||
if cur.Len() > 0 {
|
||
out = append(out, cur.String())
|
||
}
|
||
return out
|
||
}
|
||
|
||
func isRedis(d string) bool {
|
||
return driverKey(d) == "redis"
|
||
}
|
||
|
||
// IsRedisDriver exported for router guards.
|
||
func IsRedisDriver(d string) bool {
|
||
return isRedis(d)
|
||
}
|
||
|
||
func listColumnsRedis(parent context.Context, c ConnReq, key string) ([]ColumnInfo, error) {
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
|
||
defer cancel()
|
||
typ, err := cli.Type(ctx, key).Result()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ttl, _ := cli.TTL(ctx, key).Result()
|
||
return []ColumnInfo{
|
||
{Name: key, Type: typ, Comment: fmt.Sprintf("ttl=%ds", int64(ttl.Seconds()))},
|
||
}, nil
|
||
}
|
||
|
||
const redisKeyListCap = 500
|
||
const redisValueRowCap = 500
|
||
|
||
func redisKeyName(c ConnReq) string {
|
||
if c.Key != "" {
|
||
return c.Key
|
||
}
|
||
return c.Table
|
||
}
|
||
|
||
func redisScanPattern(c ConnReq) string {
|
||
p := strings.TrimSpace(c.Pattern)
|
||
if p == "" {
|
||
return "*"
|
||
}
|
||
return p
|
||
}
|
||
|
||
func redisValueLimit(c ConnReq) int {
|
||
n := c.Limit
|
||
if n <= 0 {
|
||
n = redisValueRowCap
|
||
}
|
||
if n > MaxRowLimit {
|
||
n = MaxRowLimit
|
||
}
|
||
return n
|
||
}
|
||
|
||
func ttlSeconds(d time.Duration) int64 {
|
||
// go-redis maps Redis TTL -1 (no expire) / -2 (missing) to -1ns/-2ns
|
||
// (some versions use -1s/-2s). Truncating with d/time.Second turns those into 0.
|
||
switch d {
|
||
case time.Duration(-1), -1 * time.Second:
|
||
return -1
|
||
case time.Duration(-2), -2 * time.Second:
|
||
return -2
|
||
}
|
||
return int64(d / time.Second)
|
||
}
|
||
|
||
// scanAll 全量 SCAN 到 limit 截断。集群模式遍历所有 master 分片 ——
|
||
// ClusterClient.Scan 只命中单个节点,会漏掉其他分片的键。
|
||
func scanAll(ctx context.Context, cli redis.UniversalClient, pat string, count int64, limit int) (names []string, truncated bool, err error) {
|
||
scanOne := func(c scanClient) error {
|
||
var cursor uint64
|
||
for {
|
||
batch, next, err := c.Scan(ctx, cursor, pat, count).Result()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
names = append(names, batch...)
|
||
cursor = next
|
||
if len(names) >= limit {
|
||
truncated = true
|
||
return nil
|
||
}
|
||
if cursor == 0 {
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
if cc, ok := cli.(*redis.ClusterClient); ok {
|
||
err = cc.ForEachMaster(ctx, func(ctx context.Context, mc *redis.Client) error {
|
||
return scanOne(mc)
|
||
})
|
||
if err == nil {
|
||
// 跨分片归并排序,保证键树稳定
|
||
sort.Strings(names)
|
||
if len(names) > limit {
|
||
names = names[:limit]
|
||
}
|
||
}
|
||
return names, truncated, err
|
||
}
|
||
err = scanOne(cli)
|
||
return names, truncated, err
|
||
}
|
||
|
||
// scanClient Scan 的最小接口(*redis.Client 与 UniversalClient 都满足)
|
||
type scanClient interface {
|
||
Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd
|
||
}
|
||
|
||
// ListRedisKeys SCAN + pipeline TYPE/TTL. Cap ~500.
|
||
func ListRedisKeys(parent context.Context, c ConnReq) RedisKeysResult {
|
||
start := time.Now()
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 12*time.Second)
|
||
defer cancel()
|
||
|
||
pat := redisScanPattern(c)
|
||
names, truncated, err := scanAll(ctx, cli, pat, 200, redisKeyListCap)
|
||
if err != nil {
|
||
return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
if names == nil {
|
||
names = []string{}
|
||
}
|
||
|
||
out := make([]RedisKeyInfo, len(names))
|
||
if len(names) == 0 {
|
||
return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
|
||
// Pipeline TYPE + TTL (avoid N+1)
|
||
pipe := cli.Pipeline()
|
||
typeCmds := make([]*redis.StatusCmd, len(names))
|
||
ttlCmds := make([]*redis.DurationCmd, len(names))
|
||
for i, k := range names {
|
||
typeCmds[i] = pipe.Type(ctx, k)
|
||
ttlCmds[i] = pipe.TTL(ctx, k)
|
||
}
|
||
_, _ = pipe.Exec(ctx)
|
||
for i, k := range names {
|
||
typ := "none"
|
||
if t, err := typeCmds[i].Result(); err == nil {
|
||
typ = t
|
||
}
|
||
ttl := int64(-2)
|
||
if d, err := ttlCmds[i].Result(); err == nil {
|
||
ttl = ttlSeconds(d)
|
||
}
|
||
out[i] = RedisKeyInfo{Name: k, Type: typ, TTL: ttl}
|
||
}
|
||
return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
|
||
// GetRedisKey typed value → ResultPane columns/rows.
|
||
func GetRedisKey(parent context.Context, c ConnReq) RedisKeyValue {
|
||
start := time.Now()
|
||
key := redisKeyName(c)
|
||
if key == "" {
|
||
return RedisKeyValue{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 10*time.Second)
|
||
defer cancel()
|
||
|
||
typ, err := cli.Type(ctx, key).Result()
|
||
if err != nil {
|
||
return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
ttl := int64(-2)
|
||
if d, err := cli.TTL(ctx, key).Result(); err == nil {
|
||
ttl = ttlSeconds(d)
|
||
}
|
||
limit := redisValueLimit(c)
|
||
base := RedisKeyValue{Name: key, Type: typ, TTL: ttl, Limit: limit}
|
||
|
||
switch typ {
|
||
case "none":
|
||
base.Columns = []string{"value"}
|
||
base.Rows = []map[string]interface{}{}
|
||
base.RowCount = 0
|
||
base.Error = "key 不存在"
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
case "string":
|
||
val, err := cli.Get(ctx, key).Result()
|
||
if err == redis.Nil {
|
||
base.Columns = []string{"value"}
|
||
base.Rows = []map[string]interface{}{}
|
||
base.RowCount = 0
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
if err != nil {
|
||
base.Error = errMsg(err)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
base.Columns = []string{"value"}
|
||
base.Rows = []map[string]interface{}{{"value": val}}
|
||
base.RowCount = 1
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
case "hash":
|
||
// HSCAN 游标分页:cursor 回传续载(0=起点/到底),页内按 field 排序保持观感
|
||
pairs, next, err := cli.HScan(ctx, key, uint64(c.Cursor), "*", int64(limit)).Result()
|
||
if err != nil {
|
||
base.Error = errMsg(err)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
base.Columns = []string{"field", "value"}
|
||
rows := make([]map[string]interface{}, 0, len(pairs)/2)
|
||
for i := 0; i+1 < len(pairs); i += 2 {
|
||
rows = append(rows, map[string]interface{}{"field": pairs[i], "value": pairs[i+1]})
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool { return fmt.Sprint(rows[i]["field"]) < fmt.Sprint(rows[j]["field"]) })
|
||
base.Rows = rows
|
||
base.RowCount = len(rows)
|
||
base.NextCursor = int64(next)
|
||
base.Truncated = next != 0
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
case "list":
|
||
// 偏移分页:cursor = 起始下标
|
||
offset := c.Cursor
|
||
if offset < 0 {
|
||
offset = 0
|
||
}
|
||
vals, err := cli.LRange(ctx, key, offset, offset+int64(limit)-1).Result()
|
||
if err != nil {
|
||
base.Error = errMsg(err)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
n, _ := cli.LLen(ctx, key).Result()
|
||
base.Columns = []string{"index", "value"}
|
||
rows := make([]map[string]interface{}, 0, len(vals))
|
||
for i, v := range vals {
|
||
rows = append(rows, map[string]interface{}{"index": offset + int64(i), "value": v})
|
||
}
|
||
base.Rows = rows
|
||
base.RowCount = len(rows)
|
||
if offset+int64(len(vals)) < n {
|
||
base.NextCursor = offset + int64(len(vals))
|
||
base.Truncated = true
|
||
}
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
case "set":
|
||
// SSCAN 游标分页:每页 limit,cursor 回传续载
|
||
batch, next, err := cli.SScan(ctx, key, uint64(c.Cursor), "*", int64(limit)).Result()
|
||
if err != nil {
|
||
base.Error = errMsg(err)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
base.Columns = []string{"member"}
|
||
rows := make([]map[string]interface{}, 0, len(batch))
|
||
for _, m := range batch {
|
||
rows = append(rows, map[string]interface{}{"member": m})
|
||
}
|
||
base.Rows = rows
|
||
base.RowCount = len(rows)
|
||
base.NextCursor = int64(next)
|
||
base.Truncated = next != 0
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
case "zset":
|
||
// 偏移分页(按 score 升序):cursor = 起始排名
|
||
zoffset := c.Cursor
|
||
if zoffset < 0 {
|
||
zoffset = 0
|
||
}
|
||
vals, err := cli.ZRangeWithScores(ctx, key, zoffset, zoffset+int64(limit)-1).Result()
|
||
if err != nil {
|
||
base.Error = errMsg(err)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
n, _ := cli.ZCard(ctx, key).Result()
|
||
if zoffset+int64(len(vals)) < n {
|
||
base.NextCursor = zoffset + int64(len(vals))
|
||
base.Truncated = true
|
||
}
|
||
base.Columns = []string{"score", "member"}
|
||
rows := make([]map[string]interface{}, 0, len(vals))
|
||
for _, z := range vals {
|
||
rows = append(rows, map[string]interface{}{"score": z.Score, "member": z.Member})
|
||
}
|
||
base.Rows = rows
|
||
base.RowCount = len(rows)
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
default:
|
||
// stream / recon / module types: meta only
|
||
base.Columns = []string{"info"}
|
||
base.Rows = []map[string]interface{}{{"info": fmt.Sprintf("类型 %s 暂不展开,可用命令查看", typ)}}
|
||
base.RowCount = 1
|
||
base.TimeMs = time.Since(start).Milliseconds()
|
||
return base
|
||
}
|
||
}
|
||
|
||
// DeleteRedisKey DEL one key.
|
||
func DeleteRedisKey(parent context.Context, c ConnReq) ExecResult {
|
||
start := time.Now()
|
||
key := redisKeyName(c)
|
||
if key == "" {
|
||
return ExecResult{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
|
||
defer cancel()
|
||
n, err := cli.Del(ctx, key).Result()
|
||
if err != nil {
|
||
return redisErr(start, err)
|
||
}
|
||
return redisAffected(start, n)
|
||
}
|
||
|
||
// RedisDelPatternResult 按模式批量删除(目录删除)结果。
|
||
type RedisDelPatternResult struct {
|
||
Deleted int `json:"deleted"`
|
||
Scanned int `json:"scanned"`
|
||
Truncated bool `json:"truncated"`
|
||
Error string `json:"error"`
|
||
TimeMs int64 `json:"timeMs"`
|
||
}
|
||
|
||
// 目录删除安全上限:超出即拒绝,防止误配 pattern 造成删库风暴。
|
||
const redisDelPatternCap = 10000
|
||
|
||
// DeleteRedisPattern 按模式批量删除键(SCAN + UNLINK 分批)。用于「删除目录」:
|
||
// 前端传 `目录前缀:*`。拒绝全库 `*`;扫描量超上限拒绝。
|
||
func DeleteRedisPattern(parent context.Context, c ConnReq) RedisDelPatternResult {
|
||
start := time.Now()
|
||
fail := func(msg string) RedisDelPatternResult {
|
||
return RedisDelPatternResult{Error: msg, TimeMs: time.Since(start).Milliseconds()}
|
||
}
|
||
pat := redisScanPattern(c)
|
||
if pat == "*" || pat == "" {
|
||
return fail("拒绝全库模式删除(*),请指定目录前缀")
|
||
}
|
||
cli, done, err := redisClientFor(c)
|
||
if err != nil {
|
||
return fail(errMsg(err))
|
||
}
|
||
defer done()
|
||
ctx, cancel := reqCtxRedis(parent, 60*time.Second)
|
||
defer cancel()
|
||
|
||
names, truncated, err := scanAll(ctx, cli, pat, 500, redisDelPatternCap+1)
|
||
if err != nil {
|
||
return fail(errMsg(err))
|
||
}
|
||
if len(names) > redisDelPatternCap {
|
||
return fail(fmt.Sprintf("匹配键 %d 个超过上限 %d,请缩小目录范围", len(names), redisDelPatternCap))
|
||
}
|
||
total := 0
|
||
for i := 0; i < len(names); i += 500 {
|
||
end := i + 500
|
||
if end > len(names) {
|
||
end = len(names)
|
||
}
|
||
n, err := cli.Unlink(ctx, names[i:end]...).Result()
|
||
if err != nil {
|
||
return RedisDelPatternResult{
|
||
Deleted: total, Scanned: len(names), Truncated: truncated,
|
||
Error: errMsg(err), TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
total += int(n)
|
||
}
|
||
return RedisDelPatternResult{
|
||
Deleted: total, Scanned: len(names), Truncated: truncated,
|
||
TimeMs: time.Since(start).Milliseconds(),
|
||
}
|
||
}
|
||
|