feat(attach): 附件可配置限额、流式落盘与分阶段上传

管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 07:33:16 +08:00
parent 6cc434f103
commit 61bf9fb48c
24 changed files with 2513 additions and 307 deletions

View File

@@ -2,7 +2,6 @@ package handler
import (
"errors"
"io"
"net/http"
"net/url"
"path/filepath"
@@ -282,11 +281,46 @@ func (h *Handlers) AdminPointsStats(c *gin.Context) {
// UploadPostFile 上传帖子附件(草稿态)
func (h *Handlers) UploadPostFile(c *gin.Context) {
claims := middleware.CurrentUser(c)
maxBytes, err := h.Setting.AttachmentMaxBytes()
if err != nil || maxBytes < 1 {
maxBytes = service.FileMaxBytes
}
overhead := int64(64 << 10) // multipart 边界开销
limit := maxBytes + overhead
if c.Request.ContentLength > limit {
mb := int(maxBytes >> 20)
if mb < 1 {
mb = 1
}
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "附件不能超过 " + strconv.Itoa(mb) + "MB"})
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit)
file, err := c.FormFile("file")
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
mb := int(maxBytes >> 20)
if mb < 1 {
mb = 1
}
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "附件不能超过 " + strconv.Itoa(mb) + "MB"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
return
}
if file.Size > maxBytes {
mb := int(maxBytes >> 20)
if mb < 1 {
mb = 1
}
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "附件不能超过 " + strconv.Itoa(mb) + "MB"})
return
}
price, _ := strconv.Atoi(c.DefaultPostForm("price_points", "0"))
f, err := file.Open()
if err != nil {
@@ -294,13 +328,18 @@ func (h *Handlers) UploadPostFile(c *gin.Context) {
return
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, service.FileMaxBytes+1))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取文件失败"})
return
}
att, err := h.PostFile.SaveDraftFile(claims.ID, file.Filename, data, price)
att, err := h.PostFile.SaveDraftFile(claims.ID, file.Filename, f, price)
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) || errors.Is(err, service.ErrAttachmentTooLarge) {
mb := int(maxBytes >> 20)
if mb < 1 {
mb = 1
}
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "附件不能超过 " + strconv.Itoa(mb) + "MB"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -399,7 +438,14 @@ func (h *Handlers) DownloadPostAttachment(c *gin.Context) {
path := h.PostFile.FilePath(att)
h.PostFile.IncDownload(att.ID)
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(att.Name))
c.Header("Content-Type", att.MIME)
c.Header("X-Content-Type-Options", "nosniff")
c.Header("Content-Security-Policy", "sandbox")
ct := att.MIME
ext := service.ExtOfFilename(att.Name)
if service.IsActiveContentExt(ext) || ct == "" {
ct = "application/octet-stream"
}
c.Header("Content-Type", ct)
c.File(path)
_ = filepath.Base(path)
}