From 5d42061e5693e3f4dbf0ec88236c9f7f92d5c62e Mon Sep 17 00:00:00 2001 From: langhuihui <178529795@qq.com> Date: Fri, 19 Dec 2025 17:22:55 +0800 Subject: [PATCH] feat: add storage schemas --- api.go | 11 ++- pkg/config/formily.go | 32 +++++++++ pkg/storage/cos.go | 10 ++- pkg/storage/oss.go | 10 ++- pkg/storage/s3.go | 8 +++ pkg/storage/schema.go | 153 ++++++++++++++++++++++++++++++++++++++++++ server.go | 1 + 7 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 pkg/storage/schema.go diff --git a/api.go b/api.go index 644fb33..9f7ca29 100644 --- a/api.go +++ b/api.go @@ -28,6 +28,7 @@ import ( "m7s.live/v5/pb" "m7s.live/v5/pkg" "m7s.live/v5/pkg/format" + "m7s.live/v5/pkg/storage" "m7s.live/v5/pkg/util" ) @@ -724,7 +725,7 @@ func (s *Server) GetConfigFile(_ context.Context, req *emptypb.Empty) (res *pb.G func (s *Server) UpdateConfigFile(_ context.Context, req *pb.UpdateConfigFileRequest) (res *pb.SuccessResponse, err error) { if s.configFileContent != nil { s.configFileContent = []byte(req.Content) - os.WriteFile(s.configFilePath, s.configFileContent, 0644) + err = os.WriteFile(s.configFilePath, s.configFileContent, 0644) res = &pb.SuccessResponse{} } else { err = pkg.ErrNotFound @@ -1456,3 +1457,11 @@ func (s *Server) GetAlarmList(ctx context.Context, req *pb.AlarmListRequest) (re return res, nil } + +// GetStorageSchemas 获取所有已注册的存储类型 Schema +// 用于前端动态渲染存储配置表单 +func (s *Server) GetStorageSchemas(w http.ResponseWriter, r *http.Request) { + schemas := storage.GetSchemas() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(schemas) +} diff --git a/pkg/config/formily.go b/pkg/config/formily.go index 27e861d..92eb816 100644 --- a/pkg/config/formily.go +++ b/pkg/config/formily.go @@ -518,6 +518,13 @@ func (config *Config) schema(index int) (r any) { if valueType.Kind() == reflect.Ptr { valueType = valueType.Elem() } + + // 特殊处理 map[string]any 类型(如 Storage 字段) + // 使用动态 Schema 注册机制 + if keyType.Kind() == reflect.String && valueType.Kind() == reflect.Interface { + return buildDynamicStorageSchema(config, index, isDefault, valueSource) + } + valueIsStruct := valueType.Kind() == reflect.Struct && valueType != regexpType valueIsMap := valueType.Kind() == reflect.Map @@ -1019,3 +1026,28 @@ func (config *Config) GetFormily() (r Object) { } return } + +// buildDynamicStorageSchema 为 map[string]any 类型(如 Storage)构建动态表单 Schema +// 使用 DynamicStorage 组件,前端会自动调用 /api/storage/schemas 获取存储类型列表 +func buildDynamicStorageSchema(config *Config, index int, isDefault bool, valueSource string) Property { + // 获取当前配置的值 + currentValue := config.GetValue() + + return Property{ + Type: "object", + Title: config.name, + Decorator: "FormItem", + Component: "DynamicStorage", + Index: index, + Default: currentValue, + ComplexType: "dynamic-storage", + IsDefault: isDefault, + ValueSource: valueSource, + DecoratorProps: map[string]any{ + "tooltip": config.tag.Get("desc"), + }, + ComponentProps: map[string]any{ + "apiUrl": "/api/storage/schemas", + }, + } +} diff --git a/pkg/storage/cos.go b/pkg/storage/cos.go index 4b7083b..b17350b 100644 --- a/pkg/storage/cos.go +++ b/pkg/storage/cos.go @@ -364,6 +364,14 @@ func init() { Factory["cos"] = func(config any) (Storage, error) { var cosConfig COSStorageConfig config.Parse(&cosConfig, config.(map[string]any)) - return NewCOSStorage(cosConfig) + return NewCOSStorage(&cosConfig) } + + // 注册 COS 存储类型 Schema + RegisterSchema(StorageSchema{ + Type: "cos", + Name: "腾讯云 COS", + Description: "腾讯云对象存储服务", + Properties: GenerateSchemaFromStruct(COSStorageConfig{}), + }) } diff --git a/pkg/storage/oss.go b/pkg/storage/oss.go index 0b609c6..c22729c 100644 --- a/pkg/storage/oss.go +++ b/pkg/storage/oss.go @@ -357,6 +357,14 @@ func init() { Factory["oss"] = func(config any) (Storage, error) { var ossConfig OSSStorageConfig config.Parse(&ossConfig, config.(map[string]any)) - return NewOSSStorage(ossConfig) + return NewOSSStorage(&ossConfig) } + + // 注册 OSS 存储类型 Schema + RegisterSchema(StorageSchema{ + Type: "oss", + Name: "阿里云 OSS", + Description: "阿里云对象存储服务", + Properties: GenerateSchemaFromStruct(OSSStorageConfig{}), + }) } diff --git a/pkg/storage/s3.go b/pkg/storage/s3.go index eaa05da..94e1327 100644 --- a/pkg/storage/s3.go +++ b/pkg/storage/s3.go @@ -446,4 +446,12 @@ func init() { config.Parse(&s3Config, conf.(map[string]any)) return NewS3Storage(&s3Config) } + + // 注册 S3 存储类型 Schema + RegisterSchema(StorageSchema{ + Type: "s3", + Name: "S3 存储", + Description: "AWS S3 或兼容 S3 协议的对象存储(如 MinIO)", + Properties: GenerateSchemaFromStruct(S3StorageConfig{}), + }) } diff --git a/pkg/storage/schema.go b/pkg/storage/schema.go new file mode 100644 index 0000000..7bebbb1 --- /dev/null +++ b/pkg/storage/schema.go @@ -0,0 +1,153 @@ +package storage + +import ( + "reflect" + "strings" +) + +// PropertyDef 属性定义 +type PropertyDef struct { + Type string `json:"type"` // string, number, boolean, object, array + Default any `json:"default,omitempty"` // 默认值 + Description string `json:"desc,omitempty"` // 描述 + Enum []string `json:"enum,omitempty"` // 枚举值 + Required bool `json:"required,omitempty"` // 是否必填 + Min *float64 `json:"min,omitempty"` // 最小值(number类型) + Max *float64 `json:"max,omitempty"` // 最大值(number类型) + Pattern string `json:"pattern,omitempty"` // 正则模式(string类型) + Properties Schema `json:"properties,omitempty"` // 嵌套对象的属性 +} + +// Schema 配置 Schema +type Schema map[string]PropertyDef + +// StorageSchema 存储类型 Schema +type StorageSchema struct { + Type string `json:"type"` // 存储类型标识 + Name string `json:"name"` // 显示名称 + Description string `json:"description"` // 描述 + Properties Schema `json:"properties"` // 配置属性 +} + +// SchemaRegistry 存储类型 Schema 注册表 +var SchemaRegistry = make(map[string]StorageSchema) + +// RegisterSchema 注册存储类型 Schema +func RegisterSchema(schema StorageSchema) { + SchemaRegistry[schema.Type] = schema +} + +// GetSchemas 获取所有已注册的存储类型 Schema +func GetSchemas() map[string]StorageSchema { + return SchemaRegistry +} + +// GetSchema 获取指定存储类型的 Schema +func GetSchema(storageType string) (StorageSchema, bool) { + schema, ok := SchemaRegistry[storageType] + return schema, ok +} + +// GenerateSchemaFromStruct 从结构体自动生成 Schema +// 支持 json/yaml tag 作为字段名,desc tag 作为描述,default tag 作为默认值 +func GenerateSchemaFromStruct(v any) Schema { + schema := make(Schema) + t := reflect.TypeOf(v) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return schema + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + // 获取字段名(优先使用 json tag,其次 yaml tag) + fieldName := getFieldName(field) + if fieldName == "" || fieldName == "-" { + continue + } + + prop := PropertyDef{ + Type: getFieldType(field.Type), + Description: field.Tag.Get("desc"), + } + + // 解析 default tag + if defaultVal := field.Tag.Get("default"); defaultVal != "" { + prop.Default = defaultVal + } + + // 解析 enum tag + if enumVal := field.Tag.Get("enum"); enumVal != "" { + prop.Enum = strings.Split(enumVal, ",") + } + + // 判断是否必填(通过检查是否有 required tag 或字段是否为指针类型) + if field.Tag.Get("required") == "true" { + prop.Required = true + } + + // 处理嵌套结构体 + if field.Type.Kind() == reflect.Struct && field.Type.String() != "time.Duration" { + prop.Properties = GenerateSchemaFromStruct(reflect.New(field.Type).Interface()) + } + + schema[fieldName] = prop + } + + return schema +} + +// getFieldName 获取字段名 +func getFieldName(field reflect.StructField) string { + // 优先使用 json tag + if jsonTag := field.Tag.Get("json"); jsonTag != "" { + parts := strings.Split(jsonTag, ",") + return parts[0] + } + // 其次使用 yaml tag + if yamlTag := field.Tag.Get("yaml"); yamlTag != "" { + parts := strings.Split(yamlTag, ",") + return parts[0] + } + // 最后使用字段名的小写形式 + return strings.ToLower(field.Name) +} + +// getFieldType 获取字段类型 +func getFieldType(t reflect.Type) string { + switch t.Kind() { + case reflect.String: + return "string" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return "number" + case reflect.Bool: + return "boolean" + case reflect.Slice, reflect.Array: + return "array" + case reflect.Map, reflect.Struct: + // 特殊处理 time.Duration + if t.String() == "time.Duration" { + return "string" // Duration 在 YAML 中通常表示为字符串如 "30s" + } + return "object" + case reflect.Ptr: + return getFieldType(t.Elem()) + default: + return "string" + } +} + +func init() { + // 注册 local 存储类型 Schema + RegisterSchema(StorageSchema{ + Type: "local", + Name: "本地存储", + Description: "将文件存储到本地磁盘", + Properties: GenerateSchemaFromStruct(LocalStorageConfig{}), + }) +} diff --git a/server.go b/server.go index 8632480..d77c6f0 100644 --- a/server.go +++ b/server.go @@ -284,6 +284,7 @@ func (s *Server) Start() (err error) { "/api/videotrack/sse/{streamPath...}": s.api_VideoTrack_SSE, "/api/audiotrack/sse/{streamPath...}": s.api_AudioTrack_SSE, "/annexb/{streamPath...}": s.annexB, + "/api/storage/schemas": s.GetStorageSchemas, }) if s.config.DSN != "" {