package config import ( "fmt" "math" "net" "net/url" "path/filepath" "regexp" "strconv" "strings" "git.apinb.com/bsm-sdk/core/conf" ) var ( Spec SrvConfig configNameRegexp = regexp.MustCompile(`^[a-z0-9-]+$`) ) type SrvConfig struct { conf.Base `yaml:",inline"` Databases *conf.DBConf `yaml:"Databases"` MicroService *conf.MicroServiceConf `yaml:"MicroService"` Etcd *conf.EtcdConf `yaml:"Etcd"` Storage StorageConf `yaml:"Storage"` Namespaces map[string]NamespaceConf `yaml:"Namespaces"` ServiceClients map[string]string `yaml:"ServiceClients"` Cleanup CleanupConf `yaml:"Cleanup"` } type StorageConf struct { Provider string `yaml:"Provider"` ExternalBaseURL string `yaml:"ExternalBaseURL"` AccessTTLSeconds int64 `yaml:"AccessTTLSeconds"` SigningSecret string `yaml:"SigningSecret"` Local LocalStorageConf `yaml:"Local"` Aliyun AliyunStorageConf `yaml:"Aliyun"` } type LocalStorageConf struct { RootPath string `yaml:"RootPath"` MinimumFreeSpaceMB uint64 `yaml:"MinimumFreeSpaceMB"` } type AliyunStorageConf struct { Endpoint string `yaml:"Endpoint"` Region string `yaml:"Region"` Bucket string `yaml:"Bucket"` AccessKeyID string `yaml:"AccessKeyID"` AccessKeySecret string `yaml:"AccessKeySecret"` } type NamespaceConf struct { Prefix string `yaml:"Prefix"` MaxSizeMB int64 `yaml:"MaxSizeMB"` AllowedExtensions []string `yaml:"AllowedExtensions"` } type CleanupConf struct { IntervalSeconds int64 `yaml:"IntervalSeconds"` IntegrityIntervalSeconds int64 `yaml:"IntegrityIntervalSeconds"` IntegrityBatchSize int `yaml:"IntegrityBatchSize"` } func New(srvKey string) { conf.New(srvKey, &Spec) if err := Validate(); err != nil { panic(fmt.Errorf("files 配置校验失败: %w", err)) } Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port) conf.PrintInfo(Spec.Addr) } // Validate 校验文件服务配置,不生成随机监听参数或对象存储默认值。 func Validate() error { Spec.Service = strings.TrimSpace(Spec.Service) Spec.Port = strings.TrimSpace(Spec.Port) Spec.BindIP = strings.TrimSpace(Spec.BindIP) if err := validateRequired("Service", Spec.Service); err != nil { return err } if err := validateRequired("Cache", Spec.Cache); err != nil { return err } port, err := strconv.Atoi(Spec.Port) if err != nil || port < 1 || port > 65535 { return fmt.Errorf("配置项 Port 无效:必须是 1 到 65535 的整数") } if net.ParseIP(Spec.BindIP) == nil { return fmt.Errorf("配置项 BindIP 无效:必须是明确的 IPv4 或 IPv6 地址") } if Spec.Databases == nil || strings.TrimSpace(Spec.Databases.Driver) == "" || len(Spec.Databases.Source) == 0 { return fmt.Errorf("配置项 Databases 无效:Driver 和 Source 不能为空") } Spec.Storage.Provider = strings.ToLower(strings.TrimSpace(Spec.Storage.Provider)) for path, value := range map[string]string{ "Storage.Provider": Spec.Storage.Provider, "Storage.ExternalBaseURL": Spec.Storage.ExternalBaseURL, "Storage.SigningSecret": Spec.Storage.SigningSecret, } { if err := validateRequired(path, value); err != nil { return err } } if err := validateHTTPURL("Storage.ExternalBaseURL", Spec.Storage.ExternalBaseURL); err != nil { return err } if Spec.Storage.AccessTTLSeconds < 60 || Spec.Storage.AccessTTLSeconds > 3600 { return configError("Storage.AccessTTLSeconds", "必须在 60 到 3600 秒之间") } switch Spec.Storage.Provider { case "local": Spec.Storage.Local.RootPath = strings.TrimSpace(Spec.Storage.Local.RootPath) if !filepath.IsAbs(Spec.Storage.Local.RootPath) { return configError("Storage.Local.RootPath", "必须是绝对路径") } if Spec.Storage.Local.MinimumFreeSpaceMB == 0 { return configError("Storage.Local.MinimumFreeSpaceMB", "必须大于 0") } if Spec.Storage.Local.MinimumFreeSpaceMB > math.MaxUint64/(1024*1024) { return configError("Storage.Local.MinimumFreeSpaceMB", "换算为字节后溢出") } case "aliyun": for path, value := range map[string]string{ "Storage.Aliyun.Endpoint": Spec.Storage.Aliyun.Endpoint, "Storage.Aliyun.Region": Spec.Storage.Aliyun.Region, "Storage.Aliyun.Bucket": Spec.Storage.Aliyun.Bucket, "Storage.Aliyun.AccessKeyID": Spec.Storage.Aliyun.AccessKeyID, "Storage.Aliyun.AccessKeySecret": Spec.Storage.Aliyun.AccessKeySecret, } { if err := validateRequired(path, value); err != nil { return err } } if err := validateHTTPURL("Storage.Aliyun.Endpoint", Spec.Storage.Aliyun.Endpoint); err != nil { return err } default: return configError("Storage.Provider", "必须为 local 或 aliyun") } if len(Spec.Namespaces) == 0 { return configError("Namespaces", "不能为空") } for name, namespace := range Spec.Namespaces { path := "Namespaces." + name if !configNameRegexp.MatchString(name) { return configError(path, "名称仅允许小写字母、数字和连字符") } if !isSafeRelativeObjectPath(namespace.Prefix) { return configError(path+".Prefix", "必须是安全的相对对象路径") } if namespace.MaxSizeMB <= 0 { return configError(path+".MaxSizeMB", "必须大于 0") } if len(namespace.AllowedExtensions) == 0 { return configError(path+".AllowedExtensions", "不能为空") } for index, extension := range namespace.AllowedExtensions { extensionPath := path + ".AllowedExtensions[" + stringIndex(index) + "]" if !strings.HasPrefix(extension, ".") { return configError(extensionPath, "必须以 . 开头") } namespace.AllowedExtensions[index] = strings.ToLower(extension) } Spec.Namespaces[name] = namespace } if len(Spec.ServiceClients) == 0 { return configError("ServiceClients", "不能为空") } for name, secret := range Spec.ServiceClients { path := "ServiceClients." + name if !configNameRegexp.MatchString(name) { return configError(path, "服务名仅允许小写字母、数字和连字符") } if err := validateRequired(path, secret); err != nil { return err } } if Spec.Cleanup.IntervalSeconds <= 0 { return configError("Cleanup.IntervalSeconds", "必须大于 0") } if Spec.Cleanup.IntegrityIntervalSeconds <= 0 { return configError("Cleanup.IntegrityIntervalSeconds", "必须大于 0") } if Spec.Cleanup.IntegrityBatchSize <= 0 || Spec.Cleanup.IntegrityBatchSize > 1000 { return configError("Cleanup.IntegrityBatchSize", "必须在 1 到 1000 之间") } return nil } func validateHTTPURL(path, raw string) error { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return configError(path, "必须是完整的 HTTP 或 HTTPS 地址") } return nil } func validateRequired(path, value string) error { if strings.TrimSpace(value) == "" { return configError(path, "不能为空") } return nil } func isSafeRelativeObjectPath(path string) bool { if path == "" || strings.HasPrefix(path, "/") || strings.Contains(path, "\\") { return false } for _, segment := range strings.Split(path, "/") { if segment == "." || segment == ".." { return false } } return true } func configError(path, message string) error { return fmt.Errorf("配置项 %s 无效:%s", path, message) } func stringIndex(index int) string { return strconv.Itoa(index) }