Compare commits
No commits in common. "main" and "v1.5.0" have entirely different histories.
@ -1,9 +1,5 @@
|
|||||||
# CHANGELOG - redis
|
# CHANGELOG - redis
|
||||||
|
|
||||||
## v1.5.1 (2026-06-08)
|
|
||||||
- **新增**: `SetConfig(name, setting string)` 方法,支持动态配置 Redis 连接(不依赖配置文件),方便通过别名获取连接。
|
|
||||||
- **优化**: 重构配置加载逻辑,确保动态配置与配置文件配置的共存与优先级。
|
|
||||||
|
|
||||||
## v1.3.3 (2026-05-30)
|
## v1.3.3 (2026-05-30)
|
||||||
- **新增**: 注册到 `jsmod`。
|
- **新增**: 注册到 `jsmod`。
|
||||||
- **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode` 下,仅允许读取操作(GET/EXISTS/ZRANGE等),所有写操作(SET/DEL/EXPIRE/DO等)将被拦截并返回错误。
|
- **安全性**: 引入基于 Context 的细粒度权限控制。在 `SafeMode` 下,仅允许读取操作(GET/EXISTS/ZRANGE等),所有写操作(SET/DEL/EXPIRE/DO等)将被拦截并返回错误。
|
||||||
|
|||||||
@ -11,8 +11,7 @@
|
|||||||
## API 指南
|
## API 指南
|
||||||
|
|
||||||
### 基础连接
|
### 基础连接
|
||||||
- `SetConfig(name, setting string)`: 动态设置 Redis 配置(不依赖配置文件),可通过别名获取连接。
|
- `GetRedis(name string, logger *log.Logger) *Redis`: 获取或创建一个 Redis 实例(支持 DSN 或配置文件名)。
|
||||||
- `GetRedis(name string, logger *log.Logger) *Redis`: 获取或创建一个 Redis 实例(支持 DSN、别名或配置文件名)。
|
|
||||||
- `NewRedis(conf *Config, logger *log.Logger) *Redis`: 使用指定配置创建 Redis 实例。
|
- `NewRedis(conf *Config, logger *log.Logger) *Redis`: 使用指定配置创建 Redis 实例。
|
||||||
|
|
||||||
### 核心操作
|
### 核心操作
|
||||||
|
|||||||
14
config.go
14
config.go
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/config"
|
|
||||||
"apigo.cc/go/crypto"
|
"apigo.cc/go/crypto"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/safe"
|
"apigo.cc/go/safe"
|
||||||
@ -31,19 +30,6 @@ type Config struct {
|
|||||||
|
|
||||||
var redisConfigs = make(map[string]*Config)
|
var redisConfigs = make(map[string]*Config)
|
||||||
var redisConfigsLock = sync.RWMutex{}
|
var redisConfigsLock = sync.RWMutex{}
|
||||||
var redisConfigsOnce sync.Once
|
|
||||||
|
|
||||||
func SetConfig(name, setting string) {
|
|
||||||
redisConfigsOnce.Do(func() {
|
|
||||||
_ = config.Load(&redisConfigs, "redis")
|
|
||||||
})
|
|
||||||
|
|
||||||
conf := new(Config)
|
|
||||||
conf.ConfigureBy(setting)
|
|
||||||
redisConfigsLock.Lock()
|
|
||||||
redisConfigs[name] = conf
|
|
||||||
redisConfigsLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
var confAES *crypto.Symmetric
|
var confAES *crypto.Symmetric
|
||||||
|
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -18,7 +18,7 @@ require (
|
|||||||
apigo.cc/go/file v1.5.0 // indirect
|
apigo.cc/go/file v1.5.0 // indirect
|
||||||
apigo.cc/go/rand v1.5.0 // indirect
|
apigo.cc/go/rand v1.5.0 // indirect
|
||||||
apigo.cc/go/shell v1.5.0 // indirect
|
apigo.cc/go/shell v1.5.0 // indirect
|
||||||
golang.org/x/crypto v0.52.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -28,8 +28,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
116
js_export.go
116
js_export.go
@ -3,7 +3,6 @@ package redis
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"apigo.cc/go/id"
|
"apigo.cc/go/id"
|
||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
@ -11,60 +10,13 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
jsmod.Register("redis", map[string]any{
|
jsmod.Register("redis", map[string]any{
|
||||||
// 入口:支持别名获取,不传则默认 "default"
|
"get": func(ctx context.Context, name string) (*jsRedis, error) {
|
||||||
"Get": func(ctx context.Context, name *string) (*jsRedis, error) {
|
rd := GetRedis(name, nil)
|
||||||
target := "default"
|
|
||||||
if name != nil {
|
|
||||||
target = *name
|
|
||||||
}
|
|
||||||
rd := GetRedis(target, nil)
|
|
||||||
if rd.Error != nil {
|
if rd.Error != nil {
|
||||||
return nil, rd.Error
|
return nil, rd.Error
|
||||||
}
|
}
|
||||||
return &jsRedis{rd: rd, ctx: ctx}, nil
|
return &jsRedis{rd: rd, ctx: ctx}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
// 默认快捷调用 (面向 "default" 实例)
|
|
||||||
"Do": func(ctx context.Context, cmd string, args ...any) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
if jr.rd.Error != nil {
|
|
||||||
return nil, jr.rd.Error
|
|
||||||
}
|
|
||||||
res := jr.Do(cmd, args...)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
|
|
||||||
// 常用命令平铺 (面向 "default" 实例)
|
|
||||||
"SET": func(ctx context.Context, key string, val any) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("SET", key, val)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
"GET": func(ctx context.Context, key string) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("GET", key)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
"DEL": func(ctx context.Context, key string) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("DEL", key)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
"EXISTS": func(ctx context.Context, key string) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("EXISTS", key)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
"EXPIRE": func(ctx context.Context, key string, seconds int) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("EXPIRE", key, seconds)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
"PUBLISH": func(ctx context.Context, channel, data string) (*Result, error) {
|
|
||||||
jr := &jsRedis{rd: GetRedis("default", nil), ctx: ctx}
|
|
||||||
res := jr.Do("PUBLISH", channel, data)
|
|
||||||
return res, res.Error
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,60 +28,40 @@ type jsRedis struct {
|
|||||||
|
|
||||||
var errSafeMode = errors.New("redis operation is restricted in safe mode")
|
var errSafeMode = errors.New("redis operation is restricted in safe mode")
|
||||||
|
|
||||||
// 核心写操作指令集
|
func (jr *jsRedis) checkSafe() error {
|
||||||
var writeCommands = map[string]bool{
|
|
||||||
"SET": true, "SETEX": true, "SETNX": true, "MSET": true, "MSETNX": true,
|
|
||||||
"DEL": true, "EXPIRE": true, "EXPIREAT": true, "PEXPIRE": true, "PEXPIREAT": true,
|
|
||||||
"HSET": true, "HSETNX": true, "HDEL": true, "HMSET": true,
|
|
||||||
"LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true, "LREM": true, "LTRIM": true,
|
|
||||||
"SADD": true, "SREM": true, "SPOP": true, "SMOVE": true,
|
|
||||||
"ZADD": true, "ZREM": true, "ZREMRANGEBYRANK": true, "ZREMRANGEBYSCORE": true,
|
|
||||||
"PUBLISH": true, "FLUSHDB": true, "FLUSHALL": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jr *jsRedis) checkSafe(cmd string) error {
|
|
||||||
if jsmod.IsSafeMode(jr.ctx) {
|
if jsmod.IsSafeMode(jr.ctx) {
|
||||||
cmd = strings.ToUpper(cmd)
|
|
||||||
if writeCommands[cmd] || !strings.Contains(" GET EXISTS ZRANGE HGET HGETALL SMEMBERS SISMEMBER LINDEX LLEN ", " "+cmd+" ") {
|
|
||||||
// 严格模式:不在白名单内的或在黑名单内的都禁止
|
|
||||||
return errSafeMode
|
return errSafeMode
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do executes any redis command. In SafeMode, it only allows read-only commands.
|
||||||
|
// Note: Since we don't have a reliable way to categorize all redis commands as read-only,
|
||||||
|
// and 'DO' is used for everything, we strictly block 'DO' in SafeMode if it's not a known read-only command.
|
||||||
|
// For simplicity and maximum safety as requested, we block 'DO' entirely in SafeMode.
|
||||||
func (jr *jsRedis) Do(cmd string, args ...any) *Result {
|
func (jr *jsRedis) Do(cmd string, args ...any) *Result {
|
||||||
if err := jr.checkSafe(cmd); err != nil {
|
if jr.checkSafe() != nil {
|
||||||
return &Result{Error: err}
|
return &Result{Error: errSafeMode}
|
||||||
}
|
}
|
||||||
return jr.rd.Do(cmd, args...)
|
return jr.rd.Do(cmd, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 实例方法 PascalCase 对齐
|
// ID Generation Helpers
|
||||||
func (jr *jsRedis) SET(key string, val any) *Result { return jr.Do("SET", key, val) }
|
func (jr *jsRedis) getIDMaker() *id.IDMaker {
|
||||||
func (jr *jsRedis) GET(key string) *Result { return jr.Do("GET", key) }
|
|
||||||
func (jr *jsRedis) DEL(key string) *Result { return jr.Do("DEL", key) }
|
|
||||||
func (jr *jsRedis) EXISTS(key string) *Result { return jr.Do("EXISTS", key) }
|
|
||||||
func (jr *jsRedis) EXPIRE(key string, s int) *Result { return jr.Do("EXPIRE", key, s) }
|
|
||||||
func (jr *jsRedis) HSET(key, field string, v any) *Result { return jr.Do("HSET", key, field, v) }
|
|
||||||
func (jr *jsRedis) HGET(key, field string) *Result { return jr.Do("HGET", key, field) }
|
|
||||||
func (jr *jsRedis) PUBLISH(ch, data string) *Result { return jr.Do("PUBLISH", ch, data) }
|
|
||||||
|
|
||||||
// ID Generation
|
|
||||||
func (jr *jsRedis) MakeID(size int, forDB *string) string {
|
|
||||||
if jr.idMaker == nil {
|
if jr.idMaker == nil {
|
||||||
jr.idMaker = NewIDMaker(jr.rd)
|
jr.idMaker = NewIDMaker(jr.rd)
|
||||||
}
|
}
|
||||||
dbType := ""
|
return jr.idMaker
|
||||||
if forDB != nil {
|
}
|
||||||
dbType = strings.ToLower(*forDB)
|
|
||||||
}
|
func (jr *jsRedis) GetID(size int) string {
|
||||||
switch dbType {
|
return jr.getIDMaker().Get(size)
|
||||||
case "mysql":
|
}
|
||||||
return jr.idMaker.GetForMysql(size)
|
|
||||||
case "postgres", "pg", "pgsql":
|
func (jr *jsRedis) GetForMysql(size int) string {
|
||||||
return jr.idMaker.GetForPostgreSQL(size)
|
return jr.getIDMaker().GetForMysql(size)
|
||||||
default:
|
}
|
||||||
return jr.idMaker.Get(size)
|
|
||||||
}
|
func (jr *jsRedis) GetForPostgreSQL(size int) string {
|
||||||
|
return jr.getIDMaker().GetForPostgreSQL(size)
|
||||||
}
|
}
|
||||||
|
|||||||
10
redis.go
10
redis.go
@ -57,9 +57,13 @@ func GetRedis(name string, logger *log.Logger) *Redis {
|
|||||||
return oldConn.CopyByLogger(logger)
|
return oldConn.CopyByLogger(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
redisConfigsOnce.Do(func() {
|
redisConfigsLock.RLock()
|
||||||
|
configsLen := len(redisConfigs)
|
||||||
|
redisConfigsLock.RUnlock()
|
||||||
|
|
||||||
|
if configsLen == 0 {
|
||||||
_ = config.Load(&redisConfigs, "redis")
|
_ = config.Load(&redisConfigs, "redis")
|
||||||
})
|
}
|
||||||
|
|
||||||
fullName := name
|
fullName := name
|
||||||
|
|
||||||
@ -72,7 +76,7 @@ func GetRedis(name string, logger *log.Logger) *Redis {
|
|||||||
conf = parseByName(name)
|
conf = parseByName(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pwd, err := confAES.Decrypt(cast.As(encoding.UnURLBase64(conf.Password))); err == nil {
|
if pwd, err := confAES.Decrypt(cast.As(encoding.UnUrlBase64FromString(conf.Password))); err == nil {
|
||||||
conf.pwd = pwd
|
conf.pwd = pwd
|
||||||
} else {
|
} else {
|
||||||
conf.pwd = safe.NewSafeBuf([]byte(conf.Password))
|
conf.pwd = safe.NewSafeBuf([]byte(conf.Password))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user