精简 app.ini:仅保留端口/数据目录/JWT,业务项改由管理后台配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 16:58:30 +08:00
parent 48db333272
commit 81f5d451bc
10 changed files with 24 additions and 431 deletions

View File

@@ -8,24 +8,12 @@ import (
"strings"
)
// StorageTypeLocal / StorageTypeS3 上传存储后端
// StorageTypeLocal / StorageTypeS3 上传存储后端(管理后台运行时配置)
const (
StorageTypeLocal = "local"
StorageTypeS3 = "s3"
)
// S3Config S3 兼容对象存储MinIO / 七牛 / 又拍 / 阿里云 OSS 等)
type S3Config struct {
Endpoint string // 例https://s3.example.com 或 s3.example.com:9000
Region string
Bucket string
AccessKey string
SecretKey string
PublicBaseURL string // 公开访问根 URL无尾斜杠上传后返回此前缀下的绝对地址
Prefix string // 对象 key 前缀(可选,如 forum/
ForcePathStyle bool // path-styleMinIO 等通常为 trueAWS 官方多为 false
}
// Config 应用全局配置:默认读工作目录下 app.ini命令行可覆盖
type Config struct {
// 工作目录(默认可执行文件所在目录)
@@ -34,23 +22,10 @@ type Config struct {
ConfigFile string
// 监听端口
Port int
// 对外公网根地址无尾斜杠OIDC Issuer 使用
RootURL string
// 数据目录SQLite、上传、日志绝对路径
DataDir string
// JWT 签名密钥
JWTSecret string
// OIDC 客户端P0写死在 app.ini供 Gitea 对接)
OAuthClientID string
OAuthClientSecret string
OAuthRedirectURIs []string
// Gitea API 同步种子(可选,运行时以管理后台为准)
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
// 上传存储local默认或 s3
StorageType string
S3 S3Config
// 日志文件路径
LogFile string
// 系统服务控制动作install|uninstall|start|stop|restart|status空表示正常运行
@@ -113,55 +88,24 @@ func Parse() (*Config, error) {
jwtSecret = fileCfg.JWTSecret
}
storageType := strings.ToLower(strings.TrimSpace(fileCfg.StorageType))
if storageType == "" {
storageType = StorageTypeLocal
}
if storageType != StorageTypeLocal && storageType != StorageTypeS3 {
return nil, fmt.Errorf("storage.TYPE 无效: %q可选 local / s3", fileCfg.StorageType)
}
cfg := &Config{
WorkPath: workPath,
ConfigFile: configFile,
Port: port,
RootURL: normalizeRootURL(fileCfg.RootURL),
DataDir: absData,
JWTSecret: jwtSecret,
OAuthClientID: fileCfg.OAuthClientID,
OAuthClientSecret: fileCfg.OAuthClientSecret,
OAuthRedirectURIs: splitCSV(fileCfg.OAuthRedirectURIs),
GiteaBaseURL: normalizeRootURL(fileCfg.GiteaBaseURL),
GiteaToken: fileCfg.GiteaToken,
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
StorageType: storageType,
S3: S3Config{
Endpoint: strings.TrimSpace(fileCfg.S3Endpoint),
Region: strings.TrimSpace(fileCfg.S3Region),
Bucket: strings.TrimSpace(fileCfg.S3Bucket),
AccessKey: strings.TrimSpace(fileCfg.S3AccessKey),
SecretKey: strings.TrimSpace(fileCfg.S3SecretKey),
PublicBaseURL: normalizeRootURL(fileCfg.S3PublicBaseURL),
Prefix: normalizeStoragePrefix(fileCfg.S3Prefix),
ForcePathStyle: fileCfg.S3ForcePathStyle,
},
WorkPath: workPath,
ConfigFile: configFile,
Port: port,
DataDir: absData,
JWTSecret: jwtSecret,
LogFile: filepath.Join(absData, "jiang13.log"),
ServiceAction: action,
}
// [storage] 仅作首次种子;运行时以管理后台为准,此处不强制校验 S3 完整性
needDirs := action == "" || action == "install"
if needDirs {
// 首次启动自动生成 app.ini便于像 Gitea 一样改文件而不记一长串参数
if !configExists {
dataRel := resolveDataRelForINI(workPath, absData)
if err := writeAppINI(configFile, fileSettings{
Port: port,
DataRel: dataRel,
StorageType: StorageTypeLocal,
S3ForcePathStyle: true,
S3Region: "us-east-1",
Port: port,
DataRel: dataRel,
}); err != nil {
return nil, fmt.Errorf("生成默认配置文件失败: %w", err)
}
@@ -174,25 +118,9 @@ func Parse() (*Config, error) {
iniJWT = jwtSecret
}
if err := writeAppINI(configFile, fileSettings{
Port: port,
DataRel: dataRel,
JWTSecret: iniJWT,
RootURL: fileCfg.RootURL,
OAuthClientID: fileCfg.OAuthClientID,
OAuthClientSecret: fileCfg.OAuthClientSecret,
OAuthRedirectURIs: fileCfg.OAuthRedirectURIs,
GiteaBaseURL: fileCfg.GiteaBaseURL,
GiteaToken: fileCfg.GiteaToken,
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
StorageType: fileCfg.StorageType,
S3Endpoint: fileCfg.S3Endpoint,
S3Region: fileCfg.S3Region,
S3Bucket: fileCfg.S3Bucket,
S3AccessKey: fileCfg.S3AccessKey,
S3SecretKey: fileCfg.S3SecretKey,
S3PublicBaseURL: fileCfg.S3PublicBaseURL,
S3Prefix: fileCfg.S3Prefix,
S3ForcePathStyle: fileCfg.S3ForcePathStyle,
Port: port,
DataRel: dataRel,
JWTSecret: iniJWT,
}); err != nil {
return nil, fmt.Errorf("更新配置文件失败: %w", err)
}
@@ -294,50 +222,6 @@ func (c *Config) FilterWordsPath() string {
return filepath.Join(c.DataDir, "filter_words.txt")
}
func normalizeRootURL(raw string) string {
u := strings.TrimSpace(raw)
u = strings.TrimRight(u, "/")
return u
}
// normalizeStoragePrefix 规范化对象 key 前缀:去首尾空白与首斜杠,非空时保证尾斜杠
func normalizeStoragePrefix(raw string) string {
p := strings.TrimSpace(raw)
p = strings.TrimPrefix(p, "/")
if p == "" {
return ""
}
return strings.TrimSuffix(p, "/") + "/"
}
func (s S3Config) validate() error {
if s.Endpoint == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 ENDPOINT")
}
if s.Bucket == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 BUCKET")
}
if s.AccessKey == "" || s.SecretKey == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 ACCESS_KEY 与 SECRET_KEY")
}
if s.PublicBaseURL == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 PUBLIC_BASE_URL公开访问根地址")
}
return nil
}
func splitCSV(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func generateRandomSecret(n int) string {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)

View File

@@ -18,34 +18,15 @@ const (
// fileSettings 从 app.ini 读出的原始值(尚未解析为绝对路径)
type fileSettings struct {
Port int
DataRel string
JWTSecret string
RootURL string
OAuthClientID string
OAuthClientSecret string
OAuthRedirectURIs string
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
StorageType string
S3Endpoint string
S3Region string
S3Bucket string
S3AccessKey string
S3SecretKey string
S3PublicBaseURL string
S3Prefix string
S3ForcePathStyle bool
Port int
DataRel string
JWTSecret string
}
func defaultFileSettings() fileSettings {
return fileSettings{
Port: defaultPort,
DataRel: defaultDataRel,
StorageType: StorageTypeLocal,
S3Region: "us-east-1",
S3ForcePathStyle: true,
Port: defaultPort,
DataRel: defaultDataRel,
}
}
@@ -69,7 +50,6 @@ func loadAppINI(path string) (fileSettings, error) {
}
out.Port = p
}
out.RootURL = strings.TrimSpace(sec.Key("ROOT_URL").String())
}
if sec, err := cfg.GetSection("paths"); err == nil {
@@ -82,36 +62,6 @@ func loadAppINI(path string) (fileSettings, error) {
out.JWTSecret = strings.TrimSpace(sec.Key("JWT_SECRET").String())
}
if sec, err := cfg.GetSection("oauth"); err == nil {
out.OAuthClientID = strings.TrimSpace(sec.Key("CLIENT_ID").String())
out.OAuthClientSecret = strings.TrimSpace(sec.Key("CLIENT_SECRET").String())
out.OAuthRedirectURIs = strings.TrimSpace(sec.Key("REDIRECT_URIS").String())
}
if sec, err := cfg.GetSection("gitea"); err == nil {
out.GiteaBaseURL = strings.TrimSpace(sec.Key("BASE_URL").String())
out.GiteaToken = strings.TrimSpace(sec.Key("TOKEN").String())
out.GiteaSyncEnabled = sec.Key("SYNC_ENABLED").MustBool(false)
}
if sec, err := cfg.GetSection("storage"); err == nil {
if v := strings.TrimSpace(sec.Key("TYPE").String()); v != "" {
out.StorageType = v
}
out.S3Endpoint = strings.TrimSpace(sec.Key("ENDPOINT").String())
if v := strings.TrimSpace(sec.Key("REGION").String()); v != "" {
out.S3Region = v
}
out.S3Bucket = strings.TrimSpace(sec.Key("BUCKET").String())
out.S3AccessKey = strings.TrimSpace(sec.Key("ACCESS_KEY").String())
out.S3SecretKey = strings.TrimSpace(sec.Key("SECRET_KEY").String())
out.S3PublicBaseURL = strings.TrimSpace(sec.Key("PUBLIC_BASE_URL").String())
out.S3Prefix = strings.TrimSpace(sec.Key("PREFIX").String())
if sec.HasKey("FORCE_PATH_STYLE") {
out.S3ForcePathStyle = sec.Key("FORCE_PATH_STYLE").MustBool(true)
}
}
return out, nil
}
@@ -121,18 +71,10 @@ func writeAppINI(path string, s fileSettings) error {
return err
}
storageType := strings.TrimSpace(s.StorageType)
if storageType == "" {
storageType = StorageTypeLocal
}
region := strings.TrimSpace(s.S3Region)
if region == "" {
region = "us-east-1"
}
var b strings.Builder
b.WriteString("; 姜十三论坛 Jiang13 Forum — 配置文件(风格类似 Gitea app.ini\n")
b.WriteString("; 修改后重启进程/服务生效。命令行参数优先级高于本文件。\n")
b.WriteString("; OIDC / 邮件 / Gitea 同步 / 对象存储等请在管理后台「系统设置」配置。\n")
b.WriteString(";\n")
b.WriteString("; 默认位置:程序工作目录下的 app.ini\n")
b.WriteString("; 可用 --config / --work-path 覆盖。\n")
@@ -140,11 +82,6 @@ func writeAppINI(path string, s fileSettings) error {
b.WriteString("[server]\n")
b.WriteString("HTTP_PORT = ")
b.WriteString(strconv.Itoa(s.Port))
b.WriteString("\n")
b.WriteString("; 对外公网根地址无尾斜杠OIDC Issuer / Discovery 依赖此项\n")
b.WriteString("; 例https://bbs.iioio.com\n")
b.WriteString("ROOT_URL = ")
b.WriteString(s.RootURL)
b.WriteString("\n\n")
b.WriteString("[paths]\n")
b.WriteString("; 相对路径相对于工作目录(默认可执行文件所在目录)\n")
@@ -155,66 +92,6 @@ func writeAppINI(path string, s fileSettings) error {
b.WriteString("; 留空则自动生成并持久化到 data/.jwt_secret勿把生产密钥提交到仓库\n")
b.WriteString("JWT_SECRET = ")
b.WriteString(s.JWTSecret)
b.WriteString("\n\n")
b.WriteString("[oauth]\n")
b.WriteString("; 作为 OIDC Provider 时,给 Gitea 等客户端使用的凭据P0 写死在配置)\n")
b.WriteString("; Gitea 认证源名称需与回调路径一致,例如名称 jiang13 对应:\n")
b.WriteString("; https://git.iioio.com/user/oauth2/jiang13/callback\n")
b.WriteString("CLIENT_ID = ")
b.WriteString(s.OAuthClientID)
b.WriteString("\n")
b.WriteString("CLIENT_SECRET = ")
b.WriteString(s.OAuthClientSecret)
b.WriteString("\n")
b.WriteString("; 多个回调用逗号分隔\n")
b.WriteString("REDIRECT_URIS = ")
b.WriteString(s.OAuthRedirectURIs)
b.WriteString("\n\n")
b.WriteString("[gitea]\n")
b.WriteString("; 可选:同步会员公开仓库到侧栏 /projects\n")
b.WriteString("BASE_URL = ")
b.WriteString(s.GiteaBaseURL)
b.WriteString("\n")
b.WriteString("TOKEN = ")
b.WriteString(s.GiteaToken)
b.WriteString("\n")
b.WriteString("SYNC_ENABLED = ")
b.WriteString(strconv.FormatBool(s.GiteaSyncEnabled))
b.WriteString("\n\n")
b.WriteString("[storage]\n")
b.WriteString("; 可选种子:日常请在管理后台「系统设置 → 对象存储」配置(保存即生效)\n")
b.WriteString("TYPE = ")
b.WriteString(storageType)
b.WriteString("\n")
b.WriteString("; 以下仅作首次种子MinIO / 七牛 / 又拍 / 阿里云 OSS 等)\n")
b.WriteString("; ENDPOINT = https://s3.example.com\n")
b.WriteString("ENDPOINT = ")
b.WriteString(s.S3Endpoint)
b.WriteString("\n")
b.WriteString("REGION = ")
b.WriteString(region)
b.WriteString("\n")
b.WriteString("BUCKET = ")
b.WriteString(s.S3Bucket)
b.WriteString("\n")
b.WriteString("ACCESS_KEY = ")
b.WriteString(s.S3AccessKey)
b.WriteString("\n")
b.WriteString("SECRET_KEY = ")
b.WriteString(s.S3SecretKey)
b.WriteString("\n")
b.WriteString("; 公开访问根 URL无尾斜杠上传后返回 PUBLIC_BASE_URL/avatars/xxx.jpg\n")
b.WriteString("; PUBLIC_BASE_URL = https://cdn.example.com/forum\n")
b.WriteString("PUBLIC_BASE_URL = ")
b.WriteString(s.S3PublicBaseURL)
b.WriteString("\n")
b.WriteString("; 对象 key 前缀(可选),如 forum/\n")
b.WriteString("PREFIX = ")
b.WriteString(s.S3Prefix)
b.WriteString("\n")
b.WriteString("; MinIO 等多为 trueAWS S3 官方多为 false\n")
b.WriteString("FORCE_PATH_STYLE = ")
b.WriteString(strconv.FormatBool(s.S3ForcePathStyle))
b.WriteString("\n")
return os.WriteFile(path, []byte(b.String()), 0644)