// Package config 提供全局配置:默认值对齐参考实现 core/settings.py, // 支持 FCB_* 环境变量覆盖默认值,再由数据库 settings KV 做运行时覆盖。 package config import ( "fmt" "os" "strconv" "strings" ) // 会话有效期边界(与参考实现保持一致:天级、可配 1~365 天)。 const ( // AdminSessionExpireDefault 默认 7 天(L8:由 30 天缩短,降低 localStorage // token 泄露后的暴露窗口;管理员可在 1~365 天内自行调整) AdminSessionExpireDefault = 7 * 24 * 60 * 60 AdminSessionExpireMin = 24 * 60 * 60 // 最小 1 天 AdminSessionExpireMax = 365 * 24 * 60 * 60 // 最大 365 天 // DefaultSQLitePath SQLite 模式默认数据库文件路径(相对运行目录,自动创建 data/)。 DefaultSQLitePath = "./data/fileshare.db" ) // 数据库驱动常量(需求 ⑧:SQLite 默认、Postgres 可选)。 const ( DBDriverSQLite = "sqlite" DBDriverPostgres = "postgres" ) // DefaultLogoURL / DefaultFaviconURL 默认 Logo 与 favicon(需求 ⑤): // v2 起默认改用前端打包的本地资源(web/src/assets/brand/logo.svg + favicon.png, // 经 Vite 产出 /assets/logo-*.svg 与 /assets/favicon-*.png)。此处留空, // GET /api/v1/config 下发空值时前端 displayLogoUrl/displayFaviconUrl 回落到本地打包资源; // 管理端仍可设置任意 URL 全站替换。 const DefaultLogoURL = "" // DefaultFaviconURL favicon/备用 Logo 默认空串(语义见 DefaultLogoURL 注释)。 const DefaultFaviconURL = "" // Config 运行时配置。Env 为 FCB_* 环境变量解析结果(进程级), // KV 为数据库 settings 键值覆盖(可被管理端动态修改)。 type Config struct { Env *EnvConfig KV map[string]any } // EnvConfig 进程级环境变量配置,仅能通过环境变量修改。 type EnvConfig struct { DBDriver string // FCB_DB_DRIVER,sqlite|postgres,默认 sqlite(需求 ⑧) DBDSN string // FCB_DB_DSN;postgres 必需;sqlite 为空时用 DefaultSQLitePath RedisAddr string // FCB_REDIS_ADDR,可选;为空时缓存降级为内存实现 RedisDB int // FCB_REDIS_DB,Redis 逻辑库号 0-15,默认 0(URL 形式地址以 URL 内库号优先) Listen string // FCB_LISTEN,监听地址,默认 :8466 StorageEngine string // FCB_STORAGE_ENGINE,local|s3|webdav,默认 local TrustedProxies []string // FCB_TRUSTED_PROXIES,逗号分隔的可信代理 CIDR } // defaults 返回与参考实现 core/settings.py DEFAULT_CONFIG 对齐的默认配置。 func defaults() map[string]any { return map[string]any{ // 存储引擎与路径 "file_storage": "local", "storage_path": "", "storageLimit": 0, // v3:存储引擎运行时可配(热切换);空=沿用 Env.StorageEngine 启动值 "storage_engine": "", "site_domain": "", // 站点信息 "name": "文件快传", "site_name": "文件快传", // 新增:管理端可自定义 "description": "开箱即用的文件快传系统", "notify_title": "系统通知", "notify_content": "欢迎使用文件快传,拖拽或粘贴即可分享文本与文件。", "page_explain": "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。", "keywords": "文件快传, 文件分享, 匿名口令分享文本, 文件", // 需求 ⑤:默认 Logo 与 favicon(空 = 前端使用打包的本地资源) "logo_url": DefaultLogoURL, "favicon_url": DefaultFaviconURL, // 需求 ①:背景图(v2 新增 background_url;background 为参考实现既有键,保留兼容) "background": "", "background_url": "", // 需求 ②:页脚自定义内容与备案号 "footer_text": "", "footer_beian": "", // 需求 ③:系统通知(notify_enabled 新增开关,title/content 沿用参考语义) "notify_enabled": 1, // 需求 ④:保存策略(次数上限新增;时间上限沿用 max_save_seconds) "max_save_count": 0, // 需求 ⑩:存储策略-单文件上限(0=回落 uploadSize,避免与参考键冲突) "max_file_size": 0, // 本地存储 "local_storage_path": "", // S3 引擎 "s3_access_key_id": "", "s3_secret_access_key": "", "s3_bucket_name": "", "s3_endpoint_url": "", "s3_region_name": "auto", "s3_signature_version": "s3v4", "s3_hostname": "", "s3_addressing_style": "auto", "s3_proxy": 0, "aws_session_token": "", // WebDAV 引擎 "webdav_url": "", "webdav_username": "", "webdav_password": "", "webdav_root_path": "filebox_storage", "webdav_proxy": 0, // 安全 "admin_token": "", // 管理员密码哈希;为空表示未初始化 "jwt_secret": "", "adminSessionExpire": AdminSessionExpireDefault, // 上传与分享策略 "openUpload": 1, "uploadSize": 1024 * 1024 * 10, "allowed_file_types": []string{"*"}, "expireStyle": []string{"day", "hour", "minute", "forever", "count"}, "code_generate_type": "secret", "uploadMinute": 1, "uploadCount": 10, "errorMinute": 1, "errorCount": 10, "loginCount": 5, "loginMinute": 15, "max_save_seconds": 0, "enableChunk": 0, // 界面 "opacity": 0.9, "showAdminAddr": 0, "robotsText": "User-agent: *\nDisallow: /", "serverWorkers": 1, "serverHost": "0.0.0.0", "serverPort": 8466, } } // loadEnv 解析 FCB_* 环境变量;返回 nil 表示未设置任何必需项。 func loadEnv() (*EnvConfig, error) { env := &EnvConfig{ DBDriver: strings.ToLower(strings.TrimSpace(os.Getenv("FCB_DB_DRIVER"))), DBDSN: strings.TrimSpace(os.Getenv("FCB_DB_DSN")), RedisAddr: strings.TrimSpace(os.Getenv("FCB_REDIS_ADDR")), Listen: strings.TrimSpace(os.Getenv("FCB_LISTEN")), StorageEngine: strings.TrimSpace(os.Getenv("FCB_STORAGE_ENGINE")), } // Redis 库号(FCB_REDIS_DB,0-15;非法值忽略用默认 0) if v := strings.TrimSpace(os.Getenv("FCB_REDIS_DB")); v != "" { if n, err := strconv.Atoi(v); err == nil && n >= 0 && n <= 15 { env.RedisDB = n } } if env.Listen == "" { env.Listen = ":8466" } if env.StorageEngine == "" { env.StorageEngine = "local" } switch env.StorageEngine { case "local", "s3", "webdav": default: return nil, fmt.Errorf("FCB_STORAGE_ENGINE 无效值 %q,仅支持 local|s3|webdav", env.StorageEngine) } if raw := strings.TrimSpace(os.Getenv("FCB_TRUSTED_PROXIES")); raw != "" { for _, item := range strings.Split(raw, ",") { if item = strings.TrimSpace(item); item != "" { env.TrustedProxies = append(env.TrustedProxies, item) } } } return env, nil } // New 从环境变量构造配置;KV 覆盖先为空。 // 需求 ⑧:FCB_DB_DRIVER 默认 sqlite(零依赖);postgres 必须提供 FCB_DB_DSN。 func New() (*Config, error) { env, err := loadEnv() if err != nil { return nil, err } switch env.DBDriver { case "", DBDriverSQLite: env.DBDriver = DBDriverSQLite // sqlite 模式 DSN 可为空:数据库层回退到 DefaultSQLitePath case DBDriverPostgres: if env.DBDSN == "" { return nil, fmt.Errorf("FCB_DB_DRIVER=postgres 时必须提供 FCB_DB_DSN(Postgres 连接串)") } default: return nil, fmt.Errorf("FCB_DB_DRIVER 无效值 %q,仅支持 sqlite|postgres", env.DBDriver) } return &Config{Env: env, KV: map[string]any{}}, nil } // ApplyKV 用数据库 settings KV 覆盖运行时配置(内部键以 _ 开头的不允许覆盖)。 func (c *Config) ApplyKV(kv map[string]any) { for k, v := range kv { if strings.HasPrefix(k, "_") { continue } c.KV[k] = v } } // Get 按 键读取:KV 覆盖 > 默认值;找不到返回零值与 false。 func (c *Config) Get(key string) (any, bool) { if v, ok := c.KV[key]; ok { return v, true } v, ok := defaults()[key] return v, ok } // GetString 取字符串配置。 func (c *Config) GetString(key string) string { v, ok := c.Get(key) if !ok || v == nil { return "" } if s, ok := v.(string); ok { return s } return fmt.Sprintf("%v", v) } // GetInt 取整型配置,兼容 JSON 数字(float64)与字符串。 func (c *Config) GetInt(key string) int { n, _ := c.getInt64(key) return int(n) } // GetInt64 取长整型配置。 func (c *Config) GetInt64(key string) int64 { n, _ := c.getInt64(key) return n } func (c *Config) getInt64(key string) (int64, bool) { v, ok := c.Get(key) if !ok || v == nil { return 0, false } switch n := v.(type) { case int: return int64(n), true case int64: return n, true case float64: return int64(n), true case string: if n, err := strconv.ParseInt(strings.TrimSpace(n), 10, 64); err == nil { return n, true } } return 0, false } // GetBool 取布尔配置,兼容 1/0、"true"/"false"/"on"/"yes"。 func (c *Config) GetBool(key string) bool { v, ok := c.Get(key) if !ok || v == nil { return false } switch b := v.(type) { case bool: return b case int: return b != 0 case float64: return b != 0 case string: switch strings.ToLower(strings.TrimSpace(b)) { case "1", "true", "on", "yes": return true } } return false } // GetStringSlice 取字符串切片配置。 // SiteDomain 站点对外域名(v3.1):空=分享链接用当前访问地址。 func (c *Config) SiteDomain() string { return strings.TrimRight(strings.TrimSpace(c.GetString("site_domain")), "/") } func (c *Config) GetStringSlice(key string) []string { v, ok := c.Get(key) if !ok || v == nil { return nil } switch s := v.(type) { case []string: return s case []any: out := make([]string, 0, len(s)) for _, item := range s { if item == nil { continue } out = append(out, fmt.Sprintf("%v", item)) } return out case string: var out []string for _, item := range strings.Split(s, ",") { if item = strings.TrimSpace(item); item != "" { out = append(out, item) } } return out } return nil } // —— 常用字段的便捷访问(与参考 settings.xxx 对齐)—— // SiteName 站点名称。 func (c *Config) SiteName() string { if v := c.GetString("site_name"); v != "" { return v } return c.GetString("name") } // LogoURL 页面 Logo。 func (c *Config) LogoURL() string { return c.GetString("logo_url") } // FaviconURL favicon 地址。 func (c *Config) FaviconURL() string { return c.GetString("favicon_url") } // OpenUpload 是否允许游客上传。 func (c *Config) OpenUpload() bool { return c.GetBool("openUpload") } // UploadSize 单文件大小上限(字节)。 func (c *Config) UploadSize() int64 { return c.GetInt64("uploadSize") } // AllowedFileTypes 允许的文件类型列表("*" 表示不限制)。 func (c *Config) AllowedFileTypes() []string { return c.GetStringSlice("allowed_file_types") } // ExpireStyle 允许的过期方式。 func (c *Config) ExpireStyle() []string { return c.GetStringSlice("expireStyle") } // EnableChunk 是否启用分片上传。 func (c *Config) EnableChunk() bool { return c.GetBool("enableChunk") } // MaxSaveSeconds 最长保存秒数,0 表示不限制。 func (c *Config) MaxSaveSeconds() int64 { return c.GetInt64("max_save_seconds") } // MaxSaveCount 单次分享最大可取次数上限(需求 ④),0 表示不限制。 func (c *Config) MaxSaveCount() int { return c.GetInt("max_save_count") } // MaxFileSize 存储策略-单文件上限(需求 ⑩);0 表示回落 uploadSize。 func (c *Config) MaxFileSize() int64 { if n := c.GetInt64("max_file_size"); n > 0 { return n } return c.UploadSize() } // FooterText 页脚自定义内容(需求 ②)。 func (c *Config) FooterText() string { return c.GetString("footer_text") } // FooterBeian 备案号(需求 ②)。 func (c *Config) FooterBeian() string { return c.GetString("footer_beian") } // BackgroundURL 背景图地址(需求 ①);空表示使用主题默认。 func (c *Config) BackgroundURL() string { if v := c.GetString("background_url"); v != "" { return v } return c.GetString("background") } // NotifyEnabled 系统通知开关(需求 ③):默认开启。 func (c *Config) NotifyEnabled() bool { if v, ok := c.Get("notify_enabled"); ok && v != nil { return c.GetBool("notify_enabled") } return true } // SQLitePath 数据库文件路径:sqlite 模式下 DSN 为空时回退默认路径(需求 ⑧)。 func (c *Config) SQLitePath() string { if c.Env.DBDriver != DBDriverSQLite { return "" } if c.Env.DBDSN != "" { return c.Env.DBDSN } return DefaultSQLitePath } // AdminSessionExpireSeconds 管理员会话有效期(秒), // 参考 apps/admin/dependencies.py 的 get_admin_session_expire_seconds。 func (c *Config) AdminSessionExpireSeconds() int { n := c.GetInt("adminSessionExpire") if n < AdminSessionExpireMin || n > AdminSessionExpireMax || n%AdminSessionExpireMin != 0 { return AdminSessionExpireDefault } return n } // Engine 当前存储引擎。 // Engine 返回当前存储引擎名:KV storage_engine 优先(v3 运行时可改), // 空(未设置/历史数据)回落启动值 Env.StorageEngine(env 校验过的 local|s3|webdav)。 // 枚举校验内联(避免 config→storage 反向依赖)。 func (c *Config) Engine() string { if v, ok := c.Get(KeyStorageEngine); ok { if s, isStr := v.(string); isStr { switch s { case "local", "s3", "webdav": return s } } } return c.Env.StorageEngine }