移除旧版 HTML 模板与兼容层,并完善私信、举报、媒体存储与 SEO。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 16:37:11 +08:00
parent 060b7707cb
commit 48db333272
121 changed files with 11147 additions and 3225 deletions

View File

@@ -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-styleMinIO 等通常为 trueAWS 官方多为 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))

View File

@@ -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 等多为 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)