feat(media): 配置项与上传凭证尺寸软校验
补充 MediaConfig 默认值,并在签发上传凭证时按用途检查声明大小。
This commit is contained in:
parent
e1b3a7ebd2
commit
6cfd6309d8
@ -25,6 +25,7 @@ type Config struct {
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||
Message MessageConfig `yaml:"message"`
|
||||
ObjectStorage ObjectStorageConfig `yaml:"object_storage"`
|
||||
Media MediaConfig `yaml:"media"`
|
||||
SMS SMSConfig `yaml:"sms"`
|
||||
}
|
||||
|
||||
@ -135,6 +136,23 @@ type ObjectStorageConfig struct {
|
||||
ChatExpireDays int `yaml:"chat_expire_days"`
|
||||
}
|
||||
|
||||
// MediaConfig is server-side media detect (users) + compress (bots).
|
||||
// Client App still compresses user uploads; see docs/media.md.
|
||||
type MediaConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
UserReject bool `yaml:"user_reject"` // false=log only; true=return 40005
|
||||
BotCompress bool `yaml:"bot_compress"`
|
||||
ImageMaxEdge int `yaml:"image_max_edge"`
|
||||
VideoMaxEdge int `yaml:"video_max_edge"`
|
||||
AvatarMaxEdge int `yaml:"avatar_max_edge"`
|
||||
ImageMaxBytes int64 `yaml:"image_max_bytes"`
|
||||
VideoMaxBytes int64 `yaml:"video_max_bytes"`
|
||||
AvatarMaxBytes int64 `yaml:"avatar_max_bytes"`
|
||||
JPEGQuality int `yaml:"jpeg_quality"`
|
||||
VideoCRF int `yaml:"video_crf"`
|
||||
FFmpegPath string `yaml:"ffmpeg_path"`
|
||||
}
|
||||
|
||||
type MessageConfig struct {
|
||||
MaxContentSize int `yaml:"max_content_size"`
|
||||
SyncLimit int `yaml:"sync_limit"`
|
||||
@ -314,6 +332,42 @@ func applyDefaults(cfg *Config) {
|
||||
if cfg.RateLimit.UploadPerSecond < 1 {
|
||||
cfg.RateLimit.UploadPerSecond = 10
|
||||
}
|
||||
applyMediaDefaults(&cfg.Media)
|
||||
}
|
||||
|
||||
func applyMediaDefaults(m *MediaConfig) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Numeric/path defaults only. Toggle via YAML media.enabled / media.bot_compress
|
||||
// (configs ship with enabled: true). Explicit enabled: false stays off.
|
||||
if m.ImageMaxEdge <= 0 {
|
||||
m.ImageMaxEdge = 1080
|
||||
}
|
||||
if m.VideoMaxEdge <= 0 {
|
||||
m.VideoMaxEdge = 720
|
||||
}
|
||||
if m.AvatarMaxEdge <= 0 {
|
||||
m.AvatarMaxEdge = 200
|
||||
}
|
||||
if m.ImageMaxBytes <= 0 {
|
||||
m.ImageMaxBytes = 10 << 20
|
||||
}
|
||||
if m.VideoMaxBytes <= 0 {
|
||||
m.VideoMaxBytes = 100 << 20
|
||||
}
|
||||
if m.AvatarMaxBytes <= 0 {
|
||||
m.AvatarMaxBytes = 2 << 20
|
||||
}
|
||||
if m.JPEGQuality <= 0 {
|
||||
m.JPEGQuality = 85
|
||||
}
|
||||
if m.VideoCRF <= 0 {
|
||||
m.VideoCRF = 23
|
||||
}
|
||||
if strings.TrimSpace(m.FFmpegPath) == "" {
|
||||
m.FFmpegPath = "ffmpeg"
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnv(raw string) string {
|
||||
|
||||
@ -7,8 +7,11 @@ import (
|
||||
"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.
|
||||
@ -43,6 +46,9 @@ func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (
|
||||
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 {
|
||||
@ -69,6 +75,41 @@ func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (
|
||||
}, 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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user