feat: add storage schemas

This commit is contained in:
langhuihui
2025-12-19 17:22:55 +08:00
parent fed7c65fe8
commit 5d42061e56
7 changed files with 222 additions and 3 deletions

11
api.go
View File

@@ -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)
}

View File

@@ -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",
},
}
}

View File

@@ -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{}),
})
}

View File

@@ -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{}),
})
}

View File

@@ -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{}),
})
}

153
pkg/storage/schema.go Normal file
View File

@@ -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{}),
})
}

View File

@@ -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 != "" {