@@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
type SafeBrandingConfig struct {
|
||||
SiteIconURL string `json:"siteIconUrl"`
|
||||
DeveloperAvatarURL string `json:"developerAvatarUrl"`
|
||||
DeveloperName string `json:"developerName"`
|
||||
FeedbackEmail string `json:"feedbackEmail"`
|
||||
}
|
||||
|
||||
func SafeBranding(cfg BrandingConfig) SafeBrandingConfig {
|
||||
return SafeBrandingConfig{
|
||||
SiteIconURL: strings.TrimSpace(cfg.SiteIconURL),
|
||||
DeveloperAvatarURL: strings.TrimSpace(cfg.DeveloperAvatarURL),
|
||||
DeveloperName: strings.TrimSpace(firstNonEmpty(cfg.DeveloperName, "YMhut")),
|
||||
FeedbackEmail: strings.TrimSpace(firstNonEmpty(cfg.FeedbackEmail, "support@ymhut.cn")),
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeBranding(current BrandingConfig, incoming BrandingConfig) BrandingConfig {
|
||||
next := current
|
||||
if value := strings.TrimSpace(incoming.SiteIconURL); value != "" {
|
||||
next.SiteIconURL = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.DeveloperAvatarURL); value != "" {
|
||||
next.DeveloperAvatarURL = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.DeveloperName); value != "" {
|
||||
next.DeveloperName = value
|
||||
}
|
||||
if value := strings.TrimSpace(incoming.FeedbackEmail); value != "" {
|
||||
next.FeedbackEmail = value
|
||||
}
|
||||
if next.SiteIconURL == "" {
|
||||
next.SiteIconURL = "https://img.ymhut.cn/file/1782108850041_icon.webp"
|
||||
}
|
||||
if next.DeveloperAvatarURL == "" {
|
||||
next.DeveloperAvatarURL = "https://img.ymhut.cn/file/1782108780690_b_3db45f3787f19192c8de8e06bc0987ef.webp"
|
||||
}
|
||||
if next.DeveloperName == "" {
|
||||
next.DeveloperName = "YMhut"
|
||||
}
|
||||
if next.FeedbackEmail == "" {
|
||||
next.FeedbackEmail = "support@ymhut.cn"
|
||||
}
|
||||
return next
|
||||
}
|
||||
@@ -36,6 +36,8 @@ type Config struct {
|
||||
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"`
|
||||
}
|
||||
@@ -44,6 +46,11 @@ 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"`
|
||||
@@ -52,6 +59,25 @@ type DatabaseConfig struct {
|
||||
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"`
|
||||
@@ -120,6 +146,8 @@ func defaults(root string) *Config {
|
||||
Database: DatabaseConfig{
|
||||
Provider: "sqlite",
|
||||
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
|
||||
MySQLHost: "127.0.0.1",
|
||||
MySQLPort: 3306,
|
||||
FailoverEnabled: true,
|
||||
HotSyncEnabled: true,
|
||||
HealthIntervalSec: 30,
|
||||
@@ -127,6 +155,19 @@ func defaults(root string) *Config {
|
||||
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,
|
||||
@@ -184,6 +225,66 @@ func applyEnv(cfg *Config) {
|
||||
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
|
||||
}
|
||||
@@ -267,10 +368,30 @@ func normalize(root string, cfg *Config) {
|
||||
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
|
||||
}
|
||||
@@ -316,6 +437,37 @@ func normalize(root string, cfg *Config) {
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MySQLInput struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Charset string `json:"charset"`
|
||||
ParseTime bool `json:"parseTime"`
|
||||
TLS string `json:"tls"`
|
||||
}
|
||||
|
||||
type SafeDatabaseConfig struct {
|
||||
Provider string `json:"provider"`
|
||||
SQLitePath string `json:"sqlitePath"`
|
||||
MySQLDSN string `json:"mysqlDsn"`
|
||||
MySQLHost string `json:"mysqlHost"`
|
||||
MySQLPort int `json:"mysqlPort"`
|
||||
MySQLDatabase string `json:"mysqlDatabase"`
|
||||
MySQLUser string `json:"mysqlUser"`
|
||||
HasPassword bool `json:"hasPassword"`
|
||||
}
|
||||
|
||||
func BuildMySQLDSN(input MySQLInput) (string, error) {
|
||||
host := strings.TrimSpace(input.Host)
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
port := input.Port
|
||||
if port <= 0 {
|
||||
port = 3306
|
||||
}
|
||||
database := strings.TrimSpace(input.Database)
|
||||
username := strings.TrimSpace(input.Username)
|
||||
if database == "" {
|
||||
return "", errors.New("mysql database is required")
|
||||
}
|
||||
if username == "" {
|
||||
return "", errors.New("mysql username is required")
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("charset", firstNonEmpty(strings.TrimSpace(input.Charset), "utf8mb4"))
|
||||
params.Set("parseTime", strconv.FormatBool(input.ParseTime))
|
||||
if tls := strings.TrimSpace(input.TLS); tls != "" {
|
||||
params.Set("tls", tls)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", username, input.Password, host, port, database, params.Encode()), nil
|
||||
}
|
||||
|
||||
func NormalizeDatabase(baseDir string, current DatabaseConfig, incoming DatabaseConfig, keepPassword bool) (DatabaseConfig, error) {
|
||||
next := current
|
||||
structuredChanged := false
|
||||
if incoming.Provider != "" {
|
||||
next.Provider = strings.ToLower(strings.TrimSpace(incoming.Provider))
|
||||
}
|
||||
if next.Provider == "" {
|
||||
next.Provider = "sqlite"
|
||||
}
|
||||
if incoming.SQLitePath != "" {
|
||||
next.SQLitePath = incoming.SQLitePath
|
||||
}
|
||||
if next.SQLitePath != "" && !filepath.IsAbs(next.SQLitePath) && !strings.HasPrefix(strings.ToLower(next.SQLitePath), "file:") {
|
||||
next.SQLitePath = filepath.Join(baseDir, next.SQLitePath)
|
||||
}
|
||||
if incoming.MySQLHost != "" {
|
||||
next.MySQLHost = strings.TrimSpace(incoming.MySQLHost)
|
||||
structuredChanged = true
|
||||
}
|
||||
if incoming.MySQLPort > 0 {
|
||||
next.MySQLPort = incoming.MySQLPort
|
||||
structuredChanged = true
|
||||
}
|
||||
if incoming.MySQLDatabase != "" {
|
||||
next.MySQLDatabase = strings.TrimSpace(incoming.MySQLDatabase)
|
||||
structuredChanged = true
|
||||
}
|
||||
if incoming.MySQLUser != "" {
|
||||
next.MySQLUser = strings.TrimSpace(incoming.MySQLUser)
|
||||
structuredChanged = true
|
||||
}
|
||||
if incoming.MySQLPassword != "" || !keepPassword {
|
||||
next.MySQLPassword = incoming.MySQLPassword
|
||||
structuredChanged = true
|
||||
}
|
||||
if incoming.MySQLDSN != "" {
|
||||
next.MySQLDSN = strings.TrimSpace(incoming.MySQLDSN)
|
||||
}
|
||||
if next.MySQLHost == "" {
|
||||
next.MySQLHost = "127.0.0.1"
|
||||
}
|
||||
if next.MySQLPort <= 0 {
|
||||
next.MySQLPort = 3306
|
||||
}
|
||||
if next.Provider == "sqlite" {
|
||||
next.MySQLDSN = ""
|
||||
} else if next.Provider == "mysql" {
|
||||
if structuredChanged || next.MySQLDSN == "" {
|
||||
dsn, err := BuildMySQLDSN(MySQLInput{
|
||||
Host: next.MySQLHost,
|
||||
Port: next.MySQLPort,
|
||||
Database: next.MySQLDatabase,
|
||||
Username: next.MySQLUser,
|
||||
Password: next.MySQLPassword,
|
||||
Charset: "utf8mb4",
|
||||
ParseTime: true,
|
||||
})
|
||||
if err != nil {
|
||||
return DatabaseConfig{}, err
|
||||
}
|
||||
next.MySQLDSN = dsn
|
||||
}
|
||||
if strings.TrimSpace(next.MySQLDSN) == "" {
|
||||
return DatabaseConfig{}, errors.New("mysql connection is required")
|
||||
}
|
||||
} else {
|
||||
return DatabaseConfig{}, errors.New("provider must be sqlite or mysql")
|
||||
}
|
||||
if strings.TrimSpace(next.SQLitePath) == "" {
|
||||
return DatabaseConfig{}, errors.New("sqlite path is required")
|
||||
}
|
||||
if next.MaxOpenConns <= 0 {
|
||||
next.MaxOpenConns = 10
|
||||
}
|
||||
if next.MaxIdleConns <= 0 {
|
||||
next.MaxIdleConns = 4
|
||||
}
|
||||
if next.ConnMaxLifetimeSeconds <= 0 {
|
||||
next.ConnMaxLifetimeSeconds = 300
|
||||
}
|
||||
if next.HealthIntervalSec <= 0 {
|
||||
next.HealthIntervalSec = 30
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func SafeDatabase(baseDir string, cfg DatabaseConfig) SafeDatabaseConfig {
|
||||
return SafeDatabaseConfig{
|
||||
Provider: firstNonEmpty(cfg.Provider, "sqlite"),
|
||||
SQLitePath: relativeToBase(baseDir, cfg.SQLitePath),
|
||||
MySQLDSN: MaskDSN(cfg.MySQLDSN),
|
||||
MySQLHost: cfg.MySQLHost,
|
||||
MySQLPort: cfg.MySQLPort,
|
||||
MySQLDatabase: cfg.MySQLDatabase,
|
||||
MySQLUser: cfg.MySQLUser,
|
||||
HasPassword: strings.TrimSpace(cfg.MySQLPassword) != "" || dsnHasPassword(cfg.MySQLDSN),
|
||||
}
|
||||
}
|
||||
|
||||
func MaskDSN(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
at := strings.Index(value, "@")
|
||||
colon := strings.Index(value, ":")
|
||||
if at > -1 && colon > -1 && colon < at {
|
||||
return value[:colon+1] + "******" + value[at:]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func relativeToBase(base, value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return ""
|
||||
}
|
||||
if base != "" {
|
||||
if rel, err := filepath.Rel(base, value); err == nil && !strings.HasPrefix(rel, "..") && rel != "." {
|
||||
return filepath.ToSlash(rel)
|
||||
}
|
||||
}
|
||||
return filepath.ToSlash(value)
|
||||
}
|
||||
|
||||
func dsnHasPassword(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
at := strings.Index(value, "@")
|
||||
colon := strings.Index(value, ":")
|
||||
return at > -1 && colon > -1 && colon < at && colon+1 < at
|
||||
}
|
||||
Reference in New Issue
Block a user