623 lines
20 KiB
Go
623 lines
20 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"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"`
|
|
Mail MailConfig `json:"mail"`
|
|
Branding BrandingConfig `json:"branding"`
|
|
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"`
|
|
MySQLHost string `json:"mysql_host"`
|
|
MySQLPort int `json:"mysql_port"`
|
|
MySQLDatabase string `json:"mysql_database"`
|
|
MySQLUser string `json:"mysql_user"`
|
|
MySQLPassword string `json:"mysql_password"`
|
|
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 MailConfig struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
Secure string `json:"secure"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
FromAddress string `json:"from_address"`
|
|
FromName string `json:"from_name"`
|
|
DeveloperAddress string `json:"developer_address"`
|
|
TimeoutSeconds int `json:"timeout_seconds"`
|
|
}
|
|
|
|
type BrandingConfig struct {
|
|
SiteIconURL string `json:"site_icon_url"`
|
|
DeveloperAvatarURL string `json:"developer_avatar_url"`
|
|
DeveloperName string `json:"developer_name"`
|
|
FeedbackEmail string `json:"feedback_email"`
|
|
}
|
|
|
|
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"))
|
|
var rawConfig []byte
|
|
loaded := false
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.Initialized = true
|
|
rawConfig = data
|
|
loaded = true
|
|
}
|
|
cfg.BaseDir = root
|
|
cfg.ConfigPath = path
|
|
if loaded {
|
|
sanitizeNonPortablePaths(cfg)
|
|
}
|
|
applyEnv(cfg)
|
|
normalize(root, cfg)
|
|
if loaded && shouldRewriteRelativeConfig(rawConfig) {
|
|
if err := Save(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
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"),
|
|
MySQLHost: "127.0.0.1",
|
|
MySQLPort: 3306,
|
|
FailoverEnabled: true,
|
|
HotSyncEnabled: true,
|
|
HealthIntervalSec: 30,
|
|
MaxOpenConns: 10,
|
|
MaxIdleConns: 4,
|
|
ConnMaxLifetimeSeconds: 300,
|
|
},
|
|
Mail: MailConfig{
|
|
Port: 465,
|
|
Secure: "ssl",
|
|
FromName: "YMhut Box Feedback",
|
|
DeveloperAddress: "support@ymhut.cn",
|
|
TimeoutSeconds: 20,
|
|
},
|
|
Branding: BrandingConfig{
|
|
SiteIconURL: "https://img.ymhut.cn/file/1782108850041_icon.webp",
|
|
DeveloperAvatarURL: "https://img.ymhut.cn/file/1782108780690_b_3db45f3787f19192c8de8e06bc0987ef.webp",
|
|
DeveloperName: "YMhut",
|
|
FeedbackEmail: "support@ymhut.cn",
|
|
},
|
|
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_MYSQL_HOST"); value != "" {
|
|
cfg.Database.MySQLHost = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MYSQL_PORT"); value != "" {
|
|
if parsed, err := strconv.Atoi(value); err == nil {
|
|
cfg.Database.MySQLPort = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_MYSQL_DATABASE"); value != "" {
|
|
cfg.Database.MySQLDatabase = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MYSQL_USER"); value != "" {
|
|
cfg.Database.MySQLUser = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MYSQL_PASSWORD"); value != "" {
|
|
cfg.Database.MySQLPassword = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_HOST"); value != "" {
|
|
cfg.Mail.Host = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_PORT"); value != "" {
|
|
if parsed, err := strconv.Atoi(value); err == nil {
|
|
cfg.Mail.Port = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_SECURE"); value != "" {
|
|
cfg.Mail.Secure = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_USERNAME"); value != "" {
|
|
cfg.Mail.Username = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_PASSWORD"); value != "" {
|
|
cfg.Mail.Password = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_FROM_ADDRESS"); value != "" {
|
|
cfg.Mail.FromAddress = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_FROM_NAME"); value != "" {
|
|
cfg.Mail.FromName = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_DEVELOPER_ADDRESS"); value != "" {
|
|
cfg.Mail.DeveloperAddress = value
|
|
}
|
|
if value := os.Getenv("YMHUT_MAIL_TIMEOUT_SECONDS"); value != "" {
|
|
if parsed, err := strconv.Atoi(value); err == nil {
|
|
cfg.Mail.TimeoutSeconds = parsed
|
|
}
|
|
}
|
|
if value := os.Getenv("YMHUT_BRAND_ICON_URL"); value != "" {
|
|
cfg.Branding.SiteIconURL = value
|
|
}
|
|
if value := os.Getenv("YMHUT_BRAND_DEVELOPER_AVATAR_URL"); value != "" {
|
|
cfg.Branding.DeveloperAvatarURL = value
|
|
}
|
|
if value := os.Getenv("YMHUT_BRAND_DEVELOPER_NAME"); value != "" {
|
|
cfg.Branding.DeveloperName = value
|
|
}
|
|
if value := os.Getenv("YMHUT_BRAND_FEEDBACK_EMAIL"); value != "" {
|
|
cfg.Branding.FeedbackEmail = 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"
|
|
}
|
|
cfg.Database.Provider = strings.ToLower(strings.TrimSpace(cfg.Database.Provider))
|
|
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.MySQLHost == "" {
|
|
cfg.Database.MySQLHost = "127.0.0.1"
|
|
}
|
|
if cfg.Database.MySQLPort <= 0 {
|
|
cfg.Database.MySQLPort = 3306
|
|
}
|
|
if cfg.Database.Provider == "mysql" && cfg.Database.MySQLDSN == "" && cfg.Database.MySQLDatabase != "" && cfg.Database.MySQLUser != "" {
|
|
if dsn, err := BuildMySQLDSN(MySQLInput{
|
|
Host: cfg.Database.MySQLHost,
|
|
Port: cfg.Database.MySQLPort,
|
|
Database: cfg.Database.MySQLDatabase,
|
|
Username: cfg.Database.MySQLUser,
|
|
Password: cfg.Database.MySQLPassword,
|
|
Charset: "utf8mb4",
|
|
ParseTime: true,
|
|
}); err == nil {
|
|
cfg.Database.MySQLDSN = dsn
|
|
}
|
|
}
|
|
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
|
|
}
|
|
if cfg.Mail.Port <= 0 {
|
|
cfg.Mail.Port = 465
|
|
}
|
|
cfg.Mail.Secure = strings.ToLower(strings.TrimSpace(cfg.Mail.Secure))
|
|
if cfg.Mail.Secure == "" {
|
|
cfg.Mail.Secure = "ssl"
|
|
}
|
|
if cfg.Mail.FromName == "" {
|
|
cfg.Mail.FromName = "YMhut Box Feedback"
|
|
}
|
|
if cfg.Mail.FromAddress == "" {
|
|
cfg.Mail.FromAddress = cfg.Mail.Username
|
|
}
|
|
if cfg.Mail.TimeoutSeconds <= 0 {
|
|
cfg.Mail.TimeoutSeconds = 20
|
|
}
|
|
if cfg.Branding.SiteIconURL == "" {
|
|
cfg.Branding.SiteIconURL = "https://img.ymhut.cn/file/1782108850041_icon.webp"
|
|
}
|
|
if cfg.Branding.DeveloperAvatarURL == "" {
|
|
cfg.Branding.DeveloperAvatarURL = "https://img.ymhut.cn/file/1782108780690_b_3db45f3787f19192c8de8e06bc0987ef.webp"
|
|
}
|
|
if cfg.Branding.DeveloperName == "" {
|
|
cfg.Branding.DeveloperName = "YMhut"
|
|
}
|
|
if cfg.Branding.FeedbackEmail == "" {
|
|
cfg.Branding.FeedbackEmail = "support@ymhut.cn"
|
|
}
|
|
if cfg.Mail.DeveloperAddress == "" {
|
|
cfg.Mail.DeveloperAddress = cfg.Branding.FeedbackEmail
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
persisted := cfg.relativeCopy()
|
|
data, err := json.MarshalIndent(persisted, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(cfg.ConfigPath, data, 0o600)
|
|
}
|
|
|
|
func (cfg *Config) relativeCopy() Config {
|
|
next := *cfg
|
|
base := cfg.BaseDir
|
|
next.BaseDir = "."
|
|
next.StorageDir = relativePath(base, cfg.StorageDir)
|
|
next.DataDir = relativePath(base, cfg.DataDir)
|
|
next.UpdatePublicDir = relativePath(base, cfg.UpdatePublicDir)
|
|
next.UpdateNoticeDir = relativePath(base, cfg.UpdateNoticeDir)
|
|
next.DownloadsDir = relativePath(base, cfg.DownloadsDir)
|
|
next.AdminWebDir = relativePath(base, cfg.AdminWebDir)
|
|
next.PortalWebDir = relativePath(base, cfg.PortalWebDir)
|
|
next.SetupWebDir = relativePath(base, cfg.SetupWebDir)
|
|
next.LegacyUpdateDir = relativePath(base, cfg.LegacyUpdateDir)
|
|
next.LegacyFeedbackDir = relativePath(base, cfg.LegacyFeedbackDir)
|
|
next.LegacyUpdateNoticeDir = relativePath(base, cfg.LegacyUpdateNoticeDir)
|
|
next.Database.SQLitePath = relativePath(base, cfg.Database.SQLitePath)
|
|
return next
|
|
}
|
|
|
|
func relativePath(base, value string) string {
|
|
if strings.TrimSpace(value) == "" || strings.HasPrefix(strings.ToLower(value), "file:") {
|
|
return value
|
|
}
|
|
rel, err := filepath.Rel(base, value)
|
|
if err != nil || rel == "" {
|
|
return value
|
|
}
|
|
if strings.HasPrefix(rel, "..") {
|
|
return filepath.ToSlash(rel)
|
|
}
|
|
return filepath.ToSlash(rel)
|
|
}
|
|
|
|
func sanitizeNonPortablePaths(cfg *Config) {
|
|
if runtime.GOOS == "windows" {
|
|
return
|
|
}
|
|
for _, target := range []*string{
|
|
&cfg.StorageDir,
|
|
&cfg.DataDir,
|
|
&cfg.UpdatePublicDir,
|
|
&cfg.UpdateNoticeDir,
|
|
&cfg.DownloadsDir,
|
|
&cfg.AdminWebDir,
|
|
&cfg.PortalWebDir,
|
|
&cfg.SetupWebDir,
|
|
&cfg.LegacyUpdateDir,
|
|
&cfg.LegacyFeedbackDir,
|
|
&cfg.LegacyUpdateNoticeDir,
|
|
&cfg.Database.SQLitePath,
|
|
} {
|
|
if isWindowsAbsolutePath(*target) {
|
|
*target = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func shouldRewriteRelativeConfig(data []byte) bool {
|
|
var payload any
|
|
if len(data) == 0 || json.Unmarshal(data, &payload) != nil {
|
|
return false
|
|
}
|
|
return containsAbsolutePath(payload)
|
|
}
|
|
|
|
func containsAbsolutePath(value any) bool {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
for _, item := range typed {
|
|
if containsAbsolutePath(item) {
|
|
return true
|
|
}
|
|
}
|
|
case []any:
|
|
for _, item := range typed {
|
|
if containsAbsolutePath(item) {
|
|
return true
|
|
}
|
|
}
|
|
case string:
|
|
return filepath.IsAbs(typed) || isWindowsAbsolutePath(typed)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isWindowsAbsolutePath(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if len(value) >= 3 {
|
|
drive := value[0]
|
|
if ((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z')) && value[1] == ':' && (value[2] == '\\' || value[2] == '/') {
|
|
return true
|
|
}
|
|
}
|
|
return strings.HasPrefix(value, `\\`)
|
|
}
|
|
|
|
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 ""
|
|
}
|