feat(attach): 附件可配置限额、流式落盘与分阶段上传
管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,6 +39,16 @@ const (
|
||||
SettingKeyAnimChrome = "anim_chrome"
|
||||
// SettingKeyPostLinkNewTab 帖子/评论 Markdown 外链是否新标签打开;缺行视为开启(聊天链接始终新开,不受此键控制)
|
||||
SettingKeyPostLinkNewTab = "post_link_new_tab"
|
||||
// SettingKeyAttachmentExtLimit 是否限制附件扩展名;缺行视为开启
|
||||
SettingKeyAttachmentExtLimit = "attachment_ext_limit"
|
||||
// SettingKeyAttachmentExts 允许的扩展名(逗号分隔,无点);缺行=默认列表
|
||||
SettingKeyAttachmentExts = "attachment_exts"
|
||||
// SettingKeyAttachmentMaxMB 附件单文件上限(MB);缺行=20
|
||||
SettingKeyAttachmentMaxMB = "attachment_max_mb"
|
||||
// SettingKeyAttachmentMaxCount 每帖附件个数上限;缺行=10
|
||||
SettingKeyAttachmentMaxCount = "attachment_max_count"
|
||||
// SettingKeyImageMaxMB 正文插图上限(MB);缺行=5
|
||||
SettingKeyImageMaxMB = "image_max_mb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -51,8 +61,30 @@ const (
|
||||
MaxCooldownHours = 168
|
||||
MinCodeFoldLines = 0
|
||||
MaxCodeFoldLines = 100
|
||||
|
||||
DefaultAttachmentMaxMB = 20
|
||||
DefaultAttachmentMaxCount = 10
|
||||
DefaultImageMaxMB = 5
|
||||
MinAttachmentMaxMB = 1
|
||||
MinImageMaxMB = 1
|
||||
MinAttachmentMaxCount = 1
|
||||
MaxAttachmentMaxCount = 20
|
||||
MaxAttachmentExtCount = 80
|
||||
)
|
||||
|
||||
// DefaultAttachmentExts 论坛向默认允许扩展名(含安装包/脚本/网页)
|
||||
var DefaultAttachmentExts = []string{
|
||||
"pdf", "txt", "md", "csv", "json",
|
||||
"doc", "docx", "xls", "xlsx", "ppt", "pptx",
|
||||
"zip", "7z", "rar", "tar", "gz", "tgz",
|
||||
"jpg", "jpeg", "png", "webp", "gif",
|
||||
"mp3", "wav", "ogg", "mp4", "webm",
|
||||
"exe", "msi", "msp", "apk", "dmg", "iso", "deb", "rpm", "dll",
|
||||
"html", "htm", "js", "mjs", "css", "svg", "xml", "bat", "cmd", "ps1", "sh",
|
||||
}
|
||||
|
||||
var attachmentExtRe = regexp.MustCompile(`^[a-z0-9]{1,16}$`)
|
||||
|
||||
// ErrInvalidAccent 主题色格式非法
|
||||
var ErrInvalidAccent = errors.New("主题色格式不正确,应为 #RRGGBB")
|
||||
|
||||
@@ -63,21 +95,26 @@ var accentHexRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
||||
|
||||
// PublicSiteSettings 对前台公开的站点配置(SEO / 注册入口 / 发帖冷静期提示)
|
||||
type PublicSiteSettings struct {
|
||||
Accent string `json:"accent"`
|
||||
TrustReviewedPublish bool `json:"trust_reviewed_publish"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
AllowMessages bool `json:"allow_messages"`
|
||||
PostCooldownHours int `json:"post_cooldown_hours"`
|
||||
CodeBlockAutoFold bool `json:"code_block_auto_fold"`
|
||||
CodeBlockFoldLines int `json:"code_block_fold_lines"`
|
||||
UIAnimations bool `json:"ui_animations"`
|
||||
AnimCodeFold bool `json:"anim_code_fold"`
|
||||
AnimSmoothScroll bool `json:"anim_smooth_scroll"`
|
||||
AnimChrome bool `json:"anim_chrome"`
|
||||
PostLinkNewTab bool `json:"post_link_new_tab"`
|
||||
Accent string `json:"accent"`
|
||||
TrustReviewedPublish bool `json:"trust_reviewed_publish"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
AllowMessages bool `json:"allow_messages"`
|
||||
PostCooldownHours int `json:"post_cooldown_hours"`
|
||||
CodeBlockAutoFold bool `json:"code_block_auto_fold"`
|
||||
CodeBlockFoldLines int `json:"code_block_fold_lines"`
|
||||
UIAnimations bool `json:"ui_animations"`
|
||||
AnimCodeFold bool `json:"anim_code_fold"`
|
||||
AnimSmoothScroll bool `json:"anim_smooth_scroll"`
|
||||
AnimChrome bool `json:"anim_chrome"`
|
||||
PostLinkNewTab bool `json:"post_link_new_tab"`
|
||||
AttachmentExtLimit bool `json:"attachment_ext_limit"`
|
||||
AttachmentExts []string `json:"attachment_exts"`
|
||||
AttachmentMaxMB int `json:"attachment_max_mb"`
|
||||
AttachmentMaxCount int `json:"attachment_max_count"`
|
||||
ImageMaxMB int `json:"image_max_mb"`
|
||||
}
|
||||
|
||||
// SettingService 站点级键值设置
|
||||
@@ -147,6 +184,11 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
AnimSmoothScroll: true,
|
||||
AnimChrome: true,
|
||||
PostLinkNewTab: true,
|
||||
AttachmentExtLimit: true,
|
||||
AttachmentExts: append([]string(nil), DefaultAttachmentExts...),
|
||||
AttachmentMaxMB: DefaultAttachmentMaxMB,
|
||||
AttachmentMaxCount: DefaultAttachmentMaxCount,
|
||||
ImageMaxMB: DefaultImageMaxMB,
|
||||
}
|
||||
accent, err := s.AccentColor()
|
||||
if err != nil {
|
||||
@@ -244,6 +286,36 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
return out, err
|
||||
}
|
||||
out.PostLinkNewTab = linkNewTab
|
||||
|
||||
extLimit, err := s.AttachmentExtLimit()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AttachmentExtLimit = extLimit
|
||||
|
||||
exts, err := s.AttachmentExts()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AttachmentExts = exts
|
||||
|
||||
attMB, err := s.AttachmentMaxMB()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AttachmentMaxMB = attMB
|
||||
|
||||
attCount, err := s.AttachmentMaxCount()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AttachmentMaxCount = attCount
|
||||
|
||||
imgMB, err := s.ImageMaxMB()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.ImageMaxMB = imgMB
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -480,3 +552,218 @@ func (s *SettingService) PostLinkNewTab() (bool, error) {
|
||||
func (s *SettingService) SetPostLinkNewTab(on bool) error {
|
||||
return s.setBoolDefaultTrue(SettingKeyPostLinkNewTab, on)
|
||||
}
|
||||
|
||||
// AttachmentExtLimit 是否限制附件扩展名。缺行视为开启。
|
||||
func (s *SettingService) AttachmentExtLimit() (bool, error) {
|
||||
v, found, err := s.getValue(SettingKeyAttachmentExtLimit)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return parseBoolDefaultTrue(v, found), nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAttachmentExtLimit(on bool) error {
|
||||
return s.setBoolDefaultTrue(SettingKeyAttachmentExtLimit, on)
|
||||
}
|
||||
|
||||
// NormalizeAttachmentExts 规范化扩展名列表:去点、小写、去重、校验格式。
|
||||
func NormalizeAttachmentExts(raw []string) ([]string, error) {
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
e := strings.TrimSpace(strings.ToLower(item))
|
||||
e = strings.TrimPrefix(e, ".")
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if !attachmentExtRe.MatchString(e) {
|
||||
return nil, ErrInvalidSiteSetting
|
||||
}
|
||||
if _, ok := seen[e]; ok {
|
||||
continue
|
||||
}
|
||||
seen[e] = struct{}{}
|
||||
out = append(out, e)
|
||||
if len(out) > MaxAttachmentExtCount {
|
||||
return nil, ErrInvalidSiteSetting
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseExtList(v string) []string {
|
||||
parts := strings.FieldsFunc(v, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == ';' || r == '\n' || r == '\t'
|
||||
})
|
||||
out, err := NormalizeAttachmentExts(parts)
|
||||
if err != nil || len(out) == 0 {
|
||||
return append([]string(nil), DefaultAttachmentExts...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extsEqualDefault(exts []string) bool {
|
||||
if len(exts) != len(DefaultAttachmentExts) {
|
||||
return false
|
||||
}
|
||||
for i := range exts {
|
||||
if exts[i] != DefaultAttachmentExts[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AttachmentExts 允许的扩展名(无点)。缺行=默认列表;显式空串=空列表。
|
||||
func (s *SettingService) AttachmentExts() ([]string, error) {
|
||||
v, found, err := s.getValue(SettingKeyAttachmentExts)
|
||||
if err != nil {
|
||||
return append([]string(nil), DefaultAttachmentExts...), err
|
||||
}
|
||||
if !found {
|
||||
return append([]string(nil), DefaultAttachmentExts...), nil
|
||||
}
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return parseExtList(v), nil
|
||||
}
|
||||
|
||||
// SetAttachmentExts 保存扩展名列表;与默认完全一致时删键。空列表写入空串(限制开启时表示禁止上传)。
|
||||
func (s *SettingService) SetAttachmentExts(exts []string) error {
|
||||
norm, err := NormalizeAttachmentExts(exts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(norm) == 0 {
|
||||
return s.putValue(SettingKeyAttachmentExts, "")
|
||||
}
|
||||
if extsEqualDefault(norm) {
|
||||
return s.deleteKey(SettingKeyAttachmentExts)
|
||||
}
|
||||
return s.putValue(SettingKeyAttachmentExts, strings.Join(norm, ","))
|
||||
}
|
||||
|
||||
// AttachmentExtAllowed 当前设置下扩展名(含点或不含点)是否允许上传。
|
||||
func (s *SettingService) AttachmentExtAllowed(ext string) (bool, error) {
|
||||
limit, err := s.AttachmentExtLimit()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !limit {
|
||||
return true, nil
|
||||
}
|
||||
e := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(ext)), ".")
|
||||
if e == "" {
|
||||
return false, nil
|
||||
}
|
||||
list, err := s.AttachmentExts()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, x := range list {
|
||||
if x == e {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AttachmentMaxMB 附件单文件上限(MB)。缺行=20;最小 1,无硬顶。
|
||||
func (s *SettingService) AttachmentMaxMB() (int, error) {
|
||||
v, found, err := s.getValue(SettingKeyAttachmentMaxMB)
|
||||
if err != nil {
|
||||
return DefaultAttachmentMaxMB, err
|
||||
}
|
||||
if !found {
|
||||
return DefaultAttachmentMaxMB, nil
|
||||
}
|
||||
n, convErr := strconv.Atoi(strings.TrimSpace(v))
|
||||
if convErr != nil || n < MinAttachmentMaxMB {
|
||||
return DefaultAttachmentMaxMB, nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAttachmentMaxMB(mb int) error {
|
||||
if mb < MinAttachmentMaxMB {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if mb == DefaultAttachmentMaxMB {
|
||||
return s.deleteKey(SettingKeyAttachmentMaxMB)
|
||||
}
|
||||
return s.putValue(SettingKeyAttachmentMaxMB, strconv.Itoa(mb))
|
||||
}
|
||||
|
||||
// AttachmentMaxBytes 附件字节上限(按当前 MB 设置)。
|
||||
func (s *SettingService) AttachmentMaxBytes() (int64, error) {
|
||||
mb, err := s.AttachmentMaxMB()
|
||||
if err != nil {
|
||||
return int64(DefaultAttachmentMaxMB) << 20, err
|
||||
}
|
||||
return int64(mb) << 20, nil
|
||||
}
|
||||
|
||||
// AttachmentMaxCount 每帖/草稿附件个数上限。缺行=10;范围 1–20。
|
||||
func (s *SettingService) AttachmentMaxCount() (int, error) {
|
||||
v, found, err := s.getValue(SettingKeyAttachmentMaxCount)
|
||||
if err != nil {
|
||||
return DefaultAttachmentMaxCount, err
|
||||
}
|
||||
if !found {
|
||||
return DefaultAttachmentMaxCount, nil
|
||||
}
|
||||
n, convErr := strconv.Atoi(strings.TrimSpace(v))
|
||||
if convErr != nil || n < MinAttachmentMaxCount {
|
||||
return DefaultAttachmentMaxCount, nil
|
||||
}
|
||||
if n > MaxAttachmentMaxCount {
|
||||
return MaxAttachmentMaxCount, nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAttachmentMaxCount(n int) error {
|
||||
if n < MinAttachmentMaxCount || n > MaxAttachmentMaxCount {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if n == DefaultAttachmentMaxCount {
|
||||
return s.deleteKey(SettingKeyAttachmentMaxCount)
|
||||
}
|
||||
return s.putValue(SettingKeyAttachmentMaxCount, strconv.Itoa(n))
|
||||
}
|
||||
|
||||
// ImageMaxMB 正文插图上限(MB)。缺行=5;最小 1,无硬顶。
|
||||
func (s *SettingService) ImageMaxMB() (int, error) {
|
||||
v, found, err := s.getValue(SettingKeyImageMaxMB)
|
||||
if err != nil {
|
||||
return DefaultImageMaxMB, err
|
||||
}
|
||||
if !found {
|
||||
return DefaultImageMaxMB, nil
|
||||
}
|
||||
n, convErr := strconv.Atoi(strings.TrimSpace(v))
|
||||
if convErr != nil || n < MinImageMaxMB {
|
||||
return DefaultImageMaxMB, nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetImageMaxMB(mb int) error {
|
||||
if mb < MinImageMaxMB {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if mb == DefaultImageMaxMB {
|
||||
return s.deleteKey(SettingKeyImageMaxMB)
|
||||
}
|
||||
return s.putValue(SettingKeyImageMaxMB, strconv.Itoa(mb))
|
||||
}
|
||||
|
||||
// ImageMaxBytes 插图像素字节上限。
|
||||
func (s *SettingService) ImageMaxBytes() (int64, error) {
|
||||
mb, err := s.ImageMaxMB()
|
||||
if err != nil {
|
||||
return int64(DefaultImageMaxMB) << 20, err
|
||||
}
|
||||
return int64(mb) << 20, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user