feat: support cluster mode

This commit is contained in:
tiny-craft 2023-10-14 21:26:47 +08:00
parent 44f8581a41
commit 444d0ea199
14 changed files with 614 additions and 248 deletions

View File

@ -7,11 +7,13 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"log"
"net" "net"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
. "tinyrdm/backend/storage" . "tinyrdm/backend/storage"
"tinyrdm/backend/types" "tinyrdm/backend/types"
@ -37,7 +39,7 @@ type connectionService struct {
} }
type connectionItem struct { type connectionItem struct {
rdb *redis.Client client redis.UniversalClient
ctx context.Context ctx context.Context
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
} }
@ -67,9 +69,9 @@ func (c *connectionService) Start(ctx context.Context) {
func (c *connectionService) Stop(ctx context.Context) { func (c *connectionService) Stop(ctx context.Context) {
for _, item := range c.connMap { for _, item := range c.connMap {
if item.rdb != nil { if item.client != nil {
item.cancelFunc() item.cancelFunc()
item.rdb.Close() item.client.Close()
} }
} }
c.connMap = map[string]connectionItem{} c.connMap = map[string]connectionItem{}
@ -114,6 +116,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
} }
option := &redis.Options{ option := &redis.Options{
ClientName: config.Name,
Addr: fmt.Sprintf("%s:%d", config.Addr, config.Port), Addr: fmt.Sprintf("%s:%d", config.Addr, config.Port),
Username: config.Username, Username: config.Username,
Password: config.Password, Password: config.Password,
@ -131,13 +134,14 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
return option, nil return option, nil
} }
func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*redis.Client, error) { func (c *connectionService) createRedisClient(config types.ConnectionConfig) (redis.UniversalClient, error) {
option, err := c.buildOption(config) option, err := c.buildOption(config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if config.Sentinel.Enable { if config.Sentinel.Enable {
// get master address via sentinel node
sentinel := redis.NewSentinelClient(option) sentinel := redis.NewSentinelClient(option)
defer sentinel.Close() defer sentinel.Close()
@ -155,6 +159,57 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
} }
rdb := redis.NewClient(option) rdb := redis.NewClient(option)
if config.Cluster.Enable {
// connect to cluster
var slots []redis.ClusterSlot
if slots, err = rdb.ClusterSlots(c.ctx).Result(); err == nil {
log.Println(slots)
clusterOptions := &redis.ClusterOptions{
//NewClient: nil,
//MaxRedirects: 0,
//RouteByLatency: false,
//RouteRandomly: false,
//ClusterSlots: nil,
ClientName: option.ClientName,
Dialer: option.Dialer,
OnConnect: option.OnConnect,
Protocol: option.Protocol,
Username: option.Username,
Password: option.Password,
MaxRetries: option.MaxRetries,
MinRetryBackoff: option.MinRetryBackoff,
MaxRetryBackoff: option.MaxRetryBackoff,
DialTimeout: option.DialTimeout,
ContextTimeoutEnabled: option.ContextTimeoutEnabled,
PoolFIFO: option.PoolFIFO,
PoolSize: option.PoolSize,
PoolTimeout: option.PoolTimeout,
MinIdleConns: option.MinIdleConns,
MaxIdleConns: option.MaxIdleConns,
ConnMaxIdleTime: option.ConnMaxIdleTime,
ConnMaxLifetime: option.ConnMaxLifetime,
TLSConfig: option.TLSConfig,
DisableIndentity: option.DisableIndentity,
}
if option.Dialer != nil {
clusterOptions.Dialer = option.Dialer
clusterOptions.ReadTimeout = -2
clusterOptions.WriteTimeout = -2
}
var addrs []string
for _, slot := range slots {
for _, node := range slot.Nodes {
addrs = append(addrs, node.Addr)
}
}
clusterOptions.Addrs = addrs
clusterClient := redis.NewClusterClient(clusterOptions)
return clusterClient, nil
} else {
return nil, err
}
}
return rdb, nil return rdb, nil
} }
@ -193,14 +248,14 @@ func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (
} }
func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) { func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) {
rdb, err := c.createRedisClient(config) client, err := c.createRedisClient(config)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
defer rdb.Close() defer client.Close()
if _, err = rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil { if _, err = client.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
resp.Msg = err.Error() resp.Msg = err.Error()
} else { } else {
resp.Success = true resp.Success = true
@ -318,7 +373,7 @@ func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp typ
// OpenConnection open redis server connection // OpenConnection open redis server connection
func (c *connectionService) OpenConnection(name string) (resp types.JSResp) { func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(name, 0) client, ctx, err := c.getRedisClient(name, 0)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -330,22 +385,51 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
var totaldb int var totaldb int
if selConn.DBFilterType == "" || selConn.DBFilterType == "none" { if selConn.DBFilterType == "" || selConn.DBFilterType == "none" {
// get total databases // get total databases
if config, err := rdb.ConfigGet(ctx, "databases").Result(); err == nil { if config, err := client.ConfigGet(ctx, "databases").Result(); err == nil {
if total, err := strconv.Atoi(config["databases"]); err == nil { if total, err := strconv.Atoi(config["databases"]); err == nil {
totaldb = total totaldb = total
} }
} }
} }
// parse all db, response content like below
var dbs []types.ConnectionDB
var clusterKeyCount int64
cluster, isCluster := client.(*redis.ClusterClient)
if isCluster {
var keyCount atomic.Int64
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
if size, serr := cli.DBSize(ctx).Result(); serr != nil {
return serr
} else {
keyCount.Add(size)
}
return nil
})
if err != nil {
resp.Msg = "get db size error:" + err.Error()
return
}
clusterKeyCount = keyCount.Load()
// only one database in cluster mode
dbs = []types.ConnectionDB{
{
Name: "db0",
Index: 0,
Keys: int(clusterKeyCount),
},
}
} else {
// get database info // get database info
res, err := rdb.Info(ctx, "keyspace").Result() var res string
res, err = client.Info(ctx, "keyspace").Result()
if err != nil { if err != nil {
resp.Msg = "get server info fail:" + err.Error() resp.Msg = "get server info fail:" + err.Error()
return return
} }
// parse all db, response content like below
var dbs []types.ConnectionDB
info := c.parseInfo(res) info := c.parseInfo(res)
if totaldb <= 0 { if totaldb <= 0 {
// cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace // cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
keyspace := info["Keyspace"] keyspace := info["Keyspace"]
@ -379,6 +463,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
} }
} }
} }
switch selConn.DBFilterType { switch selConn.DBFilterType {
case "show": case "show":
filterList := sliceutil.Unique(selConn.DBFilterList) filterList := sliceutil.Unique(selConn.DBFilterList)
@ -397,6 +482,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
dbs = append(dbs, queryDB(idx)) dbs = append(dbs, queryDB(idx))
} }
} }
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
@ -410,9 +496,9 @@ func (c *connectionService) CloseConnection(name string) (resp types.JSResp) {
item, ok := c.connMap[name] item, ok := c.connMap[name]
if ok { if ok {
delete(c.connMap, name) delete(c.connMap, name)
if item.rdb != nil { if item.client != nil {
item.cancelFunc() item.cancelFunc()
item.rdb.Close() item.client.Close()
} }
} }
resp.Success = true resp.Success = true
@ -421,24 +507,19 @@ func (c *connectionService) CloseConnection(name string) (resp types.JSResp) {
// get redis client from local cache or create a new open // get redis client from local cache or create a new open
// if db >= 0, will also switch to db index // if db >= 0, will also switch to db index
func (c *connectionService) getRedisClient(connName string, db int) (*redis.Client, context.Context, error) { func (c *connectionService) getRedisClient(connName string, db int) (redis.UniversalClient, context.Context, error) {
item, ok := c.connMap[connName] item, ok := c.connMap[connName]
var rdb *redis.Client var client redis.UniversalClient
var ctx context.Context var ctx context.Context
if ok { if ok {
rdb, ctx = item.rdb, item.ctx client, ctx = item.client, item.ctx
} else { } else {
selConn := c.conns.GetConnection(connName) selConn := c.conns.GetConnection(connName)
if selConn == nil { if selConn == nil {
return nil, nil, fmt.Errorf("no match connection \"%s\"", connName) return nil, nil, fmt.Errorf("no match connection \"%s\"", connName)
} }
var err error hook := redis2.NewHook(connName, func(cmd string, cost int64) {
rdb, err = c.createRedisClient(selConn.ConnectionConfig)
if err != nil {
return nil, nil, fmt.Errorf("create conenction error: %s", err.Error())
}
rdb.AddHook(redis2.NewHook(connName, func(cmd string, cost int64) {
now := time.Now() now := time.Now()
//last := strings.LastIndex(cmd, ":") //last := strings.LastIndex(cmd, ":")
//if last != -1 { //if last != -1 {
@ -450,26 +531,48 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie
Cmd: cmd, Cmd: cmd,
Cost: cost, Cost: cost,
}) })
})) })
if _, err = rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil { var err error
client, err = c.createRedisClient(selConn.ConnectionConfig)
if err != nil {
return nil, nil, fmt.Errorf("create conenction error: %s", err.Error())
}
// add hook to each node in cluster mode
var cluster *redis.ClusterClient
if cluster, ok = client.(*redis.ClusterClient); ok {
err = cluster.ForEachShard(c.ctx, func(ctx context.Context, cli *redis.Client) error {
cli.AddHook(hook)
return nil
})
if err != nil {
return nil, nil, fmt.Errorf("get cluster nodes error: %s", err.Error())
}
} else {
client.AddHook(hook)
}
if _, err = client.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
return nil, nil, errors.New("can not connect to redis server:" + err.Error()) return nil, nil, errors.New("can not connect to redis server:" + err.Error())
} }
var cancelFunc context.CancelFunc var cancelFunc context.CancelFunc
ctx, cancelFunc = context.WithCancel(c.ctx) ctx, cancelFunc = context.WithCancel(c.ctx)
c.connMap[connName] = connectionItem{ c.connMap[connName] = connectionItem{
rdb: rdb, client: client,
ctx: ctx, ctx: ctx,
cancelFunc: cancelFunc, cancelFunc: cancelFunc,
} }
} }
if db >= 0 { if db >= 0 {
var rdb *redis.Client
if rdb, ok = client.(*redis.Client); ok && rdb != nil {
if err := rdb.Do(ctx, "select", strconv.Itoa(db)).Err(); err != nil { if err := rdb.Do(ctx, "select", strconv.Itoa(db)).Err(); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
return rdb, ctx, nil }
return client, ctx, nil
} }
// parse command response content which use "redis info" // parse command response content which use "redis info"
@ -511,14 +614,14 @@ func (c *connectionService) parseDBItemInfo(info string) map[string]int {
// ServerInfo get server info // ServerInfo get server info
func (c *connectionService) ServerInfo(name string) (resp types.JSResp) { func (c *connectionService) ServerInfo(name string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(name, 0) client, ctx, err := c.getRedisClient(name, 0)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
// get database info // get database info
res, err := rdb.Info(ctx).Result() res, err := client.Info(ctx).Result()
if err != nil { if err != nil {
resp.Msg = "get server info fail:" + err.Error() resp.Msg = "get server info fail:" + err.Error()
return return
@ -537,41 +640,48 @@ func (c *connectionService) OpenDatabase(connName string, db int, match string,
// ScanKeys scan all keys // ScanKeys scan all keys
func (c *connectionService) ScanKeys(connName string, db int, match, keyType string) (resp types.JSResp) { func (c *connectionService) ScanKeys(connName string, db int, match, keyType string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
filterType := len(keyType) > 0 filterType := len(keyType) > 0
var countPerScan int64 = 10000
// define sub scan function
scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k any)) error {
var iter *redis.ScanIterator
if filterType {
iter = cli.ScanType(ctx, 0, match, countPerScan, keyType).Iterator()
} else {
iter = cli.Scan(ctx, 0, match, countPerScan).Iterator()
}
for iter.Next(ctx) {
appendFunc(strutil.EncodeRedisKey(iter.Val()))
}
return iter.Err()
}
var keys []any var keys []any
//keys := map[string]keyItem{} if cluster, ok := client.(*redis.ClusterClient); ok {
var cursor uint64 // cluster mode
for { var mutex sync.Mutex
var loadedKey []string err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
if filterType { return scan(ctx, cli, func(k any) {
loadedKey, cursor, err = rdb.ScanType(ctx, cursor, match, 10000, keyType).Result() mutex.Lock()
keys = append(keys, k)
mutex.Unlock()
})
})
} else { } else {
loadedKey, cursor, err = rdb.Scan(ctx, cursor, match, 10000).Result() err = scan(ctx, client, func(k any) {
keys = append(keys, k)
})
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
for _, k := range loadedKey {
keys = append(keys, strutil.EncodeRedisKey(k))
}
//for _, k := range loadedKey {
// //t, _ := rdb.Type(ctx, k).Result()
// keys[k] = keyItem{Type: "t"}
//}
//keys = append(keys, loadedKey...)
// no more loadedKey
if cursor == 0 {
break
}
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
@ -582,7 +692,7 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
// GetKeyValue get value by key // GetKeyValue get value by key
func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs string) (resp types.JSResp) { func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -591,7 +701,7 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var keyType string var keyType string
var dur time.Duration var dur time.Duration
keyType, err = rdb.Type(ctx, key).Result() keyType, err = client.Type(ctx, key).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -603,7 +713,7 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
} }
var ttl int64 var ttl int64
if dur, err = rdb.TTL(ctx, key).Result(); err != nil { if dur, err = client.TTL(ctx, key).Result(); err != nil {
ttl = -1 ttl = -1
} else { } else {
if dur < 0 { if dur < 0 {
@ -619,18 +729,18 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
switch strings.ToLower(keyType) { switch strings.ToLower(keyType) {
case "string": case "string":
var str string var str string
str, err = rdb.Get(ctx, key).Result() str, err = client.Get(ctx, key).Result()
value, viewAs = strutil.ConvertTo(str, viewAs) value, viewAs = strutil.ConvertTo(str, viewAs)
size, _ = rdb.StrLen(ctx, key).Result() size, _ = client.StrLen(ctx, key).Result()
case "list": case "list":
value, err = rdb.LRange(ctx, key, 0, -1).Result() value, err = client.LRange(ctx, key, 0, -1).Result()
size, _ = rdb.LLen(ctx, key).Result() size, _ = client.LLen(ctx, key).Result()
case "hash": case "hash":
//value, err = rdb.HGetAll(ctx, key).Result() //value, err = client.HGetAll(ctx, key).Result()
items := map[string]string{} items := map[string]string{}
for { for {
var loadedVal []string var loadedVal []string
loadedVal, cursor, err = rdb.HScan(ctx, key, cursor, "*", 10000).Result() loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", 10000).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -643,13 +753,13 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
} }
} }
value = items value = items
size, _ = rdb.HLen(ctx, key).Result() size, _ = client.HLen(ctx, key).Result()
case "set": case "set":
//value, err = rdb.SMembers(ctx, key).Result() //value, err = client.SMembers(ctx, key).Result()
items := []string{} items := []string{}
for { for {
var loadedKey []string var loadedKey []string
loadedKey, cursor, err = rdb.SScan(ctx, key, cursor, "*", 10000).Result() loadedKey, cursor, err = client.SScan(ctx, key, cursor, "*", 10000).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -660,13 +770,13 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
} }
} }
value = items value = items
size, _ = rdb.SCard(ctx, key).Result() size, _ = client.SCard(ctx, key).Result()
case "zset": case "zset":
//value, err = rdb.ZRangeWithScores(ctx, key, 0, -1).Result() //value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result()
var items []types.ZSetItem var items []types.ZSetItem
for { for {
var loadedVal []string var loadedVal []string
loadedVal, cursor, err = rdb.ZScan(ctx, key, cursor, "*", 10000).Result() loadedVal, cursor, err = client.ZScan(ctx, key, cursor, "*", 10000).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -685,11 +795,11 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
} }
} }
value = items value = items
size, _ = rdb.ZCard(ctx, key).Result() size, _ = client.ZCard(ctx, key).Result()
case "stream": case "stream":
var msgs []redis.XMessage var msgs []redis.XMessage
items := []types.StreamItem{} items := []types.StreamItem{}
msgs, err = rdb.XRevRange(ctx, key, "+", "-").Result() msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -701,7 +811,7 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
}) })
} }
value = items value = items
size, _ = rdb.XLen(ctx, key).Result() size, _ = client.XLen(ctx, key).Result()
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -721,7 +831,7 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
// SetKeyValue set value by key // SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl // @param ttl <= 0 means keep current ttl
func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) { func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -730,7 +840,7 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var expiration time.Duration var expiration time.Duration
if ttl < 0 { if ttl < 0 {
if expiration, err = rdb.PTTL(ctx, key).Result(); err != nil { if expiration, err = client.PTTL(ctx, key).Result(); err != nil {
expiration = redis.KeepTTL expiration = redis.KeepTTL
} }
} else { } else {
@ -747,10 +857,10 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error()) resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
return return
} }
_, err = rdb.Set(ctx, key, saveStr, 0).Result() _, err = client.Set(ctx, key, saveStr, 0).Result()
// set expiration lonely, not "keepttl" // set expiration lonely, not "keepttl"
if err == nil && expiration > 0 { if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration) client.Expire(ctx, key, expiration)
} }
} }
case "list": case "list":
@ -758,9 +868,9 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
resp.Msg = "invalid list value" resp.Msg = "invalid list value"
return return
} else { } else {
err = rdb.LPush(ctx, key, strs...).Err() err = client.LPush(ctx, key, strs...).Err()
if err == nil && expiration > 0 { if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration) client.Expire(ctx, key, expiration)
} }
} }
case "hash": case "hash":
@ -770,7 +880,7 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
} else { } else {
total := len(strs) total := len(strs)
if total > 1 { if total > 1 {
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < total; i += 2 { for i := 0; i < total; i += 2 {
pipe.HSet(ctx, key, strs[i], strs[i+1]) pipe.HSet(ctx, key, strs[i], strs[i+1])
} }
@ -787,9 +897,9 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
return return
} else { } else {
if len(strs) > 0 { if len(strs) > 0 {
err = rdb.SAdd(ctx, key, strs...).Err() err = client.SAdd(ctx, key, strs...).Err()
if err == nil && expiration > 0 { if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration) client.Expire(ctx, key, expiration)
} }
} }
} }
@ -807,9 +917,9 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
Member: strs[i], Member: strs[i],
}) })
} }
err = rdb.ZAdd(ctx, key, members...).Err() err = client.ZAdd(ctx, key, members...).Err()
if err == nil && expiration > 0 { if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration) client.Expire(ctx, key, expiration)
} }
} }
} }
@ -819,13 +929,13 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
return return
} else { } else {
if len(strs) > 2 { if len(strs) > 2 {
err = rdb.XAdd(ctx, &redis.XAddArgs{ err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key, Stream: key,
ID: strs[0].(string), ID: strs[0].(string),
Values: strs[1:], Values: strs[1:],
}).Err() }).Err()
if err == nil && expiration > 0 { if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration) client.Expire(ctx, key, expiration)
} }
} }
} }
@ -844,7 +954,7 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
// SetHashValue set hash field // SetHashValue set hash field
func (c *connectionService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) { func (c *connectionService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -855,23 +965,23 @@ func (c *connectionService) SetHashValue(connName string, db int, k any, field,
updatedField := map[string]string{} updatedField := map[string]string{}
if len(field) <= 0 { if len(field) <= 0 {
// old filed is empty, add new field // old filed is empty, add new field
_, err = rdb.HSet(ctx, key, newField, value).Result() _, err = client.HSet(ctx, key, newField, value).Result()
updatedField[newField] = value updatedField[newField] = value
} else if len(newField) <= 0 { } else if len(newField) <= 0 {
// new field is empty, delete old field // new field is empty, delete old field
_, err = rdb.HDel(ctx, key, field, value).Result() _, err = client.HDel(ctx, key, field, value).Result()
removedField = append(removedField, field) removedField = append(removedField, field)
} else if field == newField { } else if field == newField {
// replace field // replace field
_, err = rdb.HSet(ctx, key, newField, value).Result() _, err = client.HSet(ctx, key, newField, value).Result()
updatedField[newField] = value updatedField[newField] = value
} else { } else {
// remove old field and add new field // remove old field and add new field
if _, err = rdb.HDel(ctx, key, field).Result(); err != nil { if _, err = client.HDel(ctx, key, field).Result(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
_, err = rdb.HSet(ctx, key, newField, value).Result() _, err = client.HSet(ctx, key, newField, value).Result()
removedField = append(removedField, field) removedField = append(removedField, field)
updatedField[newField] = value updatedField[newField] = value
} }
@ -890,7 +1000,7 @@ func (c *connectionService) SetHashValue(connName string, db int, k any, field,
// AddHashField add or update hash field // AddHashField add or update hash field
func (c *connectionService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) { func (c *connectionService) AddHashField(connName string, db int, k any, action int, fieldItems []any) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -902,7 +1012,7 @@ func (c *connectionService) AddHashField(connName string, db int, k any, action
case 1: case 1:
// ignore duplicated fields // ignore duplicated fields
for i := 0; i < len(fieldItems); i += 2 { for i := 0; i < len(fieldItems); i += 2 {
_, err = rdb.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result() _, err = client.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result()
if err == nil { if err == nil {
updated[fieldItems[i].(string)] = fieldItems[i+1] updated[fieldItems[i].(string)] = fieldItems[i+1]
} }
@ -911,9 +1021,9 @@ func (c *connectionService) AddHashField(connName string, db int, k any, action
// overwrite duplicated fields // overwrite duplicated fields
total := len(fieldItems) total := len(fieldItems)
if total > 1 { if total > 1 {
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { _, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < total; i += 2 { for i := 0; i < total; i += 2 {
rdb.HSet(ctx, key, fieldItems[i], fieldItems[i+1]) client.HSet(ctx, key, fieldItems[i], fieldItems[i+1])
} }
return nil return nil
}) })
@ -936,7 +1046,7 @@ func (c *connectionService) AddHashField(connName string, db int, k any, action
// AddListItem add item to list or remove from it // AddListItem add item to list or remove from it
func (c *connectionService) AddListItem(connName string, db int, k any, action int, items []any) (resp types.JSResp) { func (c *connectionService) AddListItem(connName string, db int, k any, action int, items []any) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -947,11 +1057,11 @@ func (c *connectionService) AddListItem(connName string, db int, k any, action i
switch action { switch action {
case 0: case 0:
// push to head // push to head
_, err = rdb.LPush(ctx, key, items...).Result() _, err = client.LPush(ctx, key, items...).Result()
leftPush = append(leftPush, items...) leftPush = append(leftPush, items...)
default: default:
// append to tail // append to tail
_, err = rdb.RPush(ctx, key, items...).Result() _, err = client.RPush(ctx, key, items...).Result()
rightPush = append(rightPush, items...) rightPush = append(rightPush, items...)
} }
if err != nil { if err != nil {
@ -969,7 +1079,7 @@ func (c *connectionService) AddListItem(connName string, db int, k any, action i
// SetListItem update or remove list item by index // SetListItem update or remove list item by index
func (c *connectionService) SetListItem(connName string, db int, k any, index int64, value string) (resp types.JSResp) { func (c *connectionService) SetListItem(connName string, db int, k any, index int64, value string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -980,13 +1090,13 @@ func (c *connectionService) SetListItem(connName string, db int, k any, index in
updated := map[int64]string{} updated := map[int64]string{}
if len(value) <= 0 { if len(value) <= 0 {
// remove from list // remove from list
err = rdb.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err() err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
err = rdb.LRem(ctx, key, 1, "---VALUE_REMOVED_BY_TINY_RDM---").Err() err = client.LRem(ctx, key, 1, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -994,7 +1104,7 @@ func (c *connectionService) SetListItem(connName string, db int, k any, index in
removed = append(removed, index) removed = append(removed, index)
} else { } else {
// replace index value // replace index value
err = rdb.LSet(ctx, key, index, value).Err() err = client.LSet(ctx, key, index, value).Err()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1012,7 +1122,7 @@ func (c *connectionService) SetListItem(connName string, db int, k any, index in
// SetSetItem add members to set or remove from set // SetSetItem add members to set or remove from set
func (c *connectionService) SetSetItem(connName string, db int, k any, remove bool, members []any) (resp types.JSResp) { func (c *connectionService) SetSetItem(connName string, db int, k any, remove bool, members []any) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1020,9 +1130,9 @@ func (c *connectionService) SetSetItem(connName string, db int, k any, remove bo
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
if remove { if remove {
_, err = rdb.SRem(ctx, key, members...).Result() _, err = client.SRem(ctx, key, members...).Result()
} else { } else {
_, err = rdb.SAdd(ctx, key, members...).Result() _, err = client.SAdd(ctx, key, members...).Result()
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1035,15 +1145,15 @@ func (c *connectionService) SetSetItem(connName string, db int, k any, remove bo
// UpdateSetItem replace member of set // UpdateSetItem replace member of set
func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) { func (c *connectionService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, _ = rdb.SRem(ctx, key, value).Result() _, _ = client.SRem(ctx, key, value).Result()
_, err = rdb.SAdd(ctx, key, newValue).Result() _, err = client.SAdd(ctx, key, newValue).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1055,7 +1165,7 @@ func (c *connectionService) UpdateSetItem(connName string, db int, k any, value,
// UpdateZSetValue update value of sorted set member // UpdateZSetValue update value of sorted set member
func (c *connectionService) UpdateZSetValue(connName string, db int, k any, value, newValue string, score float64) (resp types.JSResp) { func (c *connectionService) UpdateZSetValue(connName string, db int, k any, value, newValue string, score float64) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1066,24 +1176,24 @@ func (c *connectionService) UpdateZSetValue(connName string, db int, k any, valu
var removed []string var removed []string
if len(newValue) <= 0 { if len(newValue) <= 0 {
// blank new value, delete value // blank new value, delete value
_, err = rdb.ZRem(ctx, key, value).Result() _, err = client.ZRem(ctx, key, value).Result()
if err == nil { if err == nil {
removed = append(removed, value) removed = append(removed, value)
} }
} else if newValue == value { } else if newValue == value {
// update score only // update score only
_, err = rdb.ZAdd(ctx, key, redis.Z{ _, err = client.ZAdd(ctx, key, redis.Z{
Score: score, Score: score,
Member: value, Member: value,
}).Result() }).Result()
} else { } else {
// remove old value and add new one // remove old value and add new one
_, err = rdb.ZRem(ctx, key, value).Result() _, err = client.ZRem(ctx, key, value).Result()
if err == nil { if err == nil {
removed = append(removed, value) removed = append(removed, value)
} }
_, err = rdb.ZAdd(ctx, key, redis.Z{ _, err = client.ZAdd(ctx, key, redis.Z{
Score: score, Score: score,
Member: newValue, Member: newValue,
}).Result() }).Result()
@ -1106,7 +1216,7 @@ func (c *connectionService) UpdateZSetValue(connName string, db int, k any, valu
// AddZSetValue add item to sorted set // AddZSetValue add item to sorted set
func (c *connectionService) AddZSetValue(connName string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) { func (c *connectionService) AddZSetValue(connName string, db int, k any, action int, valueScore map[string]float64) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1123,10 +1233,10 @@ func (c *connectionService) AddZSetValue(connName string, db int, k any, action
switch action { switch action {
case 1: case 1:
// ignore duplicated fields // ignore duplicated fields
_, err = rdb.ZAddNX(ctx, key, members...).Result() _, err = client.ZAddNX(ctx, key, members...).Result()
default: default:
// overwrite duplicated fields // overwrite duplicated fields
_, err = rdb.ZAdd(ctx, key, members...).Result() _, err = client.ZAdd(ctx, key, members...).Result()
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1139,14 +1249,14 @@ func (c *connectionService) AddZSetValue(connName string, db int, k any, action
// AddStreamValue add stream field // AddStreamValue add stream field
func (c *connectionService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) { func (c *connectionService) AddStreamValue(connName string, db int, k any, ID string, fieldItems []any) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = rdb.XAdd(ctx, &redis.XAddArgs{ _, err = client.XAdd(ctx, &redis.XAddArgs{
Stream: key, Stream: key,
ID: ID, ID: ID,
Values: fieldItems, Values: fieldItems,
@ -1162,21 +1272,21 @@ func (c *connectionService) AddStreamValue(connName string, db int, k any, ID st
// RemoveStreamValues remove stream values by id // RemoveStreamValues remove stream values by id
func (c *connectionService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) { func (c *connectionService) RemoveStreamValues(connName string, db int, k any, IDs []string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
_, err = rdb.XDel(ctx, key, IDs...).Result() _, err = client.XDel(ctx, key, IDs...).Result()
resp.Success = true resp.Success = true
return return
} }
// SetKeyTTL set ttl of key // SetKeyTTL set ttl of key
func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) { func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1185,13 +1295,13 @@ func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64)
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
var expiration time.Duration var expiration time.Duration
if ttl < 0 { if ttl < 0 {
if err = rdb.Persist(ctx, key).Err(); err != nil { if err = client.Persist(ctx, key).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
} else { } else {
expiration = time.Duration(ttl) * time.Second expiration = time.Duration(ttl) * time.Second
if err = rdb.Expire(ctx, key, expiration).Err(); err != nil { if err = client.Expire(ctx, key, expiration).Err(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
@ -1203,7 +1313,7 @@ func (c *connectionService) SetKeyTTL(connName string, db int, k any, ttl int64)
// DeleteKey remove redis key // DeleteKey remove redis key
func (c *connectionService) DeleteKey(connName string, db int, k any) (resp types.JSResp) { func (c *connectionService) DeleteKey(connName string, db int, k any) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1213,29 +1323,39 @@ func (c *connectionService) DeleteKey(connName string, db int, k any) (resp type
var deletedKeys []string var deletedKeys []string
if strings.HasSuffix(key, "*") { if strings.HasSuffix(key, "*") {
// delete by prefix // delete by prefix
var cursor uint64 var mutex sync.Mutex
for { del := func(ctx context.Context, cli redis.UniversalClient) error {
var loadedKey []string iter := cli.Scan(ctx, 0, key, 10000).Iterator()
if loadedKey, cursor, err = rdb.Scan(ctx, cursor, key, 10000).Result(); err != nil { for iter.Next(ctx) {
resp.Msg = err.Error() subKey := iter.Val()
return if err = cli.Unlink(ctx, subKey).Err(); err != nil {
log.Println("unlink error", err.Error())
return err
} else { } else {
if err = rdb.Del(ctx, loadedKey...).Err(); err != nil { mutex.Lock()
resp.Msg = err.Error() deletedKeys = append(deletedKeys, subKey)
return mutex.Unlock()
} else {
deletedKeys = append(deletedKeys, loadedKey...)
} }
} }
return nil
}
// no more loadedKey if cluster, ok := client.(*redis.ClusterClient); ok {
if cursor == 0 { // cluster mode
break err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
return del(ctx, cli)
})
} else {
err = del(ctx, client)
} }
if err != nil {
resp.Msg = err.Error()
return
} }
} else { } else {
// delete key only // delete key only
_, err = rdb.Del(ctx, key).Result() _, err = client.Del(ctx, key).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
@ -1252,13 +1372,13 @@ func (c *connectionService) DeleteKey(connName string, db int, k any) (resp type
// RenameKey rename key // RenameKey rename key
func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) { func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) client, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
_, err = rdb.RenameNX(ctx, key, newKey).Result() _, err = client.RenameNX(ctx, key, newKey).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -18,6 +18,7 @@ type ConnectionConfig struct {
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"` Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
} }
type Connection struct { type Connection struct {
@ -42,19 +43,23 @@ type ConnectionDB struct {
} }
type ConnectionSSH struct { type ConnectionSSH struct {
Enable bool `json:"enable" yaml:"enable"` Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"`
LoginType string `json:"loginType" yaml:"login_type"` LoginType string `json:"loginType,omitempty" yaml:"login_type"`
Username string `json:"username" yaml:"username,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"`
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"` PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"` Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
} }
type ConnectionSentinel struct { type ConnectionSentinel struct {
Enable bool `json:"enable" yaml:"enable"` Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
Master string `json:"master" yaml:"master"` Master string `json:"master,omitempty" yaml:"master,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"`
} }
type ConnectionCluster struct {
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
}

View File

@ -76,10 +76,8 @@ func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook {
func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error { return func(ctx context.Context, cmd redis.Cmder) error {
log.Println(cmd)
t := time.Now() t := time.Now()
err := next(ctx, cmd) err := next(ctx, cmd)
if l.cmdExec != nil {
b := make([]byte, 0, 64) b := make([]byte, 0, 64)
for i, arg := range cmd.Args() { for i, arg := range cmd.Args() {
if i > 0 { if i > 0 {
@ -87,6 +85,8 @@ func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
} }
b = appendArg(b, arg) b = appendArg(b, arg)
} }
log.Println(string(b))
if l.cmdExec != nil {
l.cmdExec(string(b), time.Since(t).Milliseconds()) l.cmdExec(string(b), time.Since(t).Milliseconds())
} }
return err return err
@ -98,19 +98,22 @@ func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.Proc
t := time.Now() t := time.Now()
err := next(ctx, cmds) err := next(ctx, cmds)
cost := time.Since(t).Milliseconds() cost := time.Since(t).Milliseconds()
b := make([]byte, 0, 64)
for _, cmd := range cmds { for _, cmd := range cmds {
log.Println("pipeline: ", cmd) log.Println("pipeline: ", cmd)
if l.cmdExec != nil { if l.cmdExec != nil {
b := make([]byte, 0, 64)
for i, arg := range cmd.Args() { for i, arg := range cmd.Args() {
if i > 0 { if i > 0 {
b = append(b, ' ') b = append(b, ' ')
} }
b = appendArg(b, arg) b = appendArg(b, arg)
} }
l.cmdExec(string(b), cost) b = append(b, '\n')
} }
} }
if l.cmdExec != nil {
l.cmdExec(string(b), cost)
}
return err return err
} }
} }

View File

@ -9,10 +9,10 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"highlight.js": "^11.8.0", "highlight.js": "^11.9.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.6", "pinia": "^2.1.7",
"sass": "^1.69.0", "sass": "^1.69.3",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.5.0" "vue-i18n": "^9.5.0"
}, },
@ -1048,9 +1048,9 @@
} }
}, },
"node_modules/highlight.js": { "node_modules/highlight.js": {
"version": "11.8.0", "version": "11.9.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.8.0.tgz", "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz",
"integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }
@ -1393,9 +1393,9 @@
} }
}, },
"node_modules/pinia": { "node_modules/pinia": {
"version": "2.1.6", "version": "2.1.7",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz", "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz",
"integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==", "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
"dependencies": { "dependencies": {
"@vue/devtools-api": "^6.5.0", "@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5" "vue-demi": ">=0.14.5"
@ -1545,9 +1545,9 @@
} }
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.69.0", "version": "1.69.3",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.0.tgz", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.3.tgz",
"integrity": "sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==", "integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==",
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",
@ -2656,9 +2656,9 @@
} }
}, },
"highlight.js": { "highlight.js": {
"version": "11.8.0", "version": "11.9.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.8.0.tgz", "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz",
"integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==" "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="
}, },
"human-signals": { "human-signals": {
"version": "2.1.0", "version": "2.1.0",
@ -2926,9 +2926,9 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
}, },
"pinia": { "pinia": {
"version": "2.1.6", "version": "2.1.7",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz", "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz",
"integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==", "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
"requires": { "requires": {
"@vue/devtools-api": "^6.5.0", "@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5" "vue-demi": ">=0.14.5"
@ -3025,9 +3025,9 @@
} }
}, },
"sass": { "sass": {
"version": "1.69.0", "version": "1.69.3",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.0.tgz", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.3.tgz",
"integrity": "sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==", "integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==",
"requires": { "requires": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",

View File

@ -10,10 +10,10 @@
}, },
"dependencies": { "dependencies": {
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"highlight.js": "^11.8.0", "highlight.js": "^11.9.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.6", "pinia": "^2.1.7",
"sass": "^1.69.0", "sass": "^1.69.3",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.5.0" "vue-i18n": "^9.5.0"
}, },

View File

@ -1 +1 @@
82f42b67ae979cb1af64c05c79c5251f 3b7cabd69c1c3dad11dea0682b3c6bef

View File

@ -248,6 +248,7 @@ const onClose = () => {
:show-icon="false" :show-icon="false"
:title="isEditMode ? $t('dialogue.connection.edit_title') : $t('dialogue.connection.new_title')" :title="isEditMode ? $t('dialogue.connection.edit_title') : $t('dialogue.connection.new_title')"
preset="dialog" preset="dialog"
style="width: 600px"
transform-origin="center"> transform-origin="center">
<n-spin :show="closingConnection"> <n-spin :show="closingConnection">
<n-tabs v-model:value="tab" animated type="line"> <n-tabs v-model:value="tab" animated type="line">
@ -486,8 +487,20 @@ const onClose = () => {
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<!-- TODO: SSL tab pane --> <!-- Cluster pane -->
<!-- TODO: Cluster tab pane --> <n-tab-pane :tab="$t('dialogue.connection.cluster.title')" display-directive="show" name="cluster">
<n-form-item label-placement="left">
<n-checkbox v-model:checked="generalForm.cluster.enable" size="medium">
{{ $t('dialogue.connection.cluster.enable') }}
</n-checkbox>
</n-form-item>
<!-- <n-form-->
<!-- :model="generalForm.cluster"-->
<!-- :show-require-mark="false"-->
<!-- :disabled="!generalForm.cluster.enable"-->
<!-- label-placement="top">-->
<!-- </n-form>-->
</n-tab-pane>
</n-tabs> </n-tabs>
<!-- test result alert--> <!-- test result alert-->

View File

@ -31,6 +31,7 @@ watch(
ttlForm.keyCode = tab.keyCode ttlForm.keyCode = tab.keyCode
if (tab.ttl < 0) { if (tab.ttl < 0) {
// forever // forever
ttlForm.ttl = -1
} else { } else {
ttlForm.ttl = tab.ttl ttlForm.ttl = tab.ttl
} }

View File

@ -0,0 +1,187 @@
<script setup>
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
strokeWidth: {
type: [Number, String],
default: 3,
},
fillColor: {
type: String,
default: '#dc423c',
},
})
</script>
<template>
<svg v-if="props.modelValue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="34"
width="8"
height="8"
:fill="props.fillColor"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="8"
y="6"
width="32"
height="12"
:fill="props.fillColor"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M24 34V18"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M8 34V26H40V34"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="36"
y="34"
width="8"
height="8"
:fill="props.fillColor"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="20"
y="34"
width="8"
height="8"
:fill="props.fillColor"
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M14 12H16"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<svg v-else viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 24L43 24"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M28 4H20C18.8954 4 18 4.89543 18 6V14C18 15.1046 18.8954 16 20 16H28C29.1046 16 30 15.1046 30 14V6C30 4.89543 29.1046 4 28 4Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M16 32H8C6.89543 32 6 32.8954 6 34V42C6 43.1046 6.89543 44 8 44H16C17.1046 44 18 43.1046 18 42V34C18 32.8954 17.1046 32 16 32Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M40 32H32C30.8954 32 30 32.8954 30 34V42C30 43.1046 30.8954 44 32 44H40C41.1046 44 42 43.1046 42 42V34C42 32.8954 41.1046 32 40 32Z"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M24 24V16"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M36 32V24"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M12 32V24"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<svg v-else viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="34"
width="8"
height="8"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="8"
y="6"
width="32"
height="12"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M24 34V18"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M8 34V26H40V34"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="36"
y="34"
width="8"
height="8"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<rect
x="20"
y="34"
width="8"
height="8"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M14 12H16"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -5,7 +5,7 @@ import { NIcon, NSpace, NTag } from 'naive-ui'
import Key from '@/components/icons/Key.vue' import Key from '@/components/icons/Key.vue'
import Binary from '@/components/icons/Binary.vue' import Binary from '@/components/icons/Binary.vue'
import ToggleDb from '@/components/icons/ToggleDb.vue' import ToggleDb from '@/components/icons/ToggleDb.vue'
import { find, get, includes, indexOf, isEmpty, pull, remove, size } from 'lodash' import { find, get, includes, indexOf, isEmpty, remove, size } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import CopyLink from '@/components/icons/CopyLink.vue' import CopyLink from '@/components/icons/CopyLink.vue'
@ -16,7 +16,6 @@ import Connect from '@/components/icons/Connect.vue'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js' import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import ToggleServer from '@/components/icons/ToggleServer.vue'
import Unlink from '@/components/icons/Unlink.vue' import Unlink from '@/components/icons/Unlink.vue'
import Filter from '@/components/icons/Filter.vue' import Filter from '@/components/icons/Filter.vue'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
@ -348,14 +347,15 @@ const onUpdateSelectedKeys = (keys, options) => {
const renderPrefix = ({ option }) => { const renderPrefix = ({ option }) => {
switch (option.type) { switch (option.type) {
case ConnectionType.Server: // case ConnectionType.Server:
return h( // const icon = option.cluster === true ? ToggleCluster : ToggleServer
NIcon, // return h(
{ size: 20 }, // NIcon,
{ // { size: 20 },
default: () => h(ToggleServer, { modelValue: false }), // {
}, // default: () => h(icon, { modelValue: false }),
) // },
// )
case ConnectionType.RedisDB: case ConnectionType.RedisDB:
return h( return h(
NIcon, NIcon,

View File

@ -6,6 +6,7 @@ import { NIcon, NSpace, NText, useThemeVars } from 'naive-ui'
import { ConnectionType } from '@/consts/connection_type.js' import { ConnectionType } from '@/consts/connection_type.js'
import ToggleFolder from '@/components/icons/ToggleFolder.vue' import ToggleFolder from '@/components/icons/ToggleFolder.vue'
import ToggleServer from '@/components/icons/ToggleServer.vue' import ToggleServer from '@/components/icons/ToggleServer.vue'
import ToggleCluster from '@/components/icons/ToggleCluster.vue'
import { debounce, get, includes, indexOf, isEmpty, split } from 'lodash' import { debounce, get, includes, indexOf, isEmpty, split } from 'lodash'
import Config from '@/components/icons/Config.vue' import Config from '@/components/icons/Config.vue'
import Delete from '@/components/icons/Delete.vue' import Delete from '@/components/icons/Delete.vue'
@ -193,12 +194,13 @@ const renderPrefix = ({ option }) => {
case ConnectionType.Server: case ConnectionType.Server:
const connected = connectionStore.isConnected(option.name) const connected = connectionStore.isConnected(option.name)
const color = getServerMarkColor(option.name) const color = getServerMarkColor(option.name)
const icon = option.cluster === true ? ToggleCluster : ToggleServer
return h( return h(
NIcon, NIcon,
{ size: 20, color: !!!connected ? color : undefined }, { size: 20, color: !!!connected ? color : undefined },
{ {
default: () => default: () =>
h(ToggleServer, { h(icon, {
modelValue: !!connected, modelValue: !!connected,
fillColor: `rgba(220,66,60,${iconTransparency})`, fillColor: `rgba(220,66,60,${iconTransparency})`,
}), }),

View File

@ -93,7 +93,7 @@
}, },
"ribbon": { "ribbon": {
"server": "Server", "server": "Server",
"browser": "Browser", "browser": "Data Browser",
"log": "Log" "log": "Log"
}, },
"dialogue": { "dialogue": {
@ -147,6 +147,16 @@
"dbfilter_input_tip": "Press Enter to confirm", "dbfilter_input_tip": "Press Enter to confirm",
"mark_color": "Mark Color" "mark_color": "Mark Color"
}, },
"ssl": {
"title": "SSL/TLS",
"enable": "Enable SSL/TLS",
"key_file": "Public Key",
"cert_file": "Private Key",
"ca_file": "Authority",
"key_file_tip": "Public Key File in PEM format",
"cert_file_tip":"Private Key File in PEM format",
"ca_file_tip": "Certificate Authority File in PEM format"
},
"ssh": { "ssh": {
"title": "SSH Tunnel", "title": "SSH Tunnel",
"enable": "Enable SSH Tuntel", "enable": "Enable SSH Tuntel",
@ -169,6 +179,11 @@
"username": "Username for Master Node", "username": "Username for Master Node",
"pwd_tip": "(Optional) Authentication username for master node", "pwd_tip": "(Optional) Authentication username for master node",
"usr_tip": "(Optional) Authentication password for master node (Redis > 6.0)" "usr_tip": "(Optional) Authentication password for master node (Redis > 6.0)"
},
"cluster": {
"title": "Cluster",
"enable": "Serve as Cluster Node",
"readonly": "Enables read-only commands on slave nodes"
} }
}, },
"group": { "group": {

View File

@ -93,7 +93,7 @@
}, },
"ribbon": { "ribbon": {
"server": "服务器", "server": "服务器",
"browser": "浏览", "browser": "数据浏览",
"log": "日志" "log": "日志"
}, },
"dialogue": { "dialogue": {
@ -147,6 +147,16 @@
"dbfilter_input_tip": "按回车确认", "dbfilter_input_tip": "按回车确认",
"mark_color": "标记颜色" "mark_color": "标记颜色"
}, },
"ssl": {
"title": "SSL/TLS",
"enable": "启用SSL",
"key_file": "公钥文件",
"cert_file": "私钥文件",
"ca_file": "授权文件",
"key_file_tip": "PEM格式公钥文件",
"cert_file_tip":"PEM格式私钥文件",
"ca_file_tip": "PEM格式授权文件"
},
"ssh": { "ssh": {
"enable": "启用SSH隧道", "enable": "启用SSH隧道",
"title": "SSH隧道", "title": "SSH隧道",
@ -169,6 +179,10 @@
"username": "主节点用户名", "username": "主节点用户名",
"pwd_tip": "(可选)主节点服务授权用户名", "pwd_tip": "(可选)主节点服务授权用户名",
"usr_tip": "(可选)主节点服务授权密码 (Redis > 6.0)" "usr_tip": "(可选)主节点服务授权密码 (Redis > 6.0)"
},
"cluster": {
"title": "集群模式",
"enable": "当前为集群节点"
} }
}, },
"group": { "group": {

View File

@ -59,6 +59,7 @@ const useConnectionStore = defineStore('connections', {
* @property {string} label display label * @property {string} label display label
* @property {string} name database name * @property {string} name database name
* @property {number} type * @property {number} type
* @property {boolean} cluster is cluster node
* @property {ConnectionItem[]} children * @property {ConnectionItem[]} children
*/ */
@ -146,6 +147,7 @@ const useConnectionStore = defineStore('connections', {
label: conn.name, label: conn.name,
name: conn.name, name: conn.name,
type: ConnectionType.Server, type: ConnectionType.Server,
cluster: get(conn, 'cluster.enable', false),
// isLeaf: false, // isLeaf: false,
}) })
profiles[conn.name] = { profiles[conn.name] = {
@ -165,6 +167,7 @@ const useConnectionStore = defineStore('connections', {
label: item.name, label: item.name,
name: item.name, name: item.name,
type: ConnectionType.Server, type: ConnectionType.Server,
cluster: get(item, 'cluster.enable', false),
// isLeaf: false, // isLeaf: false,
}) })
profiles[item.name] = { profiles[item.name] = {
@ -244,6 +247,9 @@ const useConnectionStore = defineStore('connections', {
username: '', username: '',
password: '', password: '',
}, },
cluster: {
enable: false,
},
} }
}, },
@ -624,7 +630,7 @@ const useConnectionStore = defineStore('connections', {
} }
// its danger to delete "non-exists" key, just remove from tree view // its danger to delete "non-exists" key, just remove from tree view
await this.deleteKey(server, db, key, true) await this.deleteKey(server, db, key, true)
// TODO: show key not found page? // TODO: show key not found page or check exists on server first?
} }
} }