Files
files/internal/config/config.go
2026-09-09 16:42:21 +08:00

231 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}