From 434de39d3a97144cf6361a348c5f1f383b6b2d47 Mon Sep 17 00:00:00 2001 From: langhuihui <178529795@qq.com> Date: Tue, 12 Dec 2023 17:02:18 +0800 Subject: [PATCH] feat: modify file only have modified config --- config/config.go | 256 ++++++++---------------------------------- config/config_test.go | 32 ++++++ config/formily.go | 214 ++++++++++++++++++++++++++++++++++- config/regexp.go | 19 ++-- config/types.go | 18 +-- http.go | 30 ++++- main.go | 5 +- plugin.go | 22 +++- 8 files changed, 361 insertions(+), 235 deletions(-) create mode 100644 config/config_test.go diff --git a/config/config.go b/config/config.go index 225cae0..8a89da8 100644 --- a/config/config.go +++ b/config/config.go @@ -125,7 +125,7 @@ func (config *Config) Parse(s any, prefix ...string) { config.Ptr.Set(envv) } } - if t.Kind() == reflect.Struct { + if t.Kind() == reflect.Struct && t != regexpType { for i, j := 0, t.NumField(); i < j; i++ { ft, fv := t.Field(i), v.Field(i) if !ft.IsExported() { @@ -231,16 +231,61 @@ func (config *Config) ParseModifyFile(conf map[string]any) { if config.Has(k) { if prop := config.Get(k); prop.props != nil { if v != nil { - prop.ParseModifyFile(v.(map[string]any)) + vmap := v.(map[string]any) + prop.ParseModifyFile(vmap) + if len(vmap) == 0 { + delete(conf, k) + } } } else { mv := prop.assign(k, v) - prop.Modify = mv.Interface() - prop.Value = mv.Interface() + v = mv.Interface() + vwm := prop.valueWithoutModify() + if equal(vwm, v) { + delete(conf, k) + if prop.Modify != nil { + prop.Modify = nil + prop.Value = vwm + prop.Ptr.Set(reflect.ValueOf(vwm)) + } + continue + } + prop.Modify = v + prop.Value = v prop.Ptr.Set(mv) } } } + if len(conf) == 0 { + config.Modify = nil + } +} + +func (config *Config) valueWithoutModify() any { + if config.Env != nil { + return config.Env + } + if config.File != nil { + return config.File + } + if config.Global != nil { + return config.Global.Value + } + return config.Default +} + +func equal(vwm, v any) bool { + ft := reflect.TypeOf(vwm) + switch ft { + case regexpType: + return vwm.(Regexp).String() == v.(Regexp).String() + default: + switch ft.Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + return reflect.DeepEqual(vwm, v) + } + return vwm == v + } } func (config *Config) GetMap() map[string]any { @@ -260,209 +305,6 @@ func (config *Config) GetMap() map[string]any { return nil } -func (config *Config) schema(index int) (r any) { - defer func() { - err := recover() - if err != nil { - log.Error(err) - } - }() - if config.props != nil { - r := Card{ - Type: "void", - Component: "Card", - Properties: make(map[string]any), - Index: index, - } - r.ComponentProps = map[string]any{ - "title": config.name, - } - for i, v := range config.props { - if strings.HasPrefix(v.tag.Get("desc"), "废弃") { - continue - } - r.Properties[v.name] = v.schema(i) - } - return r - } else { - p := Property{ - Title: config.name, - Default: config.Value, - DecoratorProps: map[string]any{ - "tooltip": config.tag.Get("desc"), - }, - ComponentProps: map[string]any{}, - Decorator: "FormItem", - Index: index, - } - if config.Modify != nil { - p.Description = "已动态修改" - } else if config.Env != nil { - p.Description = "使用环境变量中的值" - } else if config.File != nil { - p.Description = "使用配置文件中的值" - } else if config.Global != nil { - p.Description = "已使用全局配置中的值" - } - p.Enum = config.Enum - switch config.Ptr.Type() { - case regexpType: - p.Type = "string" - p.Component = "Input" - p.DecoratorProps["addonAfter"] = "正则表达式" - str := config.Value.(Regexp).String() - p.ComponentProps = map[string]any{ - "placeholder": str, - } - p.Default = str - case durationType: - p.Type = "string" - p.Component = "Input" - str := config.Value.(time.Duration).String() - p.ComponentProps = map[string]any{ - "placeholder": str, - } - p.Default = str - p.DecoratorProps["addonAfter"] = "时间,单位:s,m,h,d,例如:100ms, 10s, 4m, 1h" - default: - switch config.Ptr.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: - p.Type = "number" - p.Component = "InputNumber" - p.ComponentProps = map[string]any{ - "placeholder": config.Value, - } - case reflect.Bool: - p.Type = "boolean" - p.Component = "Switch" - case reflect.String: - p.Type = "string" - p.Component = "Input" - p.ComponentProps = map[string]any{ - "placeholder": config.Value, - } - case reflect.Slice: - p.Type = "array" - p.Component = "Input" - p.ComponentProps = map[string]any{ - "placeholder": config.Value, - } - p.DecoratorProps["addonAfter"] = "数组,每个元素用逗号分隔" - case reflect.Map: - var children []struct { - Key string `json:"mkey"` - Value any `json:"mvalue"` - } - p := Property{ - Type: "array", - Component: "ArrayTable", - Decorator: "FormItem", - Properties: map[string]any{ - "addition": map[string]string{ - "type": "void", - "title": "添加", - "x-component": "ArrayTable.Addition", - }, - }, - Index: index, - Title: config.name, - Items: &Object{ - Type: "object", - Properties: map[string]any{ - "c1": Card{ - Type: "void", - Component: "ArrayTable.Column", - ComponentProps: map[string]any{ - "title": config.tag.Get("key"), - "width": 300, - }, - Properties: map[string]any{ - "mkey": Property{ - Type: "string", - Decorator: "FormItem", - Component: "Input", - }, - }, - Index: 0, - }, - "c2": Card{ - Type: "void", - Component: "ArrayTable.Column", - ComponentProps: map[string]any{ - "title": config.tag.Get("value"), - }, - Properties: map[string]any{ - "mvalue": Property{ - Type: "string", - Decorator: "FormItem", - Component: "Input", - }, - }, - Index: 1, - }, - "operator": Card{ - Type: "void", - Component: "ArrayTable.Column", - ComponentProps: map[string]any{ - "title": "操作", - }, - Properties: map[string]any{ - "remove": Card{ - Type: "void", - Component: "ArrayTable.Remove", - }, - }, - Index: 2, - }, - }, - }, - } - iter := config.Ptr.MapRange() - for iter.Next() { - children = append(children, struct { - Key string `json:"mkey"` - Value any `json:"mvalue"` - }{ - Key: iter.Key().String(), - Value: iter.Value().Interface(), - }) - } - p.Default = children - return p - default: - - } - } - if len(p.Enum) > 0 { - p.Component = "Radio.Group" - } - return p - } -} - -func (config *Config) GetFormily() (r Object) { - var fromItems = make(map[string]any) - r.Type = "object" - r.Properties = map[string]any{ - "layout": Card{ - Type: "void", - Component: "FormLayout", - ComponentProps: map[string]any{ - "labelCol": 4, - "wrapperCol": 20, - }, - Properties: fromItems, - }, - } - for i, v := range config.props { - if strings.HasPrefix(v.tag.Get("desc"), "废弃") { - continue - } - fromItems[v.name] = v.schema(i) - } - return -} - var regexPureNumber = regexp.MustCompile(`^\d+$`) func (config *Config) assign(k string, v any) (target reflect.Value) { diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..3838373 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,32 @@ +package config + +import ( + "testing" +) +// TestModify 测试动态修改配置文件,比较值是否修改,修改后是否有Modify属性 +func TestModify(t *testing.T) { + t.Run(t.Name(), func(t *testing.T) { + var defaultValue struct{ + Subscribe + } + defaultValue.SubAudio = false + var conf Config + conf.Parse(&defaultValue) + conf.ParseModifyFile(map[string]any{ + "subscribe": map[string]any{ + "subaudio": false, + }, + }) + if conf.Modify != nil { + t.Fail() + } + conf.ParseModifyFile(map[string]any{ + "subscribe": map[string]any{ + "subaudio": true, + }, + }) + if conf.Modify == nil { + t.Fail() + } + }) +} diff --git a/config/formily.go b/config/formily.go index e5f6447..7a78243 100644 --- a/config/formily.go +++ b/config/formily.go @@ -1,12 +1,21 @@ package config +import ( + "fmt" + "reflect" + "strings" + "time" + + "m7s.live/engine/v4/log" +) + type Property struct { Type string `json:"type"` Title string `json:"title"` Description string `json:"description"` Enum []struct { Label string `json:"label"` - Value any `json:"value"` + Value any `json:"value"` } `json:"enum,omitempty"` Items *Object `json:"items,omitempty"` Properties map[string]any `json:"properties,omitempty"` @@ -30,3 +39,206 @@ type Object struct { Type string `json:"type"` Properties map[string]any `json:"properties"` } + +func (config *Config) schema(index int) (r any) { + defer func() { + err := recover() + if err != nil { + log.Error(err) + } + }() + if config.props != nil { + r := Card{ + Type: "void", + Component: "Card", + Properties: make(map[string]any), + Index: index, + } + r.ComponentProps = map[string]any{ + "title": config.name, + } + for i, v := range config.props { + if strings.HasPrefix(v.tag.Get("desc"), "废弃") { + continue + } + r.Properties[v.name] = v.schema(i) + } + return r + } else { + p := Property{ + Title: config.name, + Default: config.Value, + DecoratorProps: map[string]any{ + "tooltip": config.tag.Get("desc"), + }, + ComponentProps: map[string]any{}, + Decorator: "FormItem", + Index: index, + } + if config.Modify != nil { + p.Description = "已动态修改" + } else if config.Env != nil { + p.Description = "使用环境变量中的值" + } else if config.File != nil { + p.Description = "使用配置文件中的值" + } else if config.Global != nil { + p.Description = "已使用全局配置中的值" + } + p.Enum = config.Enum + switch config.Ptr.Type() { + case regexpType: + p.Type = "string" + p.Component = "Input" + p.DecoratorProps["addonAfter"] = "正则表达式" + str := config.Value.(Regexp).String() + p.ComponentProps = map[string]any{ + "placeholder": str, + } + p.Default = str + case durationType: + p.Type = "string" + p.Component = "Input" + str := config.Value.(time.Duration).String() + p.ComponentProps = map[string]any{ + "placeholder": str, + } + p.Default = str + p.DecoratorProps["addonAfter"] = "时间,单位:s,m,h,d,例如:100ms, 10s, 4m, 1h" + default: + switch config.Ptr.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: + p.Type = "number" + p.Component = "InputNumber" + p.ComponentProps = map[string]any{ + "placeholder": fmt.Sprintf("%v", config.Value), + } + case reflect.Bool: + p.Type = "boolean" + p.Component = "Switch" + case reflect.String: + p.Type = "string" + p.Component = "Input" + p.ComponentProps = map[string]any{ + "placeholder": config.Value, + } + case reflect.Slice: + p.Type = "array" + p.Component = "Input" + p.ComponentProps = map[string]any{ + "placeholder": config.Value, + } + p.DecoratorProps["addonAfter"] = "数组,每个元素用逗号分隔" + case reflect.Map: + var children []struct { + Key string `json:"mkey"` + Value any `json:"mvalue"` + } + p := Property{ + Type: "array", + Component: "ArrayTable", + Decorator: "FormItem", + Properties: map[string]any{ + "addition": map[string]string{ + "type": "void", + "title": "添加", + "x-component": "ArrayTable.Addition", + }, + }, + Index: index, + Title: config.name, + Items: &Object{ + Type: "object", + Properties: map[string]any{ + "c1": Card{ + Type: "void", + Component: "ArrayTable.Column", + ComponentProps: map[string]any{ + "title": config.tag.Get("key"), + "width": 300, + }, + Properties: map[string]any{ + "mkey": Property{ + Type: "string", + Decorator: "FormItem", + Component: "Input", + }, + }, + Index: 0, + }, + "c2": Card{ + Type: "void", + Component: "ArrayTable.Column", + ComponentProps: map[string]any{ + "title": config.tag.Get("value"), + }, + Properties: map[string]any{ + "mvalue": Property{ + Type: "string", + Decorator: "FormItem", + Component: "Input", + }, + }, + Index: 1, + }, + "operator": Card{ + Type: "void", + Component: "ArrayTable.Column", + ComponentProps: map[string]any{ + "title": "操作", + }, + Properties: map[string]any{ + "remove": Card{ + Type: "void", + Component: "ArrayTable.Remove", + }, + }, + Index: 2, + }, + }, + }, + } + iter := config.Ptr.MapRange() + for iter.Next() { + children = append(children, struct { + Key string `json:"mkey"` + Value any `json:"mvalue"` + }{ + Key: iter.Key().String(), + Value: iter.Value().Interface(), + }) + } + p.Default = children + return p + default: + + } + } + if len(p.Enum) > 0 { + p.Component = "Radio.Group" + } + return p + } +} + +func (config *Config) GetFormily() (r Object) { + var fromItems = make(map[string]any) + r.Type = "object" + r.Properties = map[string]any{ + "layout": Card{ + Type: "void", + Component: "FormLayout", + ComponentProps: map[string]any{ + "labelCol": 4, + "wrapperCol": 20, + }, + Properties: fromItems, + }, + } + for i, v := range config.props { + if strings.HasPrefix(v.tag.Get("desc"), "废弃") { + continue + } + fromItems[v.name] = v.schema(i) + } + return +} diff --git a/config/regexp.go b/config/regexp.go index ef15f86..e8d1bd8 100644 --- a/config/regexp.go +++ b/config/regexp.go @@ -10,22 +10,27 @@ type Regexp struct { *regexp.Regexp } +func (r *Regexp) Valid() bool { + return r.Regexp != nil +} + +func (r Regexp) String() string { + if !r.Valid() { + return "" + } + return r.Regexp.String() +} + func (r *Regexp) UnmarshalYAML(node *yaml.Node) error { r.Regexp = regexp.MustCompile(node.Value) return nil } func (r *Regexp) MarshalYAML() (interface{}, error) { - if r.Regexp == nil { - return "", nil - } return r.String(), nil } func (r *Regexp) MarshalJSON() ([]byte, error) { - if r.Regexp == nil { - return []byte(`""`), nil - } return []byte(`"` + r.String() + `"`), nil } @@ -41,4 +46,4 @@ func (r *Regexp) UnmarshalJSON(b []byte) error { } r.Regexp = regexp.MustCompile(string(b)) return nil -} \ No newline at end of file +} diff --git a/config/types.go b/config/types.go index 1808ece..f821aea 100755 --- a/config/types.go +++ b/config/types.go @@ -77,11 +77,11 @@ func (c *Subscribe) GetSubscribeConfig() *Subscribe { } type Pull struct { - RePull int // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉,高于0 的数代表最大重拉次数 - EnableRegexp bool // 是否启用正则表达式 - PullOnStart map[string]string // 启动时拉流的列表 - PullOnSub map[string]string // 订阅时自动拉流的列表 - Proxy string // 代理地址 + RePull int `desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉,高于0 的数代表最大重拉次数 + EnableRegexp bool `desc:"是否启用正则表达式"` // 是否启用正则表达式 + PullOnStart map[string]string `desc:"启动时拉流的列表"` // 启动时拉流的列表 + PullOnSub map[string]string `desc:"订阅时自动拉流的列表"` // 订阅时自动拉流的列表 + Proxy string `desc:"代理地址"` // 代理地址 } func (p *Pull) GetPullConfig() *Pull { @@ -145,10 +145,10 @@ func (p *Pull) AddPullOnSub(streamPath string, url string) { } type Push struct { - EnableRegexp bool // 是否启用正则表达式 - RePush int // 断开后自动重推,0 表示不自动重推,-1 表示无限重推,高于0 的数代表最大重推次数 - PushList map[string]string // 自动推流列表 - Proxy string // 代理地址 + EnableRegexp bool `desc:"是否启用正则表达式"` // 是否启用正则表达式 + RePush int `desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重推,0 表示不自动重推,-1 表示无限重推,高于0 的数代表最大重推次数 + PushList map[string]string `desc:"自动推流列表"` // 自动推流列表 + Proxy string `desc:"代理地址"` // 代理地址 } func (p *Push) GetPushConfig() *Push { diff --git a/http.go b/http.go index 05d8f95..b5dfe63 100644 --- a/http.go +++ b/http.go @@ -88,7 +88,7 @@ func (conf *GlobalConfig) API_getConfig(w http.ResponseWriter, r *http.Request) } var data any if q.Get("yaml") != "" { - var tmp struct { + var tmp struct { File string Modified string Merged string @@ -137,12 +137,14 @@ func (conf *GlobalConfig) API_modifyConfig(w http.ResponseWriter, r *http.Reques } if err != nil { util.ReturnError(util.APIErrorDecode, err.Error(), w, r) - } else if err = p.Save(); err == nil { - p.RawConfig.ParseModifyFile(modified) - util.ReturnOK(w, r) - } else { - util.ReturnError(util.APIErrorSave, err.Error(), w, r) + return } + p.RawConfig.ParseModifyFile(modified) + if err = p.Save(); err != nil { + util.ReturnError(util.APIErrorSave, err.Error(), w, r) + return + } + util.ReturnOK(w, r) } // API_updateConfig 热更新配置 @@ -159,6 +161,22 @@ func (conf *GlobalConfig) API_updateConfig(w http.ResponseWriter, r *http.Reques } else { p = Engine } + var err error + var modified map[string]any + if q.Get("yaml") != "" { + err = yaml.NewDecoder(r.Body).Decode(&modified) + } else { + err = json.NewDecoder(r.Body).Decode(&modified) + } + if err != nil { + util.ReturnError(util.APIErrorDecode, err.Error(), w, r) + return + } + p.RawConfig.ParseModifyFile(modified) + if err = p.Save(); err != nil { + util.ReturnError(util.APIErrorSave, err.Error(), w, r) + return + } p.Update(&p.RawConfig) util.ReturnOK(w, r) } diff --git a/main.go b/main.go index 5762774..c6bf3cf 100755 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "path/filepath" - "reflect" "runtime" "strings" "time" @@ -131,8 +130,8 @@ func Run(ctx context.Context, conf any) (err error) { } } var userConfig map[string]any - if defaultYaml := reflect.ValueOf(plugin.Config).Elem().FieldByName("DefaultYaml"); defaultYaml.IsValid() { - if err := yaml.Unmarshal([]byte(defaultYaml.String()), &userConfig); err != nil { + if plugin.defaultYaml != "" { + if err := yaml.Unmarshal([]byte(plugin.defaultYaml), &userConfig); err != nil { log.Error("parsing default config error:", err) } else { plugin.RawConfig.ParseDefaultYaml(userConfig) diff --git a/plugin.go b/plugin.go index 7a67daf..e454f22 100644 --- a/plugin.go +++ b/plugin.go @@ -23,7 +23,7 @@ import ( ) // InstallPlugin 安装插件,传入插件配置生成插件信息对象 -func InstallPlugin(config config.Plugin) *Plugin { +func InstallPlugin(config config.Plugin, options ...any) *Plugin { defaults.SetDefaults(config) t := reflect.TypeOf(config).Elem() name := strings.TrimSuffix(t.Name(), "Config") @@ -31,6 +31,15 @@ func InstallPlugin(config config.Plugin) *Plugin { Name: name, Config: config, } + for _, v := range options { + switch v := v.(type) { + case DefaultYaml: + plugin.defaultYaml = v + case string: + name = v + plugin.Name = name + } + } _, pluginFilePath, _, _ := runtime.Caller(1) configDir := filepath.Dir(pluginFilePath) if parts := strings.Split(configDir, "@"); len(parts) > 1 { @@ -52,6 +61,7 @@ func InstallPlugin(config config.Plugin) *Plugin { } type FirstConfig *config.Config +type UpdateConfig *config.Config type DefaultYaml string // Plugin 插件信息 @@ -62,6 +72,7 @@ type Plugin struct { Config config.Plugin `json:"-" yaml:"-"` //类型化的插件配置 Version string //插件版本 RawConfig config.Config //最终合并后的配置的map形式方便查询 + defaultYaml DefaultYaml //默认配置 *log.Logger `json:"-" yaml:"-"` saveTimer *time.Timer //用于保存的时候的延迟,防抖 Disabled bool @@ -102,6 +113,9 @@ func (opt *Plugin) assign() { if err == nil { var modifyConfig map[string]any err = yaml.NewDecoder(f).Decode(&modifyConfig) + if err != nil { + panic(err) + } opt.RawConfig.ParseModifyFile(modifyConfig) } opt.registerHandler() @@ -124,7 +138,7 @@ func (opt *Plugin) run() { // Update 热更新配置 func (opt *Plugin) Update(conf *config.Config) { - opt.Config.OnEvent(conf) + opt.Config.OnEvent(UpdateConfig(conf)) } func (opt *Plugin) registerHandler() { @@ -154,6 +168,10 @@ func (opt *Plugin) Save() error { opt.saveTimer = time.AfterFunc(time.Second, func() { lock.Lock() defer lock.Unlock() + if opt.RawConfig.Modify == nil { + os.Remove(opt.settingPath()) + return + } file, err := os.OpenFile(opt.settingPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) if err == nil { defer file.Close()