361 lines
12 KiB
Go
361 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const DefaultListen = ":33550"
|
|
|
|
var Version = "0.1.0"
|
|
|
|
type Config struct {
|
|
BaseDir string `json:"base_dir"`
|
|
ConfigPath string `json:"-"`
|
|
Initialized bool `json:"initialized"`
|
|
Listen string `json:"listen"`
|
|
BaseURL string `json:"base_url"`
|
|
StorageDir string `json:"storage_dir"`
|
|
DataDir string `json:"data_dir"`
|
|
UpdatePublicDir string `json:"update_public_dir"`
|
|
UpdateNoticeDir string `json:"update_notice_dir"`
|
|
DownloadsDir string `json:"downloads_dir"`
|
|
AdminWebDir string `json:"admin_web_dir"`
|
|
PortalWebDir string `json:"portal_web_dir"`
|
|
SetupWebDir string `json:"setup_web_dir"`
|
|
LegacyUpdateDir string `json:"legacy_update_dir"`
|
|
LegacyFeedbackDir string `json:"legacy_feedback_dir"`
|
|
LegacyUpdateNoticeDir string `json:"legacy_update_notice_dir"`
|
|
ClientSignatureKey string `json:"client_signature_key"`
|
|
PackageEncryptionKey string `json:"package_encryption_key"`
|
|
TimestampWindowSeconds int64 `json:"timestamp_window_seconds"`
|
|
MaxRequestBytes int64 `json:"max_request_bytes"`
|
|
MaxPackageBytes int64 `json:"max_package_bytes"`
|
|
Database DatabaseConfig `json:"database"`
|
|
UploadGuard UploadGuardConfig `json:"upload_guard"`
|
|
SourceCheckSeconds int `json:"source_check_seconds"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Provider string `json:"provider"`
|
|
SQLitePath string `json:"sqlite_path"`
|
|
MySQLDSN string `json:"mysql_dsn"`
|
|
FailoverEnabled bool `json:"failover_enabled"`
|
|
HotSyncEnabled bool `json:"hot_sync_enabled"`
|
|
HealthIntervalSec int `json:"health_interval_sec"`
|
|
MaxOpenConns int `json:"max_open_conns"`
|
|
MaxIdleConns int `json:"max_idle_conns"`
|
|
ConnMaxLifetimeSeconds int `json:"conn_max_lifetime_seconds"`
|
|
}
|
|
|
|
type UploadGuardConfig struct {
|
|
MaxZipFiles int `json:"max_zip_files"`
|
|
MaxDecompressedBytes int64 `json:"max_decompressed_bytes"`
|
|
MaxSingleFileBytes int64 `json:"max_single_file_bytes"`
|
|
MaxCompressionRatio float64 `json:"max_compression_ratio"`
|
|
MaxReadableTextBytes int64 `json:"max_readable_text_bytes"`
|
|
AllowUnexpectedZipFiles bool `json:"allow_unexpected_zip_files"`
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
root, err := ResolveBaseDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg := defaults(root)
|
|
path := firstNonEmpty(os.Getenv("YMHUT_UNIFIED_CONFIG"), filepath.Join(root, "config.json"))
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.Initialized = true
|
|
}
|
|
cfg.BaseDir = root
|
|
cfg.ConfigPath = path
|
|
applyEnv(cfg)
|
|
normalize(root, cfg)
|
|
return cfg, nil
|
|
}
|
|
|
|
func defaults(root string) *Config {
|
|
return &Config{
|
|
BaseDir: root,
|
|
ConfigPath: filepath.Join(root, "config.json"),
|
|
Initialized: false,
|
|
Listen: DefaultListen,
|
|
BaseURL: "https://update.ymhut.cn",
|
|
StorageDir: filepath.Join(root, "storage"),
|
|
DataDir: filepath.Join(root, "data"),
|
|
UpdatePublicDir: filepath.Join(root, "data", "update", "public"),
|
|
UpdateNoticeDir: filepath.Join(root, "data", "update-notice"),
|
|
DownloadsDir: filepath.Join(root, "data", "update", "public", "downloads"),
|
|
AdminWebDir: filepath.Join(root, "web", "admin", "dist"),
|
|
PortalWebDir: filepath.Join(root, "web", "portal", "dist"),
|
|
SetupWebDir: filepath.Join(root, "web", "setup", "dist"),
|
|
LegacyUpdateDir: filepath.Clean(filepath.Join(root, "..", "update")),
|
|
LegacyFeedbackDir: filepath.Clean(filepath.Join(root, "..", "feedback-mailer")),
|
|
LegacyUpdateNoticeDir: filepath.Clean(filepath.Join(root, "..", "..", "update-notice")),
|
|
ClientSignatureKey: "ymhut-box-feedback-client-v1",
|
|
PackageEncryptionKey: "ymhut-box-feedback-package-v1",
|
|
TimestampWindowSeconds: 600,
|
|
MaxRequestBytes: 12 * 1024 * 1024,
|
|
MaxPackageBytes: 10 * 1024 * 1024,
|
|
SourceCheckSeconds: 300,
|
|
Database: DatabaseConfig{
|
|
Provider: "sqlite",
|
|
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
|
|
FailoverEnabled: true,
|
|
HotSyncEnabled: true,
|
|
HealthIntervalSec: 30,
|
|
MaxOpenConns: 10,
|
|
MaxIdleConns: 4,
|
|
ConnMaxLifetimeSeconds: 300,
|
|
},
|
|
UploadGuard: UploadGuardConfig{
|
|
MaxZipFiles: 80,
|
|
MaxDecompressedBytes: 30 * 1024 * 1024,
|
|
MaxSingleFileBytes: 8 * 1024 * 1024,
|
|
MaxCompressionRatio: 120,
|
|
MaxReadableTextBytes: 256 * 1024,
|
|
AllowUnexpectedZipFiles: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func applyEnv(cfg *Config) {
|
|
if value := os.Getenv("YMHUT_BASE_DIR"); value != "" {
|
|
cfg.BaseDir = value
|
|
}
|
|
if value := os.Getenv("PORT"); value != "" {
|
|
cfg.Listen = ":" + value
|
|
}
|
|
if value := os.Getenv("YMHUT_LISTEN"); value != "" {
|
|
cfg.Listen = value
|
|
}
|
|
if value := os.Getenv("YMHUT_BASE_URL"); value != "" {
|
|
cfg.BaseURL = value
|
|
}
|
|
if value := os.Getenv("YMHUT_STORAGE_DIR"); value != "" {
|
|
cfg.StorageDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_DATA_DIR"); value != "" {
|
|
cfg.DataDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_UPDATE_PUBLIC_DIR"); value != "" {
|
|
cfg.UpdatePublicDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_UPDATE_NOTICE_DIR"); value != "" {
|
|
cfg.UpdateNoticeDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_DOWNLOADS_DIR"); value != "" {
|
|
cfg.DownloadsDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_LEGACY_UPDATE_DIR"); value != "" {
|
|
cfg.LegacyUpdateDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_LEGACY_FEEDBACK_DIR"); value != "" {
|
|
cfg.LegacyFeedbackDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_LEGACY_UPDATE_NOTICE_DIR"); value != "" {
|
|
cfg.LegacyUpdateNoticeDir = value
|
|
}
|
|
if value := os.Getenv("YMHUT_DB_PROVIDER"); value != "" {
|
|
cfg.Database.Provider = value
|
|
}
|
|
if value := os.Getenv("YMHUT_SQLITE_PATH"); value != "" {
|
|
cfg.Database.SQLitePath = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MYSQL_DSN"); value != "" {
|
|
cfg.Database.MySQLDSN = value
|
|
}
|
|
if value := os.Getenv("YMHUT_CLIENT_SIGNATURE_KEY"); value != "" {
|
|
cfg.ClientSignatureKey = value
|
|
}
|
|
if value := os.Getenv("YMHUT_PACKAGE_ENCRYPTION_KEY"); value != "" {
|
|
cfg.PackageEncryptionKey = value
|
|
}
|
|
if value := os.Getenv("YMHUT_TIMESTAMP_WINDOW_SECONDS"); value != "" {
|
|
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
|
|
cfg.TimestampWindowSeconds = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_MAX_REQUEST_BYTES"); value != "" {
|
|
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
|
|
cfg.MaxRequestBytes = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_MAX_PACKAGE_BYTES"); value != "" {
|
|
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
|
|
cfg.MaxPackageBytes = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_SOURCE_CHECK_SECONDS"); value != "" {
|
|
if parsed, err := strconv.Atoi(value); err == nil {
|
|
cfg.SourceCheckSeconds = parsed
|
|
}
|
|
}
|
|
}
|
|
|
|
func normalize(root string, cfg *Config) {
|
|
cfg.BaseDir = absPath(root, firstNonEmpty(cfg.BaseDir, root))
|
|
if cfg.ConfigPath == "" {
|
|
cfg.ConfigPath = filepath.Join(cfg.BaseDir, "config.json")
|
|
}
|
|
if cfg.Listen == "" {
|
|
cfg.Listen = DefaultListen
|
|
}
|
|
if cfg.StorageDir == "" {
|
|
cfg.StorageDir = filepath.Join(cfg.BaseDir, "storage")
|
|
}
|
|
cfg.StorageDir = absPath(cfg.BaseDir, cfg.StorageDir)
|
|
if cfg.DataDir == "" {
|
|
cfg.DataDir = filepath.Join(cfg.BaseDir, "data")
|
|
}
|
|
cfg.DataDir = absPath(cfg.BaseDir, cfg.DataDir)
|
|
if cfg.UpdatePublicDir == "" {
|
|
cfg.UpdatePublicDir = filepath.Join(cfg.DataDir, "update", "public")
|
|
}
|
|
cfg.UpdatePublicDir = absPath(cfg.BaseDir, cfg.UpdatePublicDir)
|
|
if cfg.UpdateNoticeDir == "" {
|
|
cfg.UpdateNoticeDir = filepath.Join(cfg.DataDir, "update-notice")
|
|
}
|
|
cfg.UpdateNoticeDir = absPath(cfg.BaseDir, cfg.UpdateNoticeDir)
|
|
if cfg.DownloadsDir == "" {
|
|
cfg.DownloadsDir = filepath.Join(cfg.UpdatePublicDir, "downloads")
|
|
}
|
|
cfg.DownloadsDir = absPath(cfg.BaseDir, cfg.DownloadsDir)
|
|
if cfg.AdminWebDir == "" {
|
|
cfg.AdminWebDir = filepath.Join(cfg.BaseDir, "web", "admin", "dist")
|
|
}
|
|
cfg.AdminWebDir = absPath(cfg.BaseDir, cfg.AdminWebDir)
|
|
if cfg.PortalWebDir == "" {
|
|
cfg.PortalWebDir = filepath.Join(cfg.BaseDir, "web", "portal", "dist")
|
|
}
|
|
cfg.PortalWebDir = absPath(cfg.BaseDir, cfg.PortalWebDir)
|
|
if cfg.SetupWebDir == "" {
|
|
cfg.SetupWebDir = filepath.Join(cfg.BaseDir, "web", "setup", "dist")
|
|
}
|
|
cfg.SetupWebDir = absPath(cfg.BaseDir, cfg.SetupWebDir)
|
|
if cfg.LegacyUpdateDir == "" {
|
|
cfg.LegacyUpdateDir = filepath.Clean(filepath.Join(cfg.BaseDir, "..", "update"))
|
|
}
|
|
cfg.LegacyUpdateDir = absPath(cfg.BaseDir, cfg.LegacyUpdateDir)
|
|
if cfg.LegacyFeedbackDir == "" {
|
|
cfg.LegacyFeedbackDir = filepath.Clean(filepath.Join(cfg.BaseDir, "..", "feedback-mailer"))
|
|
}
|
|
cfg.LegacyFeedbackDir = absPath(cfg.BaseDir, cfg.LegacyFeedbackDir)
|
|
if cfg.LegacyUpdateNoticeDir == "" {
|
|
cfg.LegacyUpdateNoticeDir = filepath.Clean(filepath.Join(cfg.BaseDir, "..", "..", "update-notice"))
|
|
}
|
|
cfg.LegacyUpdateNoticeDir = absPath(cfg.BaseDir, cfg.LegacyUpdateNoticeDir)
|
|
if cfg.Database.Provider == "" {
|
|
cfg.Database.Provider = "sqlite"
|
|
}
|
|
if cfg.Database.SQLitePath == "" {
|
|
cfg.Database.SQLitePath = filepath.Join(cfg.StorageDir, "unified.sqlite")
|
|
}
|
|
cfg.Database.SQLitePath = absPath(cfg.BaseDir, cfg.Database.SQLitePath)
|
|
if cfg.Database.HealthIntervalSec <= 0 {
|
|
cfg.Database.HealthIntervalSec = 30
|
|
}
|
|
if cfg.Database.MaxOpenConns <= 0 {
|
|
cfg.Database.MaxOpenConns = 10
|
|
}
|
|
if cfg.Database.MaxIdleConns <= 0 {
|
|
cfg.Database.MaxIdleConns = 4
|
|
}
|
|
if cfg.Database.ConnMaxLifetimeSeconds <= 0 {
|
|
cfg.Database.ConnMaxLifetimeSeconds = 300
|
|
}
|
|
if cfg.ClientSignatureKey == "" {
|
|
cfg.ClientSignatureKey = "ymhut-box-feedback-client-v1"
|
|
}
|
|
if cfg.PackageEncryptionKey == "" {
|
|
cfg.PackageEncryptionKey = "ymhut-box-feedback-package-v1"
|
|
}
|
|
if cfg.TimestampWindowSeconds <= 0 {
|
|
cfg.TimestampWindowSeconds = 600
|
|
}
|
|
if cfg.MaxRequestBytes <= 0 {
|
|
cfg.MaxRequestBytes = 12 * 1024 * 1024
|
|
}
|
|
if cfg.MaxPackageBytes <= 0 {
|
|
cfg.MaxPackageBytes = 10 * 1024 * 1024
|
|
}
|
|
if cfg.UploadGuard.MaxZipFiles <= 0 {
|
|
cfg.UploadGuard.MaxZipFiles = 80
|
|
}
|
|
if cfg.UploadGuard.MaxDecompressedBytes <= 0 {
|
|
cfg.UploadGuard.MaxDecompressedBytes = 30 * 1024 * 1024
|
|
}
|
|
if cfg.UploadGuard.MaxSingleFileBytes <= 0 {
|
|
cfg.UploadGuard.MaxSingleFileBytes = 8 * 1024 * 1024
|
|
}
|
|
if cfg.UploadGuard.MaxCompressionRatio <= 0 {
|
|
cfg.UploadGuard.MaxCompressionRatio = 120
|
|
}
|
|
if cfg.UploadGuard.MaxReadableTextBytes <= 0 {
|
|
cfg.UploadGuard.MaxReadableTextBytes = 256 * 1024
|
|
}
|
|
if cfg.SourceCheckSeconds <= 0 {
|
|
cfg.SourceCheckSeconds = 300
|
|
}
|
|
}
|
|
|
|
func ResolveBaseDir() (string, error) {
|
|
if value := os.Getenv("YMHUT_BASE_DIR"); value != "" {
|
|
return filepath.Abs(value)
|
|
}
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
if filepath.Base(cwd) == "unified-management" {
|
|
return filepath.Abs(cwd)
|
|
}
|
|
candidate := filepath.Join(cwd, "server", "unified-management")
|
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
|
return filepath.Abs(candidate)
|
|
}
|
|
}
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return os.Getwd()
|
|
}
|
|
return filepath.Abs(filepath.Dir(exe))
|
|
}
|
|
|
|
func Save(cfg *Config) error {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
normalize(firstNonEmpty(cfg.BaseDir, "."), cfg)
|
|
if err := os.MkdirAll(filepath.Dir(cfg.ConfigPath), 0o750); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(cfg.ConfigPath, data, 0o600)
|
|
}
|
|
|
|
func absPath(base, value string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return value
|
|
}
|
|
if filepath.IsAbs(value) || strings.HasPrefix(strings.ToLower(value), "file:") {
|
|
return filepath.Clean(value)
|
|
}
|
|
return filepath.Clean(filepath.Join(base, value))
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|