移除旧版 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))