spark-server/internal/module/upload/logic/credential.go
DESKTOP-L8OUL9F\gfl0034 6cfd6309d8 feat(media): 配置项与上传凭证尺寸软校验
补充 MediaConfig 默认值,并在签发上传凭证时按用途检查声明大小。
2026-09-24 23:09:46 +08:00

138 lines
4.3 KiB
Go

package logic
import (
"context"
"strconv"
"spark-server/internal/module/upload/entity"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/i18n"
"spark-server/internal/pkg/media"
"spark-server/internal/pkg/reqctx"
"spark-server/internal/pkg/validate"
"go.uber.org/zap"
)
// CreateInput requests a single-object Presigned PUT. The client does not choose the key.
type CreateInput struct {
Purpose string `json:"purpose" validate:"required" label:"field.purpose" errmsg:"required:validate.upload.purpose_required"`
Mime string `json:"mime" validate:"required,max=128" label:"field.mime" errmsg:"required:validate.upload.mime_required;max:validate.upload.mime_invalid"`
Size int64 `json:"size" validate:"required,gt=0" label:"field.size" errmsg:"required:validate.upload.size_required;gt:validate.upload.size_invalid"`
GroupID int64 `json:"group_id" validate:"omitempty,gt=0" label:"field.group_id"`
}
func (s *Service) CreateCredential(ctx context.Context, userID int64, in *CreateInput) (*entity.Credential, error) {
if !s.ready() {
return nil, errs.ErrAvailable
}
if userID <= 0 {
return nil, errs.ErrUnauthorized
}
if in == nil {
return nil, errs.ErrInvalidArgument
}
if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil {
return nil, err
}
if !s.allow(userID) {
return nil, errs.ErrRateLimited
}
return s.issuePut(ctx, userID, in)
}
func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (*entity.Credential, error) {
_, mime, err := matchSpec(ctx, in)
if err != nil {
return nil, err
}
if err := s.checkDeclaredSize(ctx, in.Purpose, in.Size); err != nil {
return nil, err
}
name := reqctx.NewObjectName()
ownerID := userID
if in.Purpose == entity.PurposeGroupAvatar {
if err := s.readyGroupAvatar(ctx, userID, in.GroupID); err != nil {
return nil, err
}
ownerID = in.GroupID
}
key := entity.BuildObjectKey(in.Purpose, ownerID, name, dayOf(s.now()), entity.ExtForMIME(mime))
url, err := s.store.PresignPut(ctx, key, mime, in.Size, s.ttl)
if err != nil {
return nil, err
}
return &entity.Credential{
ObjectKey: key,
UploadURL: url,
Method: entity.MethodPUT,
Headers: map[string]string{
"Content-Type": mime,
"Content-Length": strconv.FormatInt(in.Size, 10),
"If-None-Match": "*",
},
ExpiresIn: s.expiresIn(),
}, nil
}
// checkDeclaredSize applies soft media policy on credential size (no dimensions at this stage).
func (s *Service) checkDeclaredSize(ctx context.Context, purpose string, size int64) error {
if s == nil || !s.media.Enabled {
return nil
}
var v *media.Violation
switch purpose {
case entity.PurposeAvatar, entity.PurposeGroupAvatar, entity.PurposeMomentCover:
v = s.media.CheckAvatarMeta(0, 0, size)
case entity.PurposeChatVideo, entity.PurposeMomentVideo, entity.PurposeSquareVideo:
v = s.media.CheckVideoMeta(0, 0, size, 0)
case entity.PurposeChatImage, entity.PurposeMomentImage, entity.PurposeSquareImage, entity.PurposeChatGIF:
v = s.media.CheckImageMeta(0, 0, size)
default:
return nil
}
if v == nil {
return nil
}
log := s.log
if log == nil {
log = zap.NewNop()
}
log.Warn("upload media size soft-check",
zap.String("purpose", purpose),
zap.String("reason", v.Reason),
zap.Int64("size", v.Size),
zap.Int64("max_size", v.MaxSize),
)
if !s.media.UserReject {
return nil
}
return errs.ErrMediaOversized.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyMediaOversized, nil))
}
func matchSpec(ctx context.Context, in *CreateInput) (entity.Spec, string, error) {
lang := reqctx.Lang(ctx)
spec, ok := entity.LookupSpec(in.Purpose)
if !ok {
return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadPurposeInvalid, nil))
}
mime := entity.NormalizeMIME(in.Mime)
if _, ok := spec.MIMEs[mime]; !ok {
return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadMIMEInvalid, nil))
}
if in.Size > spec.MaxSize {
return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadSizeExceeded, nil))
}
return spec, mime, nil
}
func (s *Service) readyGroupAvatar(ctx context.Context, userID, groupID int64) error {
if groupID <= 0 {
return errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyUploadGroupRequired, nil))
}
if s.groups == nil {
return nil
}
return s.groups.CanSetAvatar(ctx, userID, groupID)
}