2026-08-03 23:51:21 +08:00
|
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-09 16:42:21 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"math"
|
2026-08-03 23:51:21 +08:00
|
|
|
|
"net"
|
2026-09-09 16:42:21 +08:00
|
|
|
|
"net/url"
|
|
|
|
|
|
"path/filepath"
|
2026-08-03 23:51:21 +08:00
|
|
|
|
"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"`
|
2026-09-09 16:42:21 +08:00
|
|
|
|
Storage StorageConf `yaml:"Storage"`
|
2026-08-03 23:51:21 +08:00
|
|
|
|
Namespaces map[string]NamespaceConf `yaml:"Namespaces"`
|
|
|
|
|
|
ServiceClients map[string]string `yaml:"ServiceClients"`
|
|
|
|
|
|
Cleanup CleanupConf `yaml:"Cleanup"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
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"`
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type NamespaceConf struct {
|
|
|
|
|
|
Prefix string `yaml:"Prefix"`
|
|
|
|
|
|
MaxSizeMB int64 `yaml:"MaxSizeMB"`
|
|
|
|
|
|
AllowedExtensions []string `yaml:"AllowedExtensions"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CleanupConf struct {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
IntervalSeconds int64 `yaml:"IntervalSeconds"`
|
|
|
|
|
|
IntegrityIntervalSeconds int64 `yaml:"IntegrityIntervalSeconds"`
|
|
|
|
|
|
IntegrityBatchSize int `yaml:"IntegrityBatchSize"`
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func New(srvKey string) {
|
|
|
|
|
|
conf.New(srvKey, &Spec)
|
2026-09-09 16:42:21 +08:00
|
|
|
|
if err := Validate(); err != nil {
|
|
|
|
|
|
panic(fmt.Errorf("files 配置校验失败: %w", err))
|
|
|
|
|
|
}
|
2026-08-03 23:51:21 +08:00
|
|
|
|
Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port)
|
|
|
|
|
|
conf.PrintInfo(Spec.Addr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
// 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 不能为空")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
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 秒之间")
|
|
|
|
|
|
}
|
2026-08-03 23:51:21 +08:00
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
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")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(Spec.Namespaces) == 0 {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError("Namespaces", "不能为空")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
for name, namespace := range Spec.Namespaces {
|
|
|
|
|
|
path := "Namespaces." + name
|
|
|
|
|
|
if !configNameRegexp.MatchString(name) {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path, "名称仅允许小写字母、数字和连字符")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
if !isSafeRelativeObjectPath(namespace.Prefix) {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path+".Prefix", "必须是安全的相对对象路径")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
if namespace.MaxSizeMB <= 0 {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path+".MaxSizeMB", "必须大于 0")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
if len(namespace.AllowedExtensions) == 0 {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path+".AllowedExtensions", "不能为空")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
for index, extension := range namespace.AllowedExtensions {
|
|
|
|
|
|
extensionPath := path + ".AllowedExtensions[" + stringIndex(index) + "]"
|
|
|
|
|
|
if !strings.HasPrefix(extension, ".") {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(extensionPath, "必须以 . 开头")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
namespace.AllowedExtensions[index] = strings.ToLower(extension)
|
|
|
|
|
|
}
|
|
|
|
|
|
Spec.Namespaces[name] = namespace
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(Spec.ServiceClients) == 0 {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError("ServiceClients", "不能为空")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
for name, secret := range Spec.ServiceClients {
|
|
|
|
|
|
path := "ServiceClients." + name
|
|
|
|
|
|
if !configNameRegexp.MatchString(name) {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path, "服务名仅允许小写字母、数字和连字符")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := validateRequired(path, secret); err != nil {
|
|
|
|
|
|
return err
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if Spec.Cleanup.IntervalSeconds <= 0 {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
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 地址")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return nil
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
func validateRequired(path, value string) error {
|
2026-08-03 23:51:21 +08:00
|
|
|
|
if strings.TrimSpace(value) == "" {
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return configError(path, "不能为空")
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
2026-09-09 16:42:21 +08:00
|
|
|
|
return nil
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 16:42:21 +08:00
|
|
|
|
func configError(path, message string) error {
|
|
|
|
|
|
return fmt.Errorf("配置项 %s 无效:%s", path, message)
|
2026-08-03 23:51:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func stringIndex(index int) string {
|
|
|
|
|
|
return strconv.Itoa(index)
|
|
|
|
|
|
}
|