管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。 Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
4.7 KiB
Go
168 lines
4.7 KiB
Go
package handler
|
||
|
||
import (
|
||
"errors"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/freefire/jiang13-bbs/middleware"
|
||
"github.com/freefire/jiang13-bbs/service"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// UploadAvatar 上传裁剪后的头像(multipart 字段 file,内容必须是 WebP)
|
||
func (h *Handlers) UploadAvatar(c *gin.Context) {
|
||
claims := middleware.CurrentUser(c)
|
||
|
||
// 限制请求体:以裁剪后文件为依据,允许少量 multipart 边界开销
|
||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, service.AvatarMaxBytes+4096)
|
||
|
||
fh, err := c.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片过大(裁剪后不能超过 2MB)或格式不正确"})
|
||
return
|
||
}
|
||
if fh.Size > service.AvatarMaxBytes {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "裁剪后的图片不能超过 2MB"})
|
||
return
|
||
}
|
||
f, err := fh.Open()
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||
return
|
||
}
|
||
defer f.Close()
|
||
|
||
// fh.Size 已受 MaxBytesReader 约束;读取时二次防御
|
||
data, err := io.ReadAll(io.LimitReader(f, service.AvatarMaxBytes+1))
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||
return
|
||
}
|
||
|
||
att, err := h.Upload.SaveAvatar(claims.ID, data)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"url": att.URL, "attachment": att})
|
||
}
|
||
|
||
// UploadImage 上传帖子插图(multipart 字段 file:JPEG / PNG / WebP)
|
||
func (h *Handlers) UploadImage(c *gin.Context) {
|
||
claims := middleware.CurrentUser(c)
|
||
|
||
maxBytes, err := h.Setting.ImageMaxBytes()
|
||
if err != nil || maxBytes < 1 {
|
||
maxBytes = service.ImageMaxBytes
|
||
}
|
||
overhead := int64(4096)
|
||
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)
|
||
|
||
fh, 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 fh.Size > maxBytes {
|
||
mb := int(maxBytes >> 20)
|
||
if mb < 1 {
|
||
mb = 1
|
||
}
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "图片不能超过 " + strconv.Itoa(mb) + "MB"})
|
||
return
|
||
}
|
||
f, err := fh.Open()
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||
return
|
||
}
|
||
defer f.Close()
|
||
|
||
att, err := h.Upload.SaveImage(claims.ID, f)
|
||
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": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"url": att.URL, "attachment": att})
|
||
}
|
||
|
||
// UseAvatarRequest 选用历史头像请求
|
||
type UseAvatarRequest struct {
|
||
URL string `json:"url"`
|
||
}
|
||
|
||
// UseAvatar 选用本人历史上传的头像
|
||
func (h *Handlers) UseAvatar(c *gin.Context) {
|
||
claims := middleware.CurrentUser(c)
|
||
var req UseAvatarRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数格式错误"})
|
||
return
|
||
}
|
||
if err := h.Upload.UseAvatar(claims.ID, req.URL); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"url": req.URL})
|
||
}
|
||
|
||
// MyMedia 媒体库:本人上传的全部图片(头像、帖子插图等)
|
||
func (h *Handlers) MyMedia(c *gin.Context) {
|
||
claims := middleware.CurrentUser(c)
|
||
list, err := h.Upload.ListMedia(claims.ID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"attachments": nonNilSlice(list)})
|
||
}
|
||
|
||
// DeleteAttachment 彻底删除本人附件(DB + 物理文件)
|
||
func (h *Handlers) DeleteAttachment(c *gin.Context) {
|
||
claims := middleware.CurrentUser(c)
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的附件 ID"})
|
||
return
|
||
}
|
||
if err := h.Upload.DeleteAttachment(claims.ID, uint(id)); err != nil {
|
||
if errors.Is(err, service.ErrAttachmentInUse) {
|
||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||
}
|