thread-master/apps/backend/internal/logic/media/upload_logic.go

136 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package media
import (
"context"
"encoding/base64"
"fmt"
"path"
"strings"
"apps/backend/internal/middleware"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
const maxUploadBytes = 5 * 1024 * 1024
var allowedImageTypes = map[string]string{
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
"image/gif": ".gif",
}
type UploadLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadLogic {
return &UploadLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// Upload 解 data URL → object storage → 回公開 URLMongo 只存此 URL
func (l *UploadLogic) Upload(req *types.MediaUploadReq) (*types.MediaUploadData, error) {
if l.svcCtx.Storage == nil || !l.svcCtx.Storage.Enabled() {
return nil, response.Biz(503, 503001, "object storage not configured")
}
uid, ok := middleware.UIDFrom(l.ctx)
if !ok || uid <= 0 {
return nil, response.Biz(401, 401001, "missing authorization")
}
contentType, raw, err := decodeImageContent(req.Content)
if err != nil {
return nil, response.Biz(400, 400001, err.Error())
}
if len(raw) > maxUploadBytes {
return nil, response.Biz(400, 400001, "file size exceeds 5MB limit")
}
ext, ok := allowedImageTypes[contentType]
if !ok {
return nil, response.Biz(400, 400001, "unsupported image type (jpeg/png/webp/gif only)")
}
kind := strings.ToLower(strings.TrimSpace(req.Kind))
if kind == "" {
kind = "avatar"
}
if kind != "avatar" && kind != "other" {
kind = "other"
}
id := uuid.NewString()
key := path.Join(kind, fmt.Sprintf("%d", uid), id+ext)
url, err := l.svcCtx.Storage.Upload(l.ctx, key, raw, contentType)
if err != nil {
l.Errorf("storage upload: %v", err)
return nil, response.Biz(500, 500000, "file upload failed")
}
return &types.MediaUploadData{Url: url, Key: key}, nil
}
func decodeImageContent(content string) (contentType string, data []byte, err error) {
content = strings.TrimSpace(content)
if content == "" {
return "", nil, fmt.Errorf("empty content")
}
// data:image/jpeg;base64,....
if strings.HasPrefix(content, "data:") {
parts := strings.SplitN(content, ",", 2)
if len(parts) != 2 {
return "", nil, fmt.Errorf("invalid data url")
}
meta := parts[0] // data:image/jpeg;base64
meta = strings.TrimPrefix(meta, "data:")
semi := strings.Index(meta, ";")
if semi < 0 {
return "", nil, fmt.Errorf("invalid data url meta")
}
contentType = strings.TrimSpace(meta[:semi])
payload := parts[1]
data, err = base64.StdEncoding.DecodeString(payload)
if err != nil {
// try raw std without padding issues
data, err = base64.RawStdEncoding.DecodeString(payload)
}
if err != nil {
return "", nil, fmt.Errorf("invalid base64")
}
return contentType, data, nil
}
// pure base64 — sniff
data, err = base64.StdEncoding.DecodeString(content)
if err != nil {
return "", nil, fmt.Errorf("invalid base64")
}
contentType = sniffImage(data)
if contentType == "" {
return "", nil, fmt.Errorf("cannot detect image type")
}
return contentType, data, nil
}
func sniffImage(b []byte) string {
if len(b) >= 3 && b[0] == 0xff && b[1] == 0xd8 && b[2] == 0xff {
return "image/jpeg"
}
if len(b) >= 8 && b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4e && b[3] == 0x47 {
return "image/png"
}
if len(b) >= 6 && (string(b[:6]) == "GIF87a" || string(b[:6]) == "GIF89a") {
return "image/gif"
}
if len(b) >= 12 && string(b[0:4]) == "RIFF" && string(b[8:12]) == "WEBP" {
return "image/webp"
}
return ""
}