spark-server/internal/module/upload/logic/credential.go
2026-09-18 14:50:41 +08:00

91 lines
2.9 KiB
Go

package logic
import (
"context"
"spark-server/internal/module/upload/entity"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/i18n"
"spark-server/internal/pkg/reqctx"
"spark-server/internal/pkg/validate"
)
// CreateInput requests a single-object Presigned PUT. The client does not choose the key.
type CreateInput struct {
Purpose string `json:"purpose" validate:"required,oneof=avatar group_avatar chat_image chat_voice chat_video chat_gif chat_file" label:"field.purpose" errmsg:"oneof:validate.upload.purpose_invalid"`
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
}
_, mime, err := matchSpec(ctx, in)
if err != nil {
return nil, err
}
objectID, err := s.ids.NextID()
if err != nil {
return nil, err
}
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, objectID, dayOf(s.now()), entity.ExtForMIME(mime))
url, err := s.store.PresignPut(ctx, key, mime, 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},
ExpiresIn: s.expiresIn(),
}, 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)
}