移除旧版 HTML 模板与兼容层,并完善私信、举报、媒体存储与 SEO。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
101
config/config.go
101
config/config.go
@@ -8,6 +8,24 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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-style(MinIO 等通常为 true;AWS 官方多为 false)
|
||||
}
|
||||
|
||||
// Config 应用全局配置:默认读工作目录下 app.ini,命令行可覆盖
|
||||
type Config struct {
|
||||
// 工作目录(默认可执行文件所在目录)
|
||||
@@ -30,6 +48,9 @@ type Config struct {
|
||||
GiteaBaseURL string
|
||||
GiteaToken string
|
||||
GiteaSyncEnabled bool
|
||||
// 上传存储:local(默认)或 s3
|
||||
StorageType string
|
||||
S3 S3Config
|
||||
// 日志文件路径
|
||||
LogFile string
|
||||
// 系统服务控制动作:install|uninstall|start|stop|restart|status,空表示正常运行
|
||||
@@ -92,6 +113,14 @@ 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,
|
||||
@@ -105,18 +134,34 @@ func Parse() (*Config, error) {
|
||||
GiteaBaseURL: normalizeRootURL(fileCfg.GiteaBaseURL),
|
||||
GiteaToken: fileCfg.GiteaToken,
|
||||
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
|
||||
LogFile: filepath.Join(absData, "jiang13.log"),
|
||||
ServiceAction: action,
|
||||
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,
|
||||
},
|
||||
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,
|
||||
Port: port,
|
||||
DataRel: dataRel,
|
||||
StorageType: StorageTypeLocal,
|
||||
S3ForcePathStyle: true,
|
||||
S3Region: "us-east-1",
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("生成默认配置文件失败: %w", err)
|
||||
}
|
||||
@@ -136,6 +181,18 @@ func Parse() (*Config, error) {
|
||||
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,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("更新配置文件失败: %w", err)
|
||||
}
|
||||
@@ -232,27 +289,43 @@ func (c *Config) SiteUploadDir() string {
|
||||
return filepath.Join(c.DataDir, "uploads", "site")
|
||||
}
|
||||
|
||||
// UploadDir 返回头像上传目录(兼容旧调用)
|
||||
func (c *Config) UploadDir() string {
|
||||
return c.AvatarUploadDir()
|
||||
}
|
||||
|
||||
// FilterWordsPath 返回敏感词配置文件路径
|
||||
func (c *Config) FilterWordsPath() string {
|
||||
return filepath.Join(c.DataDir, "filter_words.txt")
|
||||
}
|
||||
|
||||
// OIDCEnabled 是否已配置可作为 OIDC Provider
|
||||
func (c *Config) OIDCEnabled() bool {
|
||||
return c.RootURL != "" && c.OAuthClientID != "" && c.OAuthClientSecret != "" && len(c.OAuthRedirectURIs) > 0
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -28,12 +28,24 @@ type fileSettings struct {
|
||||
GiteaBaseURL string
|
||||
GiteaToken string
|
||||
GiteaSyncEnabled bool
|
||||
StorageType string
|
||||
S3Endpoint string
|
||||
S3Region string
|
||||
S3Bucket string
|
||||
S3AccessKey string
|
||||
S3SecretKey string
|
||||
S3PublicBaseURL string
|
||||
S3Prefix string
|
||||
S3ForcePathStyle bool
|
||||
}
|
||||
|
||||
func defaultFileSettings() fileSettings {
|
||||
return fileSettings{
|
||||
Port: defaultPort,
|
||||
DataRel: defaultDataRel,
|
||||
Port: defaultPort,
|
||||
DataRel: defaultDataRel,
|
||||
StorageType: StorageTypeLocal,
|
||||
S3Region: "us-east-1",
|
||||
S3ForcePathStyle: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +94,24 @@ func loadAppINI(path string) (fileSettings, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -91,6 +121,15 @@ 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")
|
||||
@@ -130,6 +169,52 @@ func writeAppINI(path string, s fileSettings) error {
|
||||
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 等多为 true;AWS 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)
|
||||
|
||||
Reference in New Issue
Block a user